Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kotlin checked exception thrown from proxy service causes UndeclaredThrowableException #33585

Open
razubuddy opened this issue Sep 24, 2024 · 8 comments
Assignees
Labels
status: waiting-for-internal-feedback An issue that needs input from a member or another Spring Team theme: kotlin An issue related to Kotlin support type: regression A bug that is also a regression
Milestone

Comments

@razubuddy
Copy link

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]
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Sep 24, 2024
@wilkinsona
Copy link
Member

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.

@razubuddy
Copy link
Author

Thanks

@bclozel bclozel transferred this issue from spring-projects/spring-boot Sep 24, 2024
@sdeleuze sdeleuze self-assigned this Sep 24, 2024
@sdeleuze sdeleuze added the theme: kotlin An issue related to Kotlin support label Sep 24, 2024
@sdeleuze
Copy link
Contributor

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.

@sdeleuze sdeleuze added the status: waiting-for-feedback We need additional information before we can continue label Sep 24, 2024
@razubuddy
Copy link
Author

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

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Sep 24, 2024
@razubuddy
Copy link
Author

razubuddy commented Sep 24, 2024

I think this is regression which was fixed once in the past #23844

@sdeleuze sdeleuze added type: regression A bug that is also a regression and removed status: waiting-for-triage An issue we've not yet triaged or decided on status: feedback-provided Feedback has been provided labels Sep 25, 2024
@sdeleuze sdeleuze added this to the 6.2.0-RC2 milestone Sep 25, 2024
@sdeleuze
Copy link
Contributor

@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?

@sdeleuze sdeleuze added the status: waiting-for-internal-feedback An issue that needs input from a member or another Spring Team label Sep 25, 2024
@jhoeller
Copy link
Contributor

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

@sdeleuze
Copy link
Contributor

@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?

@sbrannen sbrannen changed the title Kotlin checked exception throwed from proxy service causes UndeclaredThrowableException Kotlin checked exception thrown from proxy service causes UndeclaredThrowableException Sep 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: waiting-for-internal-feedback An issue that needs input from a member or another Spring Team theme: kotlin An issue related to Kotlin support type: regression A bug that is also a regression
Projects
None yet
Development

No branches or pull requests

5 participants