spring-framework icon indicating copy to clipboard operation
spring-framework copied to clipboard

Bug in spring-aop 6.1 with kotlin and generic types.

Open Vano2776 opened this issue 1 year ago • 2 comments

Thrown java.lang.NullPointerException when call method with generic type parameters from proxied class.

Case:

interface A <T> {
    suspend fun f1(a: T)
    suspend fun f2(a: T)
}

class SomeClass

@Component
class B : A<SomeClass> {

    override suspend fun f1(a: SomeClass) {}

    @Transactional
    override suspend fun f2(a: SomeClass) {}
}

@Service
class Service(
    private val component: A<SomeClass>
) {
    suspend fun test() {
        component.f2(SomeClass()) // call success
        component.f1(SomeClass()) // throw java.lang.NullPointerException
    }
}

Stack trace:

java.lang.NullPointerException: null at java.base/java.util.Objects.requireNonNull(Objects.java:233) at
org.springframework.core.CoroutinesUtils.invokeSuspendingFunction(CoroutinesUtils.java:111) at
org.springframework.aop.support.AopUtils$KotlinDelegate.invokeSuspendingFunction(AopUtils.java:376) at
org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:351) at
org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:713)

Example solve: add bridge method resolve in https://github.com/spring-projects/spring-framework/blob/5edb4cb2c92eea9a6d1b5ef36c52d41b139c591d/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java#L720

...
if (chain.isEmpty()) {
    // We can skip creating a MethodInvocation: just invoke the target directly.
    // Note that the final invoker must be an InvokerInterceptor, so we know
    // it does nothing but a reflective operation on the target, and no hot
    // swapping or fancy proxying.
    Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
    retVal = AopUtils.invokeJoinpointUsingReflection(target, BridgeMethodResolver.findBridgedMethod(method), argsToUse);
}
else {
    // We need to create a method invocation...
    retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
}
...

Affects: <Spring Framework 6.1, Spring Boot 3.2.0>

Vano2776 avatar Jun 17 '24 09:06 Vano2776

Could you please check with the latest Boot 3.2 patch release available and provide a self contained reproducer (link to a repository or an attached archive)?

sdeleuze avatar Jun 18 '24 07:06 sdeleuze

Same issue on Spring Boot Version 3.3

@SpringBootApplication
open class BuggyKotlinApplication

open class SomeClass()

open interface A<T> {
    suspend fun nonTransactional(a: T)
    suspend fun transactional(a: T)
}


@Component
open class B : A<SomeClass> {
    val logger = org.slf4j.LoggerFactory.getLogger(this::class.java)

    override suspend fun nonTransactional(a: SomeClass) {
        logger.info("f1 called")
    }

    @Transactional
    override suspend fun transactional(a: SomeClass) {
        logger.info("f2 called")
    }
}

@Service
class SomeService(
    private val component : A<SomeClass>
) {
    suspend fun test() {
        component.nonTransactional(SomeClass())
    }

    @Transactional
    suspend fun test2() {
        component.transactional(SomeClass())
    }
}

@RestController
class SomeController(
    private val service: SomeService
) {
    @GetMapping("/")
    suspend fun test(): String {
        service.test()
        return "test"
    }
    @GetMapping("/tx")
    suspend fun tx(): String {
        service.test2()
        return "tx"
    }

    @GetMapping("/both")
    suspend fun both(): String {
        service.test()
        service.test2()
        return "both"
    }

    @GetMapping("/both-rev")
    suspend fun bothRev(): String {
        service.test2()
        service.test()
        return "both-rev"
    }
}

fun main(args: Array<String>) {
    runApplication<BuggyKotlinApplication>(*args)
}

cloudchamb3r avatar Jun 18 '24 09:06 cloudchamb3r

The issue seems to exists in all 3.2.x and 3.3.x branches. The latest version I could not reproduce this was 3.1.12. Notable change of Spring Boot 3.2.x is Kotlin 1.9.x (previously 1.8.x), however downgrading Kotlin back to 1.8.x does not seem to affect the issue in any way.

Notable change from 3.1.x to 3.2.0 was introduction of AOP support for Kotlin coroutines: https://github.com/spring-projects/spring-framework/issues/22462

My guess is that the previous support was somewhat limited / broken (it worked properly until the coroutine suspended first - which would break @Transactional for sure, but has been enough for aspects like @PreAuthorize that are fully executed before first suspend).

I understand the AOP support is somewhat limited as indicated by the issue, however for aspects like @PreAuthorize this is unfortunately regression.

gitdude1458 avatar Jul 09 '24 10:07 gitdude1458

Please provide a self-contained reproducer (link to a repository or an attached archive) as asked above.

sdeleuze avatar Jul 15 '24 08:07 sdeleuze

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

spring-projects-issues avatar Jul 22 '24 08:07 spring-projects-issues

Test example: https://github.com/Vano2776/spring-aop-issue

Vano2776 avatar Jul 28 '24 20:07 Vano2776