Skip to content

Commit

Permalink
Instantiate value class parameters with Kotlin reflection
Browse files Browse the repository at this point in the history
In order to invoke the init block and to improve the maintainability.

Closes spring-projectsgh-32324
  • Loading branch information
sdeleuze committed Mar 1, 2024
1 parent 7f916e0 commit 16b9583
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;
import java.util.Objects;

Expand All @@ -30,6 +29,7 @@
import kotlin.reflect.KFunction;
import kotlin.reflect.KParameter;
import kotlin.reflect.full.KCallables;
import kotlin.reflect.full.KClasses;
import kotlin.reflect.jvm.KCallablesJvm;
import kotlin.reflect.jvm.ReflectJvmMapping;
import kotlinx.coroutines.BuildersKt;
Expand All @@ -46,7 +46,6 @@

import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;

/**
* Utilities for working with Kotlin Coroutines.
Expand All @@ -57,9 +56,6 @@
*/
public abstract class CoroutinesUtils {

private static final ReflectionUtils.MethodFilter boxImplFilter =
(method -> method.isSynthetic() && Modifier.isStatic(method.getModifiers()) && method.getName().equals("box-impl"));

/**
* Convert a {@link Deferred} instance to a {@link Mono}.
*/
Expand Down Expand Up @@ -123,10 +119,7 @@ public static Publisher<?> invokeSuspendingFunction(CoroutineContext context, Me
if (parameter.getType().getClassifier() instanceof KClass<?> kClass) {
Class<?> javaClass = JvmClassMappingKt.getJavaClass(kClass);
if (KotlinDetector.isInlineClass(javaClass)) {
Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(javaClass, boxImplFilter);
Assert.state(methods.length == 1,
"Unable to find a single box-impl synthetic static method in " + javaClass.getName());
argMap.put(parameter, ReflectionUtils.invokeMethod(methods[0], null, args[index]));
argMap.put(parameter, KClasses.getPrimaryConstructor(kClass).call(args[index]));
}
else {
argMap.put(parameter, args[index]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,17 @@ class CoroutinesUtilsTests {
}
}

@Test
fun invokeSuspendingFunctionWithValueClassWithInitParameter() {
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithValueClassWithInit") }
val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, "", null) as Mono
Assertions.assertThatIllegalArgumentException().isThrownBy {
runBlocking {
mono.awaitSingle()
}
}
}

@Test
fun invokeSuspendingFunctionWithExtension() {
val method = CoroutinesUtilsTests::class.java.getDeclaredMethod("suspendingFunctionWithExtension",
Expand Down Expand Up @@ -206,6 +217,11 @@ class CoroutinesUtilsTests {
return value.value
}

suspend fun suspendingFunctionWithValueClassWithInit(value: ValueClassWithInit): String {
delay(1)
return value.value
}

suspend fun CustomException.suspendingFunctionWithExtension(): String {
delay(1)
return "${this.message}"
Expand All @@ -219,6 +235,15 @@ class CoroutinesUtilsTests {
@JvmInline
value class ValueClass(val value: String)

@JvmInline
value class ValueClassWithInit(val value: String) {
init {
if (value.isEmpty()) {
throw IllegalArgumentException()
}
}
}

class CustomException(message: String) : Throwable(message)

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Map;

Expand All @@ -27,6 +26,7 @@
import kotlin.reflect.KClass;
import kotlin.reflect.KFunction;
import kotlin.reflect.KParameter;
import kotlin.reflect.full.KClasses;
import kotlin.reflect.jvm.KCallablesJvm;
import kotlin.reflect.jvm.ReflectJvmMapping;

Expand All @@ -37,10 +37,8 @@
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.validation.method.MethodValidator;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.SessionStatus;
Expand All @@ -64,9 +62,6 @@ public class InvocableHandlerMethod extends HandlerMethod {

private static final Class<?>[] EMPTY_GROUPS = new Class<?>[0];

private static final ReflectionUtils.MethodFilter boxImplFilter =
(method -> method.isSynthetic() && Modifier.isStatic(method.getModifiers()) && method.getName().equals("box-impl"));


private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();

Expand Down Expand Up @@ -322,10 +317,7 @@ public static Object invokeFunction(Method method, Object target, Object[] args)
if (parameter.getType().getClassifier() instanceof KClass<?> kClass) {
Class<?> javaClass = JvmClassMappingKt.getJavaClass(kClass);
if (KotlinDetector.isInlineClass(javaClass)) {
Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(javaClass, boxImplFilter);
Assert.state(methods.length == 1,
"Unable to find a single box-impl synthetic static method in " + javaClass.getName());
argMap.put(parameter, ReflectionUtils.invokeMethod(methods[0], null, args[index]));
argMap.put(parameter, KClasses.getPrimaryConstructor(kClass).call(args[index]));
}
else {
argMap.put(parameter, args[index]);
Expand All @@ -342,6 +334,7 @@ public static Object invokeFunction(Method method, Object target, Object[] args)
Object result = function.callBy(argMap);
return (result == Unit.INSTANCE ? null : result);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,24 @@ class InvocableHandlerMethodKotlinTests {
@Test
fun valueClass() {
composite.addResolver(StubArgumentResolver(Long::class.java, 1L))
val value = getInvocable(Handler::class.java, Long::class.java).invokeForRequest(request, null)
val value = getInvocable(ValueClassHandler::class.java, Long::class.java).invokeForRequest(request, null)
Assertions.assertThat(value).isEqualTo(1L)
}

@Test
fun valueClassDefaultValue() {
composite.addResolver(StubArgumentResolver(Double::class.java))
val value = getInvocable(Handler::class.java, Double::class.java).invokeForRequest(request, null)
val value = getInvocable(ValueClassHandler::class.java, Double::class.java).invokeForRequest(request, null)
Assertions.assertThat(value).isEqualTo(3.1)
}

@Test
fun valueClassWithInit() {
composite.addResolver(StubArgumentResolver(String::class.java, ""))
val invocable = getInvocable(ValueClassHandler::class.java, String::class.java)
Assertions.assertThatIllegalArgumentException().isThrownBy { invocable.invokeForRequest(request, null) }
}

@Test
fun propertyAccessor() {
val value = getInvocable(PropertyAccessorHandler::class.java).invokeForRequest(request, null)
Expand Down Expand Up @@ -153,11 +160,19 @@ class InvocableHandlerMethodKotlinTests {
return null
}

}

private class ValueClassHandler {

fun valueClass(limit: LongValueClass) =
limit.value

fun valueClass(limit: DoubleValueClass = DoubleValueClass(3.1)) =
limit.value

fun valueClassWithInit(valueClass: ValueClassWithInit) =
valueClass

}

private class PropertyAccessorHandler {
Expand All @@ -183,6 +198,15 @@ class InvocableHandlerMethodKotlinTests {
@JvmInline
value class DoubleValueClass(val value: Double)

@JvmInline
value class ValueClassWithInit(val value: String) {
init {
if (value.isEmpty()) {
throw IllegalArgumentException()
}
}
}

class CustomException(message: String) : Throwable(message)

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
Expand All @@ -32,6 +31,7 @@
import kotlin.reflect.KClass;
import kotlin.reflect.KFunction;
import kotlin.reflect.KParameter;
import kotlin.reflect.full.KClasses;
import kotlin.reflect.jvm.KCallablesJvm;
import kotlin.reflect.jvm.ReflectJvmMapping;
import reactor.core.publisher.Mono;
Expand All @@ -46,10 +46,8 @@
import org.springframework.http.HttpStatusCode;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.validation.method.MethodValidator;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.reactive.BindingContext;
Expand All @@ -74,9 +72,6 @@ public class InvocableHandlerMethod extends HandlerMethod {

private static final Object NO_ARG_VALUE = new Object();

private static final ReflectionUtils.MethodFilter boxImplFilter =
(method -> method.isSynthetic() && Modifier.isStatic(method.getModifiers()) && method.getName().equals("box-impl"));


private final HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();

Expand Down Expand Up @@ -333,10 +328,7 @@ public static Object invokeFunction(Method method, Object target, Object[] args,
if (parameter.getType().getClassifier() instanceof KClass<?> kClass) {
Class<?> javaClass = JvmClassMappingKt.getJavaClass(kClass);
if (KotlinDetector.isInlineClass(javaClass)) {
Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(javaClass, boxImplFilter);
Assert.state(methods.length == 1,
"Unable to find a single box-impl synthetic static method in " + javaClass.getName());
argMap.put(parameter, ReflectionUtils.invokeMethod(methods[0], null, args[index]));
argMap.put(parameter, KClasses.getPrimaryConstructor(kClass).call(args[index]));
}
else {
argMap.put(parameter, args[index]);
Expand All @@ -354,6 +346,7 @@ public static Object invokeFunction(Method method, Object target, Object[] args,
return (result == Unit.INSTANCE ? null : result);
}
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package org.springframework.web.reactive.result
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.delay
import org.assertj.core.api.Assertions
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.core.MethodParameter
Expand All @@ -41,6 +42,7 @@ import reactor.core.publisher.Mono
import reactor.test.StepVerifier
import java.lang.reflect.Method
import java.time.Duration
import kotlin.reflect.KClass
import kotlin.reflect.jvm.javaMethod

/**
Expand Down Expand Up @@ -197,6 +199,14 @@ class InvocableHandlerMethodKotlinTests {
assertHandlerResultValue(result, "3.1")
}

@Test
fun valueClassWithInit() {
this.resolvers.add(stubResolver("", String::class.java))
val method = ValueClassController::valueClassWithInit.javaMethod!!
val result = invoke(ValueClassController(), method)
assertExceptionThrown(result, IllegalArgumentException::class)
}

@Test
fun propertyAccessor() {
this.resolvers.add(stubResolver(null, String::class.java))
Expand Down Expand Up @@ -256,6 +266,12 @@ class InvocableHandlerMethodKotlinTests {
}.verifyComplete()
}

private fun assertExceptionThrown(mono: Mono<HandlerResult>, exceptionClass: KClass<out Throwable>) {
StepVerifier.create(mono)
.consumeNextWith { }
.verifyError(exceptionClass.java)
}

class CoroutinesController {

suspend fun singleArg(q: String?): String {
Expand Down Expand Up @@ -320,6 +336,9 @@ class InvocableHandlerMethodKotlinTests {
fun valueClassWithDefault(limit: DoubleValueClass = DoubleValueClass(3.1)) =
"${limit.value}"

fun valueClassWithInit(valueClass: ValueClassWithInit) =
valueClass

}

class PropertyAccessorController {
Expand All @@ -346,5 +365,14 @@ class InvocableHandlerMethodKotlinTests {
@JvmInline
value class DoubleValueClass(val value: Double)

@JvmInline
value class ValueClassWithInit(val value: String) {
init {
if (value.isEmpty()) {
throw IllegalArgumentException()
}
}
}

class CustomException(message: String) : Throwable(message)
}

0 comments on commit 16b9583

Please sign in to comment.