From 06c597ed6fa8bdde6b8155a9587a9250936dd402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Wed, 30 Sep 2026 10:20:48 +0200 Subject: [PATCH] Use equality check for value class KClass comparison KClass instances representing the same class are not guaranteed to be identical, so the overridden function return type check introduced in c39edeff15 could wrongly skip unboxing, for example when a class implements an interface declaring the same suspending function. See gh-37191 --- .../aop/framework/CoroutinesUtils.java | 2 +- .../aop/framework/CglibAopProxyKotlinTests.kt | 33 +++++++++++++++++ .../JdkDynamicAopProxyKotlinTests.kt | 35 +++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/CoroutinesUtils.java b/spring-aop/src/main/java/org/springframework/aop/framework/CoroutinesUtils.java index e1b279fe572..ac00faca851 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/CoroutinesUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/CoroutinesUtils.java @@ -128,7 +128,7 @@ abstract class CoroutinesUtils { for (KFunction candidate : KClasses.getDeclaredMemberFunctions(superClass)) { if (candidate.getName().equals(function.getName()) && candidate.isSuspend() && hasSameParameterTypes(function, candidate, superType) && - candidate.getReturnType().getClassifier() != valueKClass) { + !valueKClass.equals(candidate.getReturnType().getClassifier())) { return true; } } diff --git a/spring-aop/src/test/kotlin/org/springframework/aop/framework/CglibAopProxyKotlinTests.kt b/spring-aop/src/test/kotlin/org/springframework/aop/framework/CglibAopProxyKotlinTests.kt index 5b6477c16f1..413b35c80d8 100644 --- a/spring-aop/src/test/kotlin/org/springframework/aop/framework/CglibAopProxyKotlinTests.kt +++ b/spring-aop/src/test/kotlin/org/springframework/aop/framework/CglibAopProxyKotlinTests.kt @@ -282,6 +282,28 @@ class CglibAopProxyKotlinTests { assertThat(proxy.returnValue()).isEqualTo(ValueClass("bar")) } + @Test + suspend fun proxiedSuspendedInvocationValueClassOverridingValueClass() { + val proxyFactory = ProxyFactory(ValueClassOverridingValueClassBean()) + proxyFactory.isProxyTargetClass = true + proxyFactory.addAdvice(MethodInterceptor { + ValueClass("bar") + }) + val proxy = proxyFactory.proxy as ValueClassOverridingValueClassBean + assertThat(proxy.returnValue()).isEqualTo(ValueClass("bar")) + } + + @Test + suspend fun proxiedSuspendedInvocationValueClassOverridingValueClassProceed() { + val proxyFactory = ProxyFactory(ValueClassOverridingValueClassBean()) + proxyFactory.isProxyTargetClass = true + proxyFactory.addAdvice(MethodInterceptor { + it.proceed() + }) + val proxy = proxyFactory.proxy as ValueClassOverridingValueClassBean + assertThat(proxy.returnValue()).isEqualTo(ValueClass("foo")) + } + @Test suspend fun proxiedSuspendedInvocationValueClassOverridingGeneric() { val proxyFactory = ProxyFactory(ValueClassOverridingGenericBean()) @@ -473,6 +495,17 @@ class CglibAopProxyKotlinTests { } } + interface ValueClassBean { + suspend fun returnValue(): ValueClass + } + + open class ValueClassOverridingValueClassBean : ValueClassBean { + override suspend fun returnValue(): ValueClass { + delay(10.milliseconds) + return ValueClass("foo") + } + } + interface GenericBean { suspend fun returnValue(): T } diff --git a/spring-aop/src/test/kotlin/org/springframework/aop/framework/JdkDynamicAopProxyKotlinTests.kt b/spring-aop/src/test/kotlin/org/springframework/aop/framework/JdkDynamicAopProxyKotlinTests.kt index 9c4122638b8..eb866fedb3c 100644 --- a/spring-aop/src/test/kotlin/org/springframework/aop/framework/JdkDynamicAopProxyKotlinTests.kt +++ b/spring-aop/src/test/kotlin/org/springframework/aop/framework/JdkDynamicAopProxyKotlinTests.kt @@ -252,6 +252,26 @@ class JdkDynamicAopProxyKotlinTests { assertThat(proxy.returnValue()).isEqualTo(ValueClass("bar")) } + @Test + suspend fun proxiedSuspendedInvocationValueClassOverridingValueClass() { + val proxyFactory = ProxyFactory(ValueClassOverridingValueClassBeanImpl()) + proxyFactory.addAdvice(MethodInterceptor { + ValueClass("bar") + }) + val proxy = proxyFactory.proxy as ValueClassOverridingValueClassBean + assertThat(proxy.returnValue()).isEqualTo(ValueClass("bar")) + } + + @Test + suspend fun proxiedSuspendedInvocationValueClassOverridingValueClassProceed() { + val proxyFactory = ProxyFactory(ValueClassOverridingValueClassBeanImpl()) + proxyFactory.addAdvice(MethodInterceptor { + it.proceed() + }) + val proxy = proxyFactory.proxy as ValueClassOverridingValueClassBean + assertThat(proxy.returnValue()).isEqualTo(ValueClass("foo")) + } + @Test suspend fun proxiedSuspendedInvocationValueClassOverridingGeneric() { val proxyFactory = ProxyFactory(ValueClassOverridingGenericBeanImpl()) @@ -421,6 +441,21 @@ class JdkDynamicAopProxyKotlinTests { } } + interface ValueClassBean { + suspend fun returnValue(): ValueClass + } + + interface ValueClassOverridingValueClassBean : ValueClassBean { + override suspend fun returnValue(): ValueClass + } + + class ValueClassOverridingValueClassBeanImpl : ValueClassOverridingValueClassBean { + override suspend fun returnValue(): ValueClass { + delay(10.milliseconds) + return ValueClass("foo") + } + } + interface GenericBean { suspend fun returnValue(): T }