Spring Boot: 3.4.0-M3
This is minimal code to reproduce:
package com.example.demo
import org.springframework.boot.ApplicationRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@SpringBootApplication
class DemoApplication {
@Bean
fun runner(service: DemoService) = ApplicationRunner {
try {
println(service.results())
} catch (e: DemoException) {
println("exception: ${e.message}")
}
}
}
fun main(args: Array<String>) {
runApplication<DemoApplication>(*args)
}
class DemoException(message: String?) : Exception(message)
interface DemoService {
fun results(): Map<String, Any>
}
@Service
class DemoServiceImpl : DemoService {
@Transactional(readOnly = true)
override fun results(): Map<String, Any> {
throw DemoException("sww")
}
}
Expected output:
exception: sww
Actual output:
java.lang.reflect.UndeclaredThrowableException: null
at com.example.demo.DemoServiceImpl$$SpringCGLIB$$0.results(<generated>) ~[main/:na]
at com.example.demo.DemoApplication.runner$lambda$0(DemoApplication.kt:15) ~[main/:na]
at org.springframework.boot.SpringApplication.lambda$callRunner$4(SpringApplication.java:787) ~[spring-boot-3.4.0-M3.jar:3.4.0-M3]
at org.springframework.util.function.ThrowingConsumer$1.acceptWithException(ThrowingConsumer.java:83) ~[spring-core-6.2.0-RC1.jar:6.2.0-RC1]
at org.springframework.util.function.ThrowingConsumer.accept(ThrowingConsumer.java:60) ~[spring-core-6.2.0-RC1.jar:6.2.0-RC1]
at org.springframework.util.function.ThrowingConsumer$1.accept(ThrowingConsumer.java:88) ~[spring-core-6.2.0-RC1.jar:6.2.0-RC1]
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:799) ~[spring-boot-3.4.0-M3.jar:3.4.0-M3]
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:787) ~[spring-boot-3.4.0-M3.jar:3.4.0-M3]
at org.springframework.boot.SpringApplication.lambda$callRunners$3(SpringApplication.java:775) ~[spring-boot-3.4.0-M3.jar:3.4.0-M3]
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184) ~[na:na]
at java.base/java.util.stream.SortedOps$SizedRefSortingSink.end(SortedOps.java:357) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:510) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) ~[na:na]
at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151) ~[na:na]
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:na]
at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596) ~[na:na]
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:775) ~[spring-boot-3.4.0-M3.jar:3.4.0-M3]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:325) ~[spring-boot-3.4.0-M3.jar:3.4.0-M3]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1364) ~[spring-boot-3.4.0-M3.jar:3.4.0-M3]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1353) ~[spring-boot-3.4.0-M3.jar:3.4.0-M3]
at com.example.demo.DemoApplicationKt.main(DemoApplication.kt:40) ~[main/:na]
Caused by: com.example.demo.DemoException: sww
at com.example.demo.DemoServiceImpl.results(DemoApplication.kt:36) ~[main/:na]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359) ~[spring-aop-6.2.0-RC1.jar:6.2.0-RC1]
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196) ~[spring-aop-6.2.0-RC1.jar:6.2.0-RC1]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-6.2.0-RC1.jar:6.2.0-RC1]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:771) ~[spring-aop-6.2.0-RC1.jar:6.2.0-RC1]
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:380) ~[spring-tx-6.2.0-RC1.jar:6.2.0-RC1]
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-6.2.0-RC1.jar:6.2.0-RC1]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.0-RC1.jar:6.2.0-RC1]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:771) ~[spring-aop-6.2.0-RC1.jar:6.2.0-RC1]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:724) ~[spring-aop-6.2.0-RC1.jar:6.2.0-RC1]
Comment From: wilkinsona
Thanks for the report. The proxying here is part of Spring Framework's support for @Transactional
and is out of Spring Boot's control. We'll transfer this issue to the Framework team so that they can investigate.
Comment From: razubuddy
Thanks
Comment From: sdeleuze
I can't reproduce so please provide a self-contained reproduced (including the Maven or Gradle build) via an attached archive or a link to a repository.
Comment From: razubuddy
Sure, here is demo for issue reproduction https://github.com/razubuddy/demo
This is a generated application with Spring Boot 3.4.0-M3, it uses spring-framework 6.2.0-RC1
Comment From: razubuddy
I think this is regression which was fixed once in the past https://github.com/spring-projects/spring-framework/issues/23844
Comment From: sdeleuze
@jhoeller Looks like a regression caused by #32469, I guess we need to restore the special Kotlin code path in the new implementation. Any guidance from your side?
Comment From: jhoeller
@sdeleuze that special code path propagated the exception as-is, and we're always propagating exceptions as-is there now. Is the UndeclaredThrowableException
maybe coming out of a different place within CGLIB where we need to make it Kotlin-friendly in our CGLIB fork?
As a side note: As of #32469, CglibMethodInvocation
currently does not add anything, so could be replaced by a plain ReflectiveMethodInvocation
. Unless we have to add any special checks to it for fixing this issue here.
Comment From: sdeleuze
@jhoeller Indeed, the related UndeclaredThrowableException
likely comes from the bytecode generated by EmitUtils#wrap_undeclared_throwable
invocation in UndeclaredThrowableTransformer
. We may potentially have to adapt the one in InvocationHandlerGenerator
too (not sure yet).
If I enable the Spring Boot AOT plugin to see what the CGLIB generated class looks, I see:
public final Map results() {
try {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
return var10000 != null ? (Map)var10000.intercept(this, CGLIB$results$0$Method, CGLIB$emptyArgs, CGLIB$results$0$Proxy) : super.results();
} catch (Error | RuntimeException var1) {
throw var1;
} catch (Throwable var2) {
throw new UndeclaredThrowableException(var2);
}
}
I guess we need to skip that EmitUtils#wrap_undeclared_throwable
invocation for Kotlin, but how can we detect such information in CGLIB world? KotlinDetector
dectect a Kotlin class via the presence of a kotlin.Metadata
declared annotation at class level. I don't think org.springframework.cglib.core.ClassInfo
currently provides that information, should we enrich it with that information via ClassEmitter
-> ClassVisitor#visitAnnotation
? Is it ok to just store a boolean
to limit the memory impact compared to if we were storing all the class annotation information?
Comment From: jhoeller
@sdeleuze maybe we could do that Kotlin check in our CglibAopProxy
class, conditionally calling enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader))
instead of enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader, undeclaredThrowableStrategy))
if KotlinDetector.isKotlinType(proxySuperClass)
? That would avoid modifying the actual CGLIB code, applying a different generator strategy per target class instead.