Move Kotlin value class unboxing to InvocableHandlerMethod
Before this commit, in Spring Framework 6.2, Kotlin value class unboxing was done at CoroutinesUtils level, which is a good fit for InvocableHandlerMethod use case, but not for other ones like AopUtils. This commit moves such unboxing to InvocableHandlerMethod in order to keep the HTTP response body support while fixing other regressions. Closes gh-33943
This commit is contained in:
@@ -36,6 +36,7 @@ import kotlin.reflect.full.KClasses;
|
||||
import kotlin.reflect.jvm.KCallablesJvm;
|
||||
import kotlin.reflect.jvm.ReflectJvmMapping;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.SynchronousSink;
|
||||
import reactor.core.scheduler.Scheduler;
|
||||
|
||||
import org.springframework.core.CoroutinesUtils;
|
||||
@@ -323,18 +324,15 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
||||
private static final String COROUTINE_CONTEXT_ATTRIBUTE = "org.springframework.web.server.CoWebFilter.context";
|
||||
|
||||
@Nullable
|
||||
@SuppressWarnings({"deprecation", "DataFlowIssue"})
|
||||
@SuppressWarnings("DataFlowIssue")
|
||||
public static Object invokeFunction(Method method, Object target, Object[] args, boolean isSuspendingFunction,
|
||||
ServerWebExchange exchange) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
|
||||
|
||||
if (isSuspendingFunction) {
|
||||
Object coroutineContext = exchange.getAttribute(COROUTINE_CONTEXT_ATTRIBUTE);
|
||||
if (coroutineContext == null) {
|
||||
return CoroutinesUtils.invokeSuspendingFunction(method, target, args);
|
||||
}
|
||||
else {
|
||||
return CoroutinesUtils.invokeSuspendingFunction((CoroutineContext) coroutineContext, method, target, args);
|
||||
}
|
||||
Object result = (coroutineContext == null ? CoroutinesUtils.invokeSuspendingFunction(method, target, args) :
|
||||
CoroutinesUtils.invokeSuspendingFunction((CoroutineContext) coroutineContext, method, target, args));
|
||||
return (result instanceof Mono<?> mono ? mono.handle(KotlinDelegate::handleResult) : result);
|
||||
}
|
||||
else {
|
||||
KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method);
|
||||
@@ -370,11 +368,35 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
||||
}
|
||||
Object result = function.callBy(argMap);
|
||||
if (result != null && KotlinDetector.isInlineClass(result.getClass())) {
|
||||
return result.getClass().getDeclaredMethod("unbox-impl").invoke(result);
|
||||
result = unbox(result);
|
||||
}
|
||||
return (result == Unit.INSTANCE ? null : result);
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleResult(Object result, SynchronousSink<Object> sink) {
|
||||
if (KotlinDetector.isInlineClass(result.getClass())) {
|
||||
try {
|
||||
Object unboxed = unbox(result);
|
||||
if (unboxed != Unit.INSTANCE) {
|
||||
sink.next(unboxed);
|
||||
}
|
||||
sink.complete();
|
||||
}
|
||||
catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ex) {
|
||||
sink.error(ex);
|
||||
}
|
||||
}
|
||||
else {
|
||||
sink.next(result);
|
||||
sink.complete();
|
||||
}
|
||||
}
|
||||
|
||||
private static Object unbox(Object result) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
|
||||
return result.getClass().getDeclaredMethod("unbox-impl").invoke(result);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -208,10 +208,17 @@ class InvocableHandlerMethodKotlinTests {
|
||||
@Test
|
||||
fun valueClassReturnValue() {
|
||||
val method = ValueClassController::valueClassReturnValue.javaMethod!!
|
||||
val result = invoke(ValueClassController(), method,)
|
||||
val result = invoke(ValueClassController(), method)
|
||||
assertHandlerResultValue(result, "foo")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resultOfUnitReturnValue() {
|
||||
val method = ValueClassController::resultOfUnitReturnValue.javaMethod!!
|
||||
val result = invoke(ValueClassController(), method)
|
||||
assertHandlerResultValue(result, null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun valueClassWithDefaultValue() {
|
||||
this.resolvers.add(stubResolver(null, Double::class.java))
|
||||
@@ -244,6 +251,60 @@ class InvocableHandlerMethodKotlinTests {
|
||||
assertHandlerResultValue(result, "1")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun suspendingValueClass() {
|
||||
this.resolvers.add(stubResolver(1L, Long::class.java))
|
||||
val method = SuspendingValueClassController::valueClass.javaMethod!!
|
||||
val result = invoke(SuspendingValueClassController(), method,1L)
|
||||
assertHandlerResultValue(result, "1")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun suspendingValueClassReturnValue() {
|
||||
val method = SuspendingValueClassController::valueClassReturnValue.javaMethod!!
|
||||
val result = invoke(SuspendingValueClassController(), method)
|
||||
assertHandlerResultValue(result, "foo")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun suspendingResultOfUnitReturnValue() {
|
||||
val method = SuspendingValueClassController::resultOfUnitReturnValue.javaMethod!!
|
||||
val result = invoke(SuspendingValueClassController(), method)
|
||||
assertComplete(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun suspendingValueClassWithDefaultValue() {
|
||||
this.resolvers.add(stubResolver(null, Double::class.java))
|
||||
val method = SuspendingValueClassController::valueClassWithDefault.javaMethod!!
|
||||
val result = invoke(SuspendingValueClassController(), method)
|
||||
assertHandlerResultValue(result, "3.1")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun suspendingValueClassWithInit() {
|
||||
this.resolvers.add(stubResolver("", String::class.java))
|
||||
val method = SuspendingValueClassController::valueClassWithInit.javaMethod!!
|
||||
val result = invoke(SuspendingValueClassController(), method)
|
||||
assertExceptionThrown(result, IllegalArgumentException::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun suspendingValueClassWithNullable() {
|
||||
this.resolvers.add(stubResolver(null, LongValueClass::class.java))
|
||||
val method = SuspendingValueClassController::valueClassWithNullable.javaMethod!!
|
||||
val result = invoke(SuspendingValueClassController(), method, null)
|
||||
assertHandlerResultValue(result, "null")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun suspendingValueClassWithPrivateConstructor() {
|
||||
this.resolvers.add(stubResolver(1L, Long::class.java))
|
||||
val method = SuspendingValueClassController::valueClassWithPrivateConstructor.javaMethod!!
|
||||
val result = invoke(SuspendingValueClassController(), method, 1L)
|
||||
assertHandlerResultValue(result, "1")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun propertyAccessor() {
|
||||
this.resolvers.add(stubResolver(null, String::class.java))
|
||||
@@ -313,9 +374,14 @@ class InvocableHandlerMethodKotlinTests {
|
||||
}
|
||||
|
||||
private fun assertExceptionThrown(mono: Mono<HandlerResult>, exceptionClass: KClass<out Throwable>) {
|
||||
StepVerifier.create(mono).verifyError(exceptionClass.java)
|
||||
StepVerifier.create(mono.flatMap { t -> t.returnValue as Mono<*> }).verifyError(exceptionClass.java)
|
||||
}
|
||||
|
||||
private fun assertComplete(mono: Mono<HandlerResult>) {
|
||||
StepVerifier.create(mono.flatMap { t -> t.returnValue as Mono<*> }).verifyComplete()
|
||||
}
|
||||
|
||||
|
||||
class CoroutinesController {
|
||||
|
||||
suspend fun singleArg(q: String?): String {
|
||||
@@ -380,23 +446,57 @@ class InvocableHandlerMethodKotlinTests {
|
||||
|
||||
class ValueClassController {
|
||||
|
||||
fun valueClass(limit: LongValueClass) =
|
||||
"${limit.value}"
|
||||
fun valueClass(limit: LongValueClass) = "${limit.value}"
|
||||
|
||||
fun valueClassReturnValue() =
|
||||
StringValueClass("foo")
|
||||
fun valueClassReturnValue() = StringValueClass("foo")
|
||||
|
||||
fun valueClassWithDefault(limit: DoubleValueClass = DoubleValueClass(3.1)) =
|
||||
"${limit.value}"
|
||||
fun resultOfUnitReturnValue() = Result.success(Unit)
|
||||
|
||||
fun valueClassWithInit(valueClass: ValueClassWithInit) =
|
||||
valueClass
|
||||
fun valueClassWithDefault(limit: DoubleValueClass = DoubleValueClass(3.1)) = "${limit.value}"
|
||||
|
||||
fun valueClassWithNullable(limit: LongValueClass?) =
|
||||
"${limit?.value}"
|
||||
fun valueClassWithInit(valueClass: ValueClassWithInit) = valueClass
|
||||
|
||||
fun valueClassWithPrivateConstructor(limit: ValueClassWithPrivateConstructor) =
|
||||
"${limit.value}"
|
||||
fun valueClassWithNullable(limit: LongValueClass?) = "${limit?.value}"
|
||||
|
||||
fun valueClassWithPrivateConstructor(limit: ValueClassWithPrivateConstructor) = "${limit.value}"
|
||||
}
|
||||
|
||||
class SuspendingValueClassController {
|
||||
|
||||
suspend fun valueClass(limit: LongValueClass): String {
|
||||
delay(1)
|
||||
return "${limit.value}"
|
||||
}
|
||||
|
||||
suspend fun valueClassReturnValue(): StringValueClass {
|
||||
delay(1)
|
||||
return StringValueClass("foo")
|
||||
}
|
||||
|
||||
suspend fun resultOfUnitReturnValue(): Result<Unit> {
|
||||
delay(1)
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
suspend fun valueClassWithDefault(limit: DoubleValueClass = DoubleValueClass(3.1)): String {
|
||||
delay(1)
|
||||
return "${limit.value}"
|
||||
}
|
||||
|
||||
suspend fun valueClassWithInit(valueClass: ValueClassWithInit): ValueClassWithInit {
|
||||
delay(1)
|
||||
return valueClass
|
||||
}
|
||||
|
||||
suspend fun valueClassWithNullable(limit: LongValueClass?): String {
|
||||
delay(1)
|
||||
return "${limit?.value}"
|
||||
}
|
||||
|
||||
suspend fun valueClassWithPrivateConstructor(limit: ValueClassWithPrivateConstructor): String {
|
||||
delay(1)
|
||||
return "${limit.value}"
|
||||
}
|
||||
}
|
||||
|
||||
class PropertyAccessorController {
|
||||
|
||||
Reference in New Issue
Block a user