spring-framework
spring-framework copied to clipboard
Bug in spring-aop 6.1 with kotlin and generic types.
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>
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)?
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)
}
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.
Please provide a self-contained reproducer (link to a repository or an attached archive) as asked above.
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.
Test example: https://github.com/Vano2776/spring-aop-issue