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

스크린샷 2024-06-29 오후 6 57 45

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).