Refine Kotlin value class contribution

Only unbox value class results when the caller expects their unboxed
representation (non-primitive underlying type, non-nullable underlying
type for nullable return types, and no overridden function with a
different return type), and add related tests.

Cache the unbox method resolution per method and make it accessible
to support non-public value classes.

Closes gh-37191
This commit is contained in:
Sébastien Deleuze
2026-09-29 16:49:17 +02:00
parent c08fd24de3
commit c39edeff15
6 changed files with 731 additions and 38 deletions
+1
View File
@@ -8,6 +8,7 @@ dependencies {
compileOnly("com.google.code.findbugs:jsr305") // for the AOP Alliance fork
optional("org.apache.commons:commons-pool2")
optional("org.aspectj:aspectjweaver")
optional("org.jetbrains.kotlin:kotlin-reflect")
optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
testFixturesImplementation(testFixtures(project(":spring-beans")))
testFixturesImplementation(testFixtures(project(":spring-core")))
@@ -17,7 +17,6 @@
package org.springframework.aop.framework;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.UndeclaredThrowableException;
@@ -52,7 +51,6 @@ import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.cglib.proxy.NoOp;
import org.springframework.cglib.transform.impl.UndeclaredThrowableStrategy;
import org.springframework.core.KotlinDetector;
import org.springframework.core.MethodParameter;
import org.springframework.core.SmartClassLoader;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -97,8 +95,6 @@ class CglibAopProxy implements AopProxy, Serializable {
private static final int INVOKE_HASHCODE = 6;
private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow";
private static final boolean COROUTINES_REACTOR_PRESENT = ClassUtils.isPresent(
"kotlinx.coroutines.reactor.MonoKt", CglibAopProxy.class.getClassLoader());
@@ -422,8 +418,7 @@ class CglibAopProxy implements AopProxy, Serializable {
* Also takes care of the conversion from {@code Mono} to Kotlin Coroutines if needed.
*/
private static @Nullable Object processReturnType(
Object proxy, @Nullable Object target, Method method, Object[] arguments, @Nullable Object returnValue) throws
NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Object proxy, @Nullable Object target, Method method, Object[] arguments, @Nullable Object returnValue) {
// Massage return value if necessary
if (returnValue != null && returnValue == target &&
@@ -438,14 +433,7 @@ class CglibAopProxy implements AopProxy, Serializable {
"Null return value from advice does not match primitive return type for: " + method);
}
if (COROUTINES_REACTOR_PRESENT && KotlinDetector.isSuspendingFunction(method)) {
Class<?> returnParameterType = new MethodParameter(method, -1).getParameterType();
if (COROUTINES_FLOW_CLASS_NAME.equals(returnParameterType.getName())) {
return CoroutinesUtils.asFlow(returnValue);
}
Object awaitResult = CoroutinesUtils.awaitSingleOrNull(returnValue, arguments[arguments.length - 1]);
return KotlinDetector.isInlineClass(returnParameterType) &&
awaitResult != null && KotlinDetector.isInlineClass(awaitResult.getClass()) ?
awaitResult.getClass().getDeclaredMethod("unbox-impl").invoke(awaitResult) : awaitResult;
return CoroutinesUtils.adaptReturnValue(method, returnValue, arguments[arguments.length - 1]);
}
return returnValue;
}
@@ -16,13 +16,32 @@
package org.springframework.aop.framework;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import kotlin.coroutines.Continuation;
import kotlin.jvm.JvmClassMappingKt;
import kotlin.reflect.KClass;
import kotlin.reflect.KFunction;
import kotlin.reflect.KParameter;
import kotlin.reflect.KType;
import kotlin.reflect.KTypeParameter;
import kotlin.reflect.full.KClasses;
import kotlin.reflect.jvm.KTypesJvm;
import kotlin.reflect.jvm.ReflectJvmMapping;
import kotlinx.coroutines.reactive.ReactiveFlowKt;
import kotlinx.coroutines.reactor.MonoKt;
import org.jspecify.annotations.Nullable;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import org.springframework.core.KotlinDetector;
import org.springframework.core.MethodParameter;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ReflectionUtils;
/**
* Package-visible class designed to avoid a hard dependency on Kotlin and Coroutines dependency at runtime.
*
@@ -31,6 +50,21 @@ import reactor.core.publisher.Mono;
*/
abstract class CoroutinesUtils {
private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow";
private static final Method NO_UNBOX_METHOD;
private static final Map<Method, Method> unboxMethodCache = new ConcurrentReferenceHashMap<>();
static {
try {
NO_UNBOX_METHOD = CoroutinesUtils.class.getDeclaredMethod("noUnboxMethod");
}
catch (NoSuchMethodException ex) {
throw new IllegalStateException("Expected method not found: " + ex);
}
}
static Object asFlow(@Nullable Object publisher) {
if (publisher instanceof Publisher<?> rsPublisher) {
return ReactiveFlowKt.asFlow(rsPublisher);
@@ -46,4 +80,109 @@ abstract class CoroutinesUtils {
(Continuation<Object>) continuation);
}
/**
* Adapt the return value of a proxied suspending function: convert it to a
* {@code Flow} or await its completion, unboxing Kotlin value classes when the
* caller expects their unboxed representation.
* @param method the suspending function
* @param returnValue the return value, typically a {@code Publisher}
* @param continuation the {@code Continuation} argument of the suspending function
* @return the adapted return value
*/
static @Nullable Object adaptReturnValue(Method method, @Nullable Object returnValue, Object continuation) {
MethodParameter returnParameter = new MethodParameter(method, -1);
Class<?> returnParameterType = returnParameter.getParameterType();
if (COROUTINES_FLOW_CLASS_NAME.equals(returnParameterType.getName())) {
return asFlow(returnValue);
}
Object result = awaitSingleOrNull(returnValue, continuation);
if (KotlinDetector.isInlineClass(returnParameterType) && returnParameterType.isInstance(result)) {
Method unboxMethod = unboxMethodCache.computeIfAbsent(method, key ->
findUnboxMethod(key, returnParameterType, returnParameter.isOptional()));
if (unboxMethod != NO_UNBOX_METHOD) {
return ReflectionUtils.invokeMethod(unboxMethod, result);
}
}
return result;
}
private static Method findUnboxMethod(Method method, Class<?> valueClass, boolean nullableReturnType) {
Method unboxMethod = ReflectionUtils.findMethod(valueClass, "unbox-impl");
if (unboxMethod == null || unboxMethod.getReturnType().isPrimitive() ||
(nullableReturnType && isUnderlyingTypeNullable(valueClass)) ||
overridesFunctionWithDifferentReturnType(method, valueClass)) {
return NO_UNBOX_METHOD;
}
ReflectionUtils.makeAccessible(unboxMethod);
return unboxMethod;
}
private static boolean overridesFunctionWithDifferentReturnType(Method method, Class<?> valueClass) {
KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method);
if (function == null) {
return false;
}
KClass<?> valueKClass = JvmClassMappingKt.getKotlinClass(valueClass);
for (KType superType : KClasses.getAllSupertypes(JvmClassMappingKt.getKotlinClass(method.getDeclaringClass()))) {
if (superType.getClassifier() instanceof KClass<?> superClass) {
for (KFunction<?> candidate : KClasses.getDeclaredMemberFunctions(superClass)) {
if (candidate.getName().equals(function.getName()) && candidate.isSuspend() &&
hasSameParameterTypes(function, candidate, superType) &&
candidate.getReturnType().getClassifier() != valueKClass) {
return true;
}
}
}
}
return false;
}
private static boolean hasSameParameterTypes(KFunction<?> function, KFunction<?> candidate, KType superType) {
List<KParameter> parameters = function.getParameters();
List<KParameter> candidateParameters = candidate.getParameters();
if (parameters.size() != candidateParameters.size()) {
return false;
}
for (int i = 1; i < parameters.size(); i++) {
KType candidateType = candidateParameters.get(i).getType();
if (candidateType.getClassifier() instanceof KTypeParameter typeParameter &&
superType.getClassifier() instanceof KClass<?> superClass) {
int index = superClass.getTypeParameters().indexOf(typeParameter);
KType argumentType = (index != -1 ? superType.getArguments().get(index).getType() : null);
if (argumentType != null) {
candidateType = argumentType;
}
}
if (!KTypesJvm.getJvmErasure(candidateType).equals(KTypesJvm.getJvmErasure(parameters.get(i).getType()))) {
return false;
}
}
return true;
}
private static boolean isUnderlyingTypeNullable(Class<?> valueClass) {
KFunction<?> constructor = Objects.requireNonNull(
KClasses.getPrimaryConstructor(JvmClassMappingKt.getKotlinClass(valueClass)));
return isNullable(constructor.getParameters().get(0).getType());
}
private static boolean isNullable(KType type) {
if (type.isMarkedNullable()) {
return true;
}
if (type.getClassifier() instanceof KTypeParameter typeParameter) {
return typeParameter.getUpperBounds().stream().anyMatch(CoroutinesUtils::isNullable);
}
return (type.getClassifier() instanceof KClass<?> kClass &&
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass)) &&
isUnderlyingTypeNullable(JvmClassMappingKt.getJavaClass(kClass)));
}
/**
* For the {@link #NO_UNBOX_METHOD} constant.
*/
@SuppressWarnings("unused")
private static void noUnboxMethod() {
}
}
@@ -35,7 +35,6 @@ import org.springframework.aop.TargetSource;
import org.springframework.aop.support.AopUtils;
import org.springframework.core.DecoratingProxy;
import org.springframework.core.KotlinDetector;
import org.springframework.core.MethodParameter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -73,8 +72,6 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa
private static final long serialVersionUID = 5531744639992436476L;
private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow";
private static final boolean COROUTINES_REACTOR_PRESENT = ClassUtils.isPresent(
"kotlinx.coroutines.reactor.MonoKt", JdkDynamicAopProxy.class.getClassLoader());
@@ -237,14 +234,7 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa
"Null return value from advice does not match primitive return type for: " + method);
}
if (COROUTINES_REACTOR_PRESENT && KotlinDetector.isSuspendingFunction(method)) {
Class<?> returnParameterType = new MethodParameter(method, -1).getParameterType();
if (COROUTINES_FLOW_CLASS_NAME.equals(returnParameterType.getName())) {
return CoroutinesUtils.asFlow(retVal);
}
Object awaitResult = CoroutinesUtils.awaitSingleOrNull(retVal, args[args.length - 1]);
return KotlinDetector.isInlineClass(returnParameterType) &&
awaitResult != null && KotlinDetector.isInlineClass(awaitResult.getClass()) ?
awaitResult.getClass().getDeclaredMethod("unbox-impl").invoke(awaitResult) : awaitResult;
return CoroutinesUtils.adaptReturnValue(method, retVal, args[args.length - 1]);
}
return retVal;
}
@@ -149,6 +149,172 @@ class CglibAopProxyKotlinTests {
assertThat(proxy.returnAny()).isEqualTo(ValueClass("bar"))
}
@Test
suspend fun proxiedSuspendedInvocationValueClassPrimitiveValue() {
val proxyFactory = ProxyFactory(TestBean())
proxyFactory.addAdvice(MethodInterceptor {
ValueClassPrimitiveValue(1)
})
val proxy = proxyFactory.proxy as TestBean
assertThat(proxy.returnValueClassPrimitiveValue()).isEqualTo(ValueClassPrimitiveValue(1))
}
@Test
suspend fun proxiedSuspendedInvocationValueClassPrimitiveValueProceed() {
val proxyFactory = ProxyFactory(TestBean())
proxyFactory.addAdvice(MethodInterceptor {
it.proceed()
})
val proxy = proxyFactory.proxy as TestBean
assertThat(proxy.returnValueClassPrimitiveValue()).isEqualTo(ValueClassPrimitiveValue(0))
}
@Test
suspend fun proxiedSuspendedInvocationNullableValueClassPrimitiveValue() {
val proxyFactory = ProxyFactory(TestBean())
proxyFactory.addAdvice(MethodInterceptor {
ValueClassPrimitiveValue(1)
})
val proxy = proxyFactory.proxy as TestBean
assertThat(proxy.returnNullableValueClassPrimitiveValue()).isEqualTo(ValueClassPrimitiveValue(1))
}
@Test
suspend fun proxiedSuspendedInvocationNullableValueClassPrimitiveValueNull() {
val proxyFactory = ProxyFactory(TestBean())
proxyFactory.addAdvice(MethodInterceptor {
null
})
val proxy = proxyFactory.proxy as TestBean
assertThat(proxy.returnNullableValueClassPrimitiveValue()).isNull()
}
@Test
suspend fun proxiedSuspendedInvocationNullableValueClassNullableValue() {
val proxyFactory = ProxyFactory(TestBean())
proxyFactory.addAdvice(MethodInterceptor {
ValueClassNullableValue("bar")
})
val proxy = proxyFactory.proxy as TestBean
assertThat(proxy.returnNullableValueClassNullableValue()).isEqualTo(ValueClassNullableValue("bar"))
}
@Test
suspend fun proxiedSuspendedInvocationNullableValueClassNullableValueNull() {
val proxyFactory = ProxyFactory(TestBean())
proxyFactory.addAdvice(MethodInterceptor {
ValueClassNullableValue(null)
})
val proxy = proxyFactory.proxy as TestBean
assertThat(proxy.returnNullableValueClassNullableValue()).isEqualTo(ValueClassNullableValue(null))
}
@Test
suspend fun proxiedSuspendedInvocationNullableGenericValueClass() {
val proxyFactory = ProxyFactory(TestBean())
proxyFactory.addAdvice(MethodInterceptor {
GenericValueClass("bar")
})
val proxy = proxyFactory.proxy as TestBean
assertThat(proxy.returnNullableGenericValueClass()).isEqualTo(GenericValueClass("bar"))
}
@Test
suspend fun proxiedSuspendedInvocationNestedValueClass() {
val proxyFactory = ProxyFactory(TestBean())
proxyFactory.addAdvice(MethodInterceptor {
NestedValueClass(ValueClass("bar"))
})
val proxy = proxyFactory.proxy as TestBean
assertThat(proxy.returnNestedValueClass()).isEqualTo(NestedValueClass(ValueClass("bar")))
}
@Test
suspend fun proxiedSuspendedInvocationNullableNestedValueClassPrimitiveValue() {
val proxyFactory = ProxyFactory(TestBean())
proxyFactory.addAdvice(MethodInterceptor {
NestedValueClassPrimitiveValue(ValueClassPrimitiveValue(1))
})
val proxy = proxyFactory.proxy as TestBean
assertThat(proxy.returnNullableNestedValueClassPrimitiveValue())
.isEqualTo(NestedValueClassPrimitiveValue(ValueClassPrimitiveValue(1)))
}
@Test
suspend fun proxiedSuspendedInvocationNullableNestedValueClassNullableValue() {
val proxyFactory = ProxyFactory(TestBean())
proxyFactory.addAdvice(MethodInterceptor {
NestedValueClassNullableValue(ValueClassNullableValue("bar"))
})
val proxy = proxyFactory.proxy as TestBean
assertThat(proxy.returnNullableNestedValueClassNullableValue())
.isEqualTo(NestedValueClassNullableValue(ValueClassNullableValue("bar")))
}
@Test
suspend fun proxiedSuspendedInvocationNullableNonNullGenericValueClass() {
val proxyFactory = ProxyFactory(TestBean())
proxyFactory.addAdvice(MethodInterceptor {
NonNullGenericValueClass("bar")
})
val proxy = proxyFactory.proxy as TestBean
assertThat(proxy.returnNullableNonNullGenericValueClass()).isEqualTo(NonNullGenericValueClass("bar"))
}
@Test
suspend fun proxiedSuspendedInvocationNullableResult() {
val proxyFactory = ProxyFactory(TestBean())
proxyFactory.addAdvice(MethodInterceptor {
Result.success("bar")
})
val proxy = proxyFactory.proxy as TestBean
assertThat(proxy.returnNullableResult()?.getOrNull()).isEqualTo("bar")
}
@Test
suspend fun proxiedSuspendedInvocationValueClassOverridingAny() {
val proxyFactory = ProxyFactory(ValueClassOverridingAnyBean())
proxyFactory.isProxyTargetClass = true
proxyFactory.addAdvice(MethodInterceptor {
ValueClass("bar")
})
val proxy = proxyFactory.proxy as ValueClassOverridingAnyBean
assertThat(proxy.returnValue()).isEqualTo(ValueClass("bar"))
}
@Test
suspend fun proxiedSuspendedInvocationValueClassOverridingGeneric() {
val proxyFactory = ProxyFactory(ValueClassOverridingGenericBean())
proxyFactory.isProxyTargetClass = true
proxyFactory.addAdvice(MethodInterceptor {
ValueClass("bar")
})
val proxy = proxyFactory.proxy as ValueClassOverridingGenericBean
assertThat(proxy.returnValue()).isEqualTo(ValueClass("bar"))
}
@Test
suspend fun proxiedSuspendedInvocationValueClassOverridingGenericParameter() {
val proxyFactory = ProxyFactory(ValueClassOverridingGenericParameterBean())
proxyFactory.isProxyTargetClass = true
proxyFactory.addAdvice(MethodInterceptor {
ValueClass("bar")
})
val proxy = proxyFactory.proxy as ValueClassOverridingGenericParameterBean
assertThat(proxy.returnValue("foo")).isEqualTo(ValueClass("bar"))
}
@Test
suspend fun proxiedSuspendedInvocationValueClassOverloadingGenericParameter() {
val proxyFactory = ProxyFactory(ValueClassOverloadingGenericParameterBean())
proxyFactory.isProxyTargetClass = true
proxyFactory.addAdvice(MethodInterceptor {
ValueClass("bar")
})
val proxy = proxyFactory.proxy as ValueClassOverloadingGenericParameterBean
assertThat(proxy.returnValue("foo").value).isEqualTo("bar")
}
open class MyKotlinBean {
open fun capitalize(value: String) = value.uppercase()
@@ -183,42 +349,172 @@ class CglibAopProxyKotlinTests {
val updatedAt: LocalDateTime? = null,
)
@Test
suspend fun proxiedSuspendedInvocationPrivateValueClass() {
val proxyFactory = ProxyFactory(PrivateValueClassBean())
proxyFactory.isProxyTargetClass = true
proxyFactory.addAdvice(MethodInterceptor {
PrivateValueClass("bar")
})
val proxy = proxyFactory.proxy as PrivateValueClassBean
assertThat(proxy.returnValue()).isEqualTo(PrivateValueClass("bar"))
}
@JvmInline
value class ValueClass(val value: String)
@JvmInline
value class ValueClassNullableValue(val value: String?)
@JvmInline
value class ValueClassPrimitiveValue(val value: Int)
@JvmInline
value class GenericValueClass<T>(val value: T)
@JvmInline
value class NestedValueClass(val value: ValueClass)
@JvmInline
value class NestedValueClassPrimitiveValue(val value: ValueClassPrimitiveValue)
@JvmInline
value class NestedValueClassNullableValue(val value: ValueClassNullableValue)
@JvmInline
value class NonNullGenericValueClass<T : Any>(val value: T)
open class TestBean {
open suspend fun returnValueClass(): ValueClass {
delay(1000.milliseconds)
delay(10.milliseconds)
return ValueClass("foo")
}
open suspend fun returnNullableValueClass(): ValueClass? {
delay(1000.milliseconds)
delay(10.milliseconds)
return null
}
open suspend fun returnValueClassNullableValue(): ValueClassNullableValue {
delay(1000.milliseconds)
delay(10.milliseconds)
return ValueClassNullableValue(null)
}
open suspend fun returnResult(): Result<String> {
delay(1000.milliseconds)
delay(10.milliseconds)
return Result.success("foo")
}
open suspend fun returnString(): String {
delay(1000.milliseconds)
delay(10.milliseconds)
return "foo"
}
open suspend fun returnAny(): Any {
delay(1000.milliseconds)
delay(10.milliseconds)
return ValueClass("foo")
}
open suspend fun returnValueClassPrimitiveValue(): ValueClassPrimitiveValue {
delay(10.milliseconds)
return ValueClassPrimitiveValue(0)
}
open suspend fun returnNullableValueClassPrimitiveValue(): ValueClassPrimitiveValue? {
delay(10.milliseconds)
return null
}
open suspend fun returnNullableValueClassNullableValue(): ValueClassNullableValue? {
delay(10.milliseconds)
return null
}
open suspend fun returnNullableGenericValueClass(): GenericValueClass<String>? {
delay(10.milliseconds)
return null
}
open suspend fun returnNestedValueClass(): NestedValueClass {
delay(10.milliseconds)
return NestedValueClass(ValueClass("foo"))
}
open suspend fun returnNullableNestedValueClassPrimitiveValue(): NestedValueClassPrimitiveValue? {
delay(10.milliseconds)
return null
}
open suspend fun returnNullableNestedValueClassNullableValue(): NestedValueClassNullableValue? {
delay(10.milliseconds)
return null
}
open suspend fun returnNullableNonNullGenericValueClass(): NonNullGenericValueClass<String>? {
delay(10.milliseconds)
return null
}
open suspend fun returnNullableResult(): Result<String>? {
delay(10.milliseconds)
return null
}
}
interface AnyBean {
suspend fun returnValue(): Any
}
open class ValueClassOverridingAnyBean : AnyBean {
override suspend fun returnValue(): ValueClass {
delay(10.milliseconds)
return ValueClass("foo")
}
}
interface GenericBean<T> {
suspend fun returnValue(): T
}
open class ValueClassOverridingGenericBean : GenericBean<ValueClass> {
override suspend fun returnValue(): ValueClass {
delay(10.milliseconds)
return ValueClass("foo")
}
}
interface GenericParameterBean<T> {
suspend fun returnValue(value: T): Any
}
open class ValueClassOverridingGenericParameterBean : GenericParameterBean<String> {
override suspend fun returnValue(value: String): ValueClass {
delay(10.milliseconds)
return ValueClass(value)
}
}
open class ValueClassOverloadingGenericParameterBean : GenericParameterBean<Int> {
override suspend fun returnValue(value: Int): Any {
delay(10.milliseconds)
return value
}
open suspend fun returnValue(value: String): ValueClass {
delay(10.milliseconds)
return ValueClass(value)
}
}
@JvmInline
private value class PrivateValueClass(val value: String)
private open class PrivateValueClassBean {
open suspend fun returnValue(): PrivateValueClass {
delay(10.milliseconds)
return PrivateValueClass("foo")
}
}
}
@@ -26,6 +26,7 @@ import kotlin.time.Duration.Companion.milliseconds
* Tests for Kotlin support in [JdkDynamicAopProxy].
*
* @author Dmitry Sulman
* @author Sebastien Deleuze
*/
class JdkDynamicAopProxyKotlinTests {
@@ -119,12 +120,182 @@ class JdkDynamicAopProxyKotlinTests {
assertThat(proxy.returnAny()).isEqualTo(ValueClass("bar"))
}
@Test
suspend fun proxiedSuspendedInvocationValueClassPrimitiveValue() {
val proxyFactory = ProxyFactory(TestBeanImpl())
proxyFactory.addAdvice(MethodInterceptor {
ValueClassPrimitiveValue(1)
})
val proxy = proxyFactory.proxy as TestBean
assertThat(proxy.returnValueClassPrimitiveValue()).isEqualTo(ValueClassPrimitiveValue(1))
}
@Test
suspend fun proxiedSuspendedInvocationValueClassPrimitiveValueProceed() {
val proxyFactory = ProxyFactory(TestBeanImpl())
proxyFactory.addAdvice(MethodInterceptor {
it.proceed()
})
val proxy = proxyFactory.proxy as TestBean
assertThat(proxy.returnValueClassPrimitiveValue()).isEqualTo(ValueClassPrimitiveValue(0))
}
@Test
suspend fun proxiedSuspendedInvocationNullableValueClassPrimitiveValue() {
val proxyFactory = ProxyFactory(TestBeanImpl())
proxyFactory.addAdvice(MethodInterceptor {
ValueClassPrimitiveValue(1)
})
val proxy = proxyFactory.proxy as TestBean
assertThat(proxy.returnNullableValueClassPrimitiveValue()).isEqualTo(ValueClassPrimitiveValue(1))
}
@Test
suspend fun proxiedSuspendedInvocationNullableValueClassPrimitiveValueNull() {
val proxyFactory = ProxyFactory(TestBeanImpl())
proxyFactory.addAdvice(MethodInterceptor {
null
})
val proxy = proxyFactory.proxy as TestBean
assertThat(proxy.returnNullableValueClassPrimitiveValue()).isNull()
}
@Test
suspend fun proxiedSuspendedInvocationNullableValueClassNullableValue() {
val proxyFactory = ProxyFactory(TestBeanImpl())
proxyFactory.addAdvice(MethodInterceptor {
ValueClassNullableValue("bar")
})
val proxy = proxyFactory.proxy as TestBean
assertThat(proxy.returnNullableValueClassNullableValue()).isEqualTo(ValueClassNullableValue("bar"))
}
@Test
suspend fun proxiedSuspendedInvocationNullableValueClassNullableValueNull() {
val proxyFactory = ProxyFactory(TestBeanImpl())
proxyFactory.addAdvice(MethodInterceptor {
ValueClassNullableValue(null)
})
val proxy = proxyFactory.proxy as TestBean
assertThat(proxy.returnNullableValueClassNullableValue()).isEqualTo(ValueClassNullableValue(null))
}
@Test
suspend fun proxiedSuspendedInvocationNullableGenericValueClass() {
val proxyFactory = ProxyFactory(TestBeanImpl())
proxyFactory.addAdvice(MethodInterceptor {
GenericValueClass("bar")
})
val proxy = proxyFactory.proxy as TestBean
assertThat(proxy.returnNullableGenericValueClass()).isEqualTo(GenericValueClass("bar"))
}
@Test
suspend fun proxiedSuspendedInvocationNestedValueClass() {
val proxyFactory = ProxyFactory(TestBeanImpl())
proxyFactory.addAdvice(MethodInterceptor {
NestedValueClass(ValueClass("bar"))
})
val proxy = proxyFactory.proxy as TestBean
assertThat(proxy.returnNestedValueClass()).isEqualTo(NestedValueClass(ValueClass("bar")))
}
@Test
suspend fun proxiedSuspendedInvocationNullableNestedValueClassPrimitiveValue() {
val proxyFactory = ProxyFactory(TestBeanImpl())
proxyFactory.addAdvice(MethodInterceptor {
NestedValueClassPrimitiveValue(ValueClassPrimitiveValue(1))
})
val proxy = proxyFactory.proxy as TestBean
assertThat(proxy.returnNullableNestedValueClassPrimitiveValue())
.isEqualTo(NestedValueClassPrimitiveValue(ValueClassPrimitiveValue(1)))
}
@Test
suspend fun proxiedSuspendedInvocationNullableNestedValueClassNullableValue() {
val proxyFactory = ProxyFactory(TestBeanImpl())
proxyFactory.addAdvice(MethodInterceptor {
NestedValueClassNullableValue(ValueClassNullableValue("bar"))
})
val proxy = proxyFactory.proxy as TestBean
assertThat(proxy.returnNullableNestedValueClassNullableValue())
.isEqualTo(NestedValueClassNullableValue(ValueClassNullableValue("bar")))
}
@Test
suspend fun proxiedSuspendedInvocationNullableNonNullGenericValueClass() {
val proxyFactory = ProxyFactory(TestBeanImpl())
proxyFactory.addAdvice(MethodInterceptor {
NonNullGenericValueClass("bar")
})
val proxy = proxyFactory.proxy as TestBean
assertThat(proxy.returnNullableNonNullGenericValueClass()).isEqualTo(NonNullGenericValueClass("bar"))
}
@Test
suspend fun proxiedSuspendedInvocationNullableResult() {
val proxyFactory = ProxyFactory(TestBeanImpl())
proxyFactory.addAdvice(MethodInterceptor {
Result.success("bar")
})
val proxy = proxyFactory.proxy as TestBean
assertThat(proxy.returnNullableResult()?.getOrNull()).isEqualTo("bar")
}
@Test
suspend fun proxiedSuspendedInvocationValueClassOverridingAny() {
val proxyFactory = ProxyFactory(ValueClassOverridingAnyBeanImpl())
proxyFactory.addAdvice(MethodInterceptor {
ValueClass("bar")
})
val proxy = proxyFactory.proxy as ValueClassOverridingAnyBean
assertThat(proxy.returnValue()).isEqualTo(ValueClass("bar"))
}
@Test
suspend fun proxiedSuspendedInvocationValueClassOverridingGeneric() {
val proxyFactory = ProxyFactory(ValueClassOverridingGenericBeanImpl())
proxyFactory.addAdvice(MethodInterceptor {
ValueClass("bar")
})
val proxy = proxyFactory.proxy as ValueClassOverridingGenericBean
assertThat(proxy.returnValue()).isEqualTo(ValueClass("bar"))
}
@Test
suspend fun proxiedSuspendedInvocationPrivateValueClass() {
val proxyFactory = ProxyFactory(PrivateValueClassBeanImpl())
proxyFactory.addAdvice(MethodInterceptor {
PrivateValueClass("bar")
})
val proxy = proxyFactory.proxy as PrivateValueClassBean
assertThat(proxy.returnValue()).isEqualTo(PrivateValueClass("bar"))
}
@JvmInline
value class ValueClass(val value: String)
@JvmInline
value class ValueClassNullableValue(val value: String?)
@JvmInline
value class ValueClassPrimitiveValue(val value: Int)
@JvmInline
value class GenericValueClass<T>(val value: T)
@JvmInline
value class NestedValueClass(val value: ValueClass)
@JvmInline
value class NestedValueClassPrimitiveValue(val value: ValueClassPrimitiveValue)
@JvmInline
value class NestedValueClassNullableValue(val value: ValueClassNullableValue)
@JvmInline
value class NonNullGenericValueClass<T : Any>(val value: T)
interface TestBean {
suspend fun returnValueClass(): ValueClass
@@ -137,38 +308,146 @@ class JdkDynamicAopProxyKotlinTests {
suspend fun returnString(): String
suspend fun returnAny(): Any
suspend fun returnValueClassPrimitiveValue(): ValueClassPrimitiveValue
suspend fun returnNullableValueClassPrimitiveValue(): ValueClassPrimitiveValue?
suspend fun returnNullableValueClassNullableValue(): ValueClassNullableValue?
suspend fun returnNullableGenericValueClass(): GenericValueClass<String>?
suspend fun returnNestedValueClass(): NestedValueClass
suspend fun returnNullableNestedValueClassPrimitiveValue(): NestedValueClassPrimitiveValue?
suspend fun returnNullableNestedValueClassNullableValue(): NestedValueClassNullableValue?
suspend fun returnNullableNonNullGenericValueClass(): NonNullGenericValueClass<String>?
suspend fun returnNullableResult(): Result<String>?
}
class TestBeanImpl : TestBean {
override suspend fun returnValueClass(): ValueClass {
delay(1000.milliseconds)
delay(10.milliseconds)
return ValueClass("foo")
}
override suspend fun returnNullableValueClass(): ValueClass? {
delay(1000.milliseconds)
delay(10.milliseconds)
return null
}
override suspend fun returnValueClassNullableValue(): ValueClassNullableValue {
delay(1000.milliseconds)
delay(10.milliseconds)
return ValueClassNullableValue(null)
}
override suspend fun returnResult(): Result<String> {
delay(1000.milliseconds)
delay(10.milliseconds)
return Result.success("foo")
}
override suspend fun returnString(): String {
delay(1000.milliseconds)
delay(10.milliseconds)
return "foo"
}
override suspend fun returnAny(): Any {
delay(1000.milliseconds)
delay(10.milliseconds)
return ValueClass("foo")
}
override suspend fun returnValueClassPrimitiveValue(): ValueClassPrimitiveValue {
delay(10.milliseconds)
return ValueClassPrimitiveValue(0)
}
override suspend fun returnNullableValueClassPrimitiveValue(): ValueClassPrimitiveValue? {
delay(10.milliseconds)
return null
}
override suspend fun returnNullableValueClassNullableValue(): ValueClassNullableValue? {
delay(10.milliseconds)
return null
}
override suspend fun returnNullableGenericValueClass(): GenericValueClass<String>? {
delay(10.milliseconds)
return null
}
override suspend fun returnNestedValueClass(): NestedValueClass {
delay(10.milliseconds)
return NestedValueClass(ValueClass("foo"))
}
override suspend fun returnNullableNestedValueClassPrimitiveValue(): NestedValueClassPrimitiveValue? {
delay(10.milliseconds)
return null
}
override suspend fun returnNullableNestedValueClassNullableValue(): NestedValueClassNullableValue? {
delay(10.milliseconds)
return null
}
override suspend fun returnNullableNonNullGenericValueClass(): NonNullGenericValueClass<String>? {
delay(10.milliseconds)
return null
}
override suspend fun returnNullableResult(): Result<String>? {
delay(10.milliseconds)
return null
}
}
interface AnyBean {
suspend fun returnValue(): Any
}
interface ValueClassOverridingAnyBean : AnyBean {
override suspend fun returnValue(): ValueClass
}
class ValueClassOverridingAnyBeanImpl : ValueClassOverridingAnyBean {
override suspend fun returnValue(): ValueClass {
delay(10.milliseconds)
return ValueClass("foo")
}
}
}
interface GenericBean<T> {
suspend fun returnValue(): T
}
interface ValueClassOverridingGenericBean : GenericBean<ValueClass> {
override suspend fun returnValue(): ValueClass
}
class ValueClassOverridingGenericBeanImpl : ValueClassOverridingGenericBean {
override suspend fun returnValue(): ValueClass {
delay(10.milliseconds)
return ValueClass("foo")
}
}
@JvmInline
private value class PrivateValueClass(val value: String)
private interface PrivateValueClassBean {
suspend fun returnValue(): PrivateValueClass
}
private class PrivateValueClassBeanImpl : PrivateValueClassBean {
override suspend fun returnValue(): PrivateValueClass {
delay(10.milliseconds)
return PrivateValueClass("foo")
}
}
}