spring boot version 3.3.0
When using Spring Coroutine AOP, if the @transactional annotation and a custom AOP are applied together without specifying an order in the Aspect, there is an issue where the custom AOP does not get applied.
@Service
class TargetService() {
private val log = KotlinLogging.logger { }
@Logging
@Transactional
suspend fun aop(): String {
delay(100)
log.info { "aop target method call" }
return "ok"
}
}
@Aspect
//@Order(1)
@Component
class LoggingAspect {
private val log = KotlinLogging.logger {}
@Around("@annotation(com.example.aopwithtransaction.aop.Logging)")
fun logging(joinPoint: ProceedingJoinPoint): Any? {
return mono {
log.info { "Aop Logging started" }
val result = joinPoint.proceed().let { result ->
if (result is Mono<*>) {
result.awaitSingleOrNull()
} else {
result
}
}
log.info { "Aop Logging completed" }
result
}
}
}
In cases like the one above, the custom AOP does not function unless the order is specified, in which case it does function.
This is a simple example : https://github.com/backtony/spring-reactive-aop-transaction
Comment From: sdeleuze
It is likely coming from the fact using mono { }
loses the CoroutineContext
. I would suggest to write your aspect with Reactive operators if you are not familiar with those advanced concepts, for example this should work even without @Order(1)
:
@Around("@annotation(com.example.aopwithtransaction.aop.Logging)")
fun logging(joinPoint: ProceedingJoinPoint): Any? {
log.info { "Aop Logging started" }
return (joinPoint.proceed(joinPoint.args) as Mono<*>).doOnTerminate {
log.info { "Aop Logging completed" }
}
}
As a consequence, I am closing the issue as invalid.
Comment From: backtony
@sdeleuze Thank you for your response.
@Around("@annotation(com.example.aopwithtransaction.aop.Logging)")
fun logging(joinPoint: ProceedingJoinPoint): Any? {
log.info { "Aop Logging started" }
return (joinPoint.proceed(joinPoint.args) as Mono<*>).doOnTerminate {
log.info { "Aop Logging completed" }
}
}
Even when using the code you provided, applying both @Transactional and @Logging annotations on a method results in only @Transactional being applied, and the @Logging AOP is not applied.
The @Logging AOP only works when @Order is also added to the code you provided.
Is this really an issue related to the use of Mono? The same issue occurs even when using coroutines.
@Service
class TargetService() {
private val log = KotlinLogging.logger { }
@CoroutineLogging
@Transactional
suspend fun coroutineAop(): String {
delay(100)
log.info { "aop target method call" }
return "ok"
}
}
@Aspect
//@Order(1)
@Component
class LoggingAspect {
private val log = KotlinLogging.logger {}
@Around("@annotation(com.example.aopwithtransaction.aop.CoroutineLogging)")
fun coroutineLogging(joinPoint: ProceedingJoinPoint): Any? {
return joinPoint.runCoroutine {
log.info { "coroutine logging" }
val result = joinPoint.proceedCoroutine().let { result ->
if (result is Mono<*>) {
result.awaitSingleOrNull()
} else {
result
}
}
log.info { "Coroutine logging" }
result
}
}
}
fun ProceedingJoinPoint.runCoroutine(
block: suspend () -> Any?,
): Any? = block.startCoroutineUninterceptedOrReturn(this.coroutineContinuation())
@Suppress("UNCHECKED_CAST")
fun ProceedingJoinPoint.coroutineContinuation(): Continuation<Any?> {
return this.args.last() as Continuation<Any?>
}
fun ProceedingJoinPoint.coroutineArgs(): Array<Any?> {
return this.args.sliceArray(0 until this.args.size - 1)
}
suspend fun ProceedingJoinPoint.proceedCoroutine(
args: Array<Any?> = this.coroutineArgs(),
): Any? = suspendCoroutineUninterceptedOrReturn { continuation ->
this.proceed(args + continuation)
}
Comment From: sdeleuze
I checked locally with:
@GetMapping("/aop")
suspend fun aop(): String {
delay(100)
return targetService.aop().awaitSingle()
}
And
@Aspect
@Component
class LoggingAspect {
private val log = KotlinLogging.logger {}
@Around("@annotation(com.example.aopwithtransaction.aop.Logging)")
fun logging(joinPoint: ProceedingJoinPoint): Any? {
log.info { "Aop Logging started" }
return (joinPoint.proceed(joinPoint.args) as Mono<*>).doOnTerminate {
log.info { "Aop Logging completed" }
}
}
}
And the logging is working.
Comment From: backtony
@sdeleuze Thank you for your response.
Since TargetService.aop() is a suspend function, awaitSingle is not possible.
The reason AOP logging is printed in your local environment is probably because the targetService.aop() method did not have @Transactional.
When both @Transactional and @Logging are attached to targetService.aop() as shown below, only @Transactional is applied, and AOP for logging is not applied.
Please check the sample I provided again. https://github.com/backtony/spring-reactive-aop-transaction
@RestController
class AopController(
private val targetService: TargetService,
) {
@GetMapping("/aop")
suspend fun aop(): String {
return targetService.aop()
}
}
@Service
class TargetService() {
private val log = KotlinLogging.logger { }
@Logging
@Transactional
suspend fun aop(): String {
delay(100)
log.info { "aop target method call" }
return "ok"
}
}
@Aspect
@Component
class LoggingAspect() {
private val log = KotlinLogging.logger {}
@Around("@annotation(com.example.aopwithtransaction.aop.Logging)")
fun logging(joinPoint: ProceedingJoinPoint): Any? {
log.info { "Aop Logging started" }
return (joinPoint.proceed(joinPoint.args) as Mono<*>).doOnTerminate {
log.info { "Aop Logging completed" }
}
}
}
result
Comment From: sdeleuze
Thanks for the updated sample, I can indeed reproduce (only with a suspending function, a function returning Mono<String>
works as expected).