Skip to content

Commit 1aede29

Browse files
committed
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
1 parent ea3bd7a commit 1aede29

File tree

6 files changed

+320
-62
lines changed

6 files changed

+320
-62
lines changed

Diff for: spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java

+2-21
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
import org.reactivestreams.Publisher;
4545
import reactor.core.publisher.Flux;
4646
import reactor.core.publisher.Mono;
47-
import reactor.core.publisher.SynchronousSink;
4847

4948
import org.springframework.lang.Nullable;
5049
import org.springframework.util.Assert;
@@ -109,7 +108,7 @@ public static Publisher<?> invokeSuspendingFunction(Method method, Object target
109108
* @throws IllegalArgumentException if {@code method} is not a suspending function
110109
* @since 6.0
111110
*/
112-
@SuppressWarnings({"deprecation", "DataFlowIssue", "NullAway"})
111+
@SuppressWarnings({"DataFlowIssue", "NullAway"})
113112
public static Publisher<?> invokeSuspendingFunction(
114113
CoroutineContext context, Method method, @Nullable Object target, @Nullable Object... args) {
115114

@@ -146,7 +145,7 @@ public static Publisher<?> invokeSuspendingFunction(
146145
}
147146
return KCallables.callSuspendBy(function, argMap, continuation);
148147
})
149-
.handle(CoroutinesUtils::handleResult)
148+
.filter(result -> result != Unit.INSTANCE)
150149
.onErrorMap(InvocationTargetException.class, InvocationTargetException::getTargetException);
151150

152151
KType returnType = function.getReturnType();
@@ -166,22 +165,4 @@ private static Flux<?> asFlux(Object flow) {
166165
return ReactorFlowKt.asFlux(((Flow<?>) flow));
167166
}
168167

169-
private static void handleResult(Object result, SynchronousSink<Object> sink) {
170-
if (result == Unit.INSTANCE) {
171-
sink.complete();
172-
}
173-
else if (KotlinDetector.isInlineClass(result.getClass())) {
174-
try {
175-
sink.next(result.getClass().getDeclaredMethod("unbox-impl").invoke(result));
176-
sink.complete();
177-
}
178-
catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ex) {
179-
sink.error(ex);
180-
}
181-
}
182-
else {
183-
sink.next(result);
184-
sink.complete();
185-
}
186-
}
187168
}

Diff for: spring-core/src/test/kotlin/org/springframework/core/CoroutinesUtilsTests.kt

+17-3
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ class CoroutinesUtilsTests {
192192

193193
@Test
194194
fun invokeSuspendingFunctionWithValueClassParameter() {
195-
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithValueClass") }
195+
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithValueClassParameter") }
196196
val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, "foo", null) as Mono
197197
runBlocking {
198198
Assertions.assertThat(mono.awaitSingle()).isEqualTo("foo")
@@ -204,7 +204,16 @@ class CoroutinesUtilsTests {
204204
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithValueClassReturnValue") }
205205
val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, null) as Mono
206206
runBlocking {
207-
Assertions.assertThat(mono.awaitSingle()).isEqualTo("foo")
207+
Assertions.assertThat(mono.awaitSingle()).isEqualTo(ValueClass("foo"))
208+
}
209+
}
210+
211+
@Test
212+
fun invokeSuspendingFunctionWithResultOfUnitReturnValue() {
213+
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithResultOfUnitReturnValue") }
214+
val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, null) as Mono
215+
runBlocking {
216+
Assertions.assertThat(mono.awaitSingle()).isEqualTo(Result.success(Unit))
208217
}
209218
}
210219

@@ -314,7 +323,7 @@ class CoroutinesUtilsTests {
314323
return null
315324
}
316325

317-
suspend fun suspendingFunctionWithValueClass(value: ValueClass): String {
326+
suspend fun suspendingFunctionWithValueClassParameter(value: ValueClass): String {
318327
delay(1)
319328
return value.value
320329
}
@@ -324,6 +333,11 @@ class CoroutinesUtilsTests {
324333
return ValueClass("foo")
325334
}
326335

336+
suspend fun suspendingFunctionWithResultOfUnitReturnValue(): Result<Unit> {
337+
delay(1)
338+
return Result.success(Unit)
339+
}
340+
327341
suspend fun suspendingFunctionWithValueClassWithInit(value: ValueClassWithInit): String {
328342
delay(1)
329343
return value.value

Diff for: spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java

+29-3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
import kotlin.reflect.full.KClasses;
3131
import kotlin.reflect.jvm.KCallablesJvm;
3232
import kotlin.reflect.jvm.ReflectJvmMapping;
33+
import reactor.core.publisher.Mono;
34+
import reactor.core.publisher.SynchronousSink;
3335

3436
import org.springframework.context.MessageSource;
3537
import org.springframework.core.CoroutinesUtils;
@@ -288,7 +290,8 @@ else if (targetException instanceof Exception exception) {
288290
* @since 6.0
289291
*/
290292
protected Object invokeSuspendingFunction(Method method, Object target, Object[] args) {
291-
return CoroutinesUtils.invokeSuspendingFunction(method, target, args);
293+
Object result = CoroutinesUtils.invokeSuspendingFunction(method, target, args);
294+
return (result instanceof Mono<?> mono ? mono.handle(KotlinDelegate::handleResult) : result);
292295
}
293296

294297

@@ -298,7 +301,7 @@ protected Object invokeSuspendingFunction(Method method, Object target, Object[]
298301
private static class KotlinDelegate {
299302

300303
@Nullable
301-
@SuppressWarnings({"deprecation", "DataFlowIssue"})
304+
@SuppressWarnings("DataFlowIssue")
302305
public static Object invokeFunction(Method method, Object target, Object[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
303306
KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method);
304307
// For property accessors
@@ -333,10 +336,33 @@ public static Object invokeFunction(Method method, Object target, Object[] args)
333336
}
334337
Object result = function.callBy(argMap);
335338
if (result != null && KotlinDetector.isInlineClass(result.getClass())) {
336-
return result.getClass().getDeclaredMethod("unbox-impl").invoke(result);
339+
result = unbox(result);
337340
}
338341
return (result == Unit.INSTANCE ? null : result);
339342
}
343+
344+
private static void handleResult(Object result, SynchronousSink<Object> sink) {
345+
if (KotlinDetector.isInlineClass(result.getClass())) {
346+
try {
347+
Object unboxed = unbox(result);
348+
if (unboxed != Unit.INSTANCE) {
349+
sink.next(unboxed);
350+
}
351+
sink.complete();
352+
}
353+
catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ex) {
354+
sink.error(ex);
355+
}
356+
}
357+
else {
358+
sink.next(result);
359+
sink.complete();
360+
}
361+
}
362+
363+
private static Object unbox(Object result) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
364+
return result.getClass().getDeclaredMethod("unbox-impl").invoke(result);
365+
}
340366
}
341367

342368
}

Diff for: spring-web/src/test/kotlin/org/springframework/web/method/support/InvocableHandlerMethodKotlinTests.kt

+128-13
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,18 @@
1616

1717
package org.springframework.web.method.support
1818

19+
import kotlinx.coroutines.delay
1920
import org.assertj.core.api.Assertions
2021
import org.junit.jupiter.api.Test
22+
import org.springframework.core.MethodParameter
2123
import org.springframework.util.ReflectionUtils
24+
import org.springframework.web.bind.support.WebDataBinderFactory
2225
import org.springframework.web.context.request.NativeWebRequest
2326
import org.springframework.web.context.request.ServletWebRequest
24-
import org.springframework.web.testfixture.method.ResolvableMethod
2527
import org.springframework.web.testfixture.servlet.MockHttpServletRequest
2628
import org.springframework.web.testfixture.servlet.MockHttpServletResponse
29+
import reactor.core.publisher.Mono
30+
import reactor.test.StepVerifier
2731
import java.lang.reflect.Method
2832
import kotlin.reflect.jvm.javaGetter
2933
import kotlin.reflect.jvm.javaMethod
@@ -33,6 +37,7 @@ import kotlin.reflect.jvm.javaMethod
3337
*
3438
* @author Sebastien Deleuze
3539
*/
40+
@Suppress("UNCHECKED_CAST")
3641
class InvocableHandlerMethodKotlinTests {
3742

3843
private val request: NativeWebRequest = ServletWebRequest(MockHttpServletRequest(), MockHttpServletResponse())
@@ -110,6 +115,12 @@ class InvocableHandlerMethodKotlinTests {
110115
Assertions.assertThat(value).isEqualTo("foo")
111116
}
112117

118+
@Test
119+
fun resultOfUnitReturnValue() {
120+
val value = getInvocable(ValueClassHandler::resultOfUnitReturnValue.javaMethod!!).invokeForRequest(request, null)
121+
Assertions.assertThat(value).isNull()
122+
}
123+
113124
@Test
114125
fun valueClassDefaultValue() {
115126
composite.addResolver(StubArgumentResolver(Double::class.java))
@@ -138,6 +149,60 @@ class InvocableHandlerMethodKotlinTests {
138149
Assertions.assertThat(value).isEqualTo('a')
139150
}
140151

152+
@Test
153+
fun suspendingValueClass() {
154+
composite.addResolver(ContinuationHandlerMethodArgumentResolver())
155+
composite.addResolver(StubArgumentResolver(Long::class.java, 1L))
156+
val value = getInvocable(SuspendingValueClassHandler::longValueClass.javaMethod!!).invokeForRequest(request, null)
157+
StepVerifier.create(value as Mono<Long>).expectNext(1L).verifyComplete()
158+
}
159+
160+
@Test
161+
fun suspendingValueClassReturnValue() {
162+
composite.addResolver(ContinuationHandlerMethodArgumentResolver())
163+
val value = getInvocable(SuspendingValueClassHandler::valueClassReturnValue.javaMethod!!).invokeForRequest(request, null)
164+
StepVerifier.create(value as Mono<String>).expectNext("foo").verifyComplete()
165+
}
166+
167+
@Test
168+
fun suspendingResultOfUnitReturnValue() {
169+
composite.addResolver(ContinuationHandlerMethodArgumentResolver())
170+
val value = getInvocable(SuspendingValueClassHandler::resultOfUnitReturnValue.javaMethod!!).invokeForRequest(request, null)
171+
StepVerifier.create(value as Mono<Unit>).verifyComplete()
172+
}
173+
174+
@Test
175+
fun suspendingValueClassDefaultValue() {
176+
composite.addResolver(ContinuationHandlerMethodArgumentResolver())
177+
composite.addResolver(StubArgumentResolver(Double::class.java))
178+
val value = getInvocable(SuspendingValueClassHandler::doubleValueClass.javaMethod!!).invokeForRequest(request, null)
179+
StepVerifier.create(value as Mono<Double>).expectNext(3.1).verifyComplete()
180+
}
181+
182+
@Test
183+
fun suspendingValueClassWithInit() {
184+
composite.addResolver(ContinuationHandlerMethodArgumentResolver())
185+
composite.addResolver(StubArgumentResolver(String::class.java, ""))
186+
val value = getInvocable(SuspendingValueClassHandler::valueClassWithInit.javaMethod!!).invokeForRequest(request, null)
187+
StepVerifier.create(value as Mono<String>).verifyError(IllegalArgumentException::class.java)
188+
}
189+
190+
@Test
191+
fun suspendingValueClassWithNullable() {
192+
composite.addResolver(ContinuationHandlerMethodArgumentResolver())
193+
composite.addResolver(StubArgumentResolver(LongValueClass::class.java, null))
194+
val value = getInvocable(SuspendingValueClassHandler::valueClassWithNullable.javaMethod!!).invokeForRequest(request, null)
195+
StepVerifier.create(value as Mono<Long>).verifyComplete()
196+
}
197+
198+
@Test
199+
fun suspendingValueClassWithPrivateConstructor() {
200+
composite.addResolver(ContinuationHandlerMethodArgumentResolver())
201+
composite.addResolver(StubArgumentResolver(Char::class.java, 'a'))
202+
val value = getInvocable(SuspendingValueClassHandler::valueClassWithPrivateConstructor.javaMethod!!).invokeForRequest(request, null)
203+
StepVerifier.create(value as Mono<Char>).expectNext('a').verifyComplete()
204+
}
205+
141206
@Test
142207
fun propertyAccessor() {
143208
val value = getInvocable(PropertyAccessorHandler::prop.javaGetter!!).invokeForRequest(request, null)
@@ -206,23 +271,58 @@ class InvocableHandlerMethodKotlinTests {
206271

207272
private class ValueClassHandler {
208273

209-
fun valueClassReturnValue() =
210-
StringValueClass("foo")
274+
fun valueClassReturnValue() = StringValueClass("foo")
275+
276+
fun resultOfUnitReturnValue() = Result.success(Unit)
277+
278+
fun longValueClass(limit: LongValueClass) = limit.value
279+
280+
fun doubleValueClass(limit: DoubleValueClass = DoubleValueClass(3.1)) = limit.value
281+
282+
fun valueClassWithInit(valueClass: ValueClassWithInit) = valueClass
283+
284+
fun valueClassWithNullable(limit: LongValueClass?) = limit?.value
285+
286+
fun valueClassWithPrivateConstructor(limit: ValueClassWithPrivateConstructor) = limit.value
287+
}
288+
289+
private class SuspendingValueClassHandler {
290+
291+
suspend fun valueClassReturnValue(): StringValueClass {
292+
delay(1)
293+
return StringValueClass("foo")
294+
}
295+
296+
suspend fun resultOfUnitReturnValue(): Result<Unit> {
297+
delay(1)
298+
return Result.success(Unit)
299+
}
211300

212-
fun longValueClass(limit: LongValueClass) =
213-
limit.value
301+
suspend fun longValueClass(limit: LongValueClass): Long {
302+
delay(1)
303+
return limit.value
304+
}
214305

215-
fun doubleValueClass(limit: DoubleValueClass = DoubleValueClass(3.1)) =
216-
limit.value
217306

218-
fun valueClassWithInit(valueClass: ValueClassWithInit) =
219-
valueClass
307+
suspend fun doubleValueClass(limit: DoubleValueClass = DoubleValueClass(3.1)): Double {
308+
delay(1)
309+
return limit.value
310+
}
220311

221-
fun valueClassWithNullable(limit: LongValueClass?) =
222-
limit?.value
312+
suspend fun valueClassWithInit(valueClass: ValueClassWithInit): ValueClassWithInit {
313+
delay(1)
314+
return valueClass
315+
}
316+
317+
suspend fun valueClassWithNullable(limit: LongValueClass?): Long? {
318+
delay(1)
319+
return limit?.value
320+
}
223321

224-
fun valueClassWithPrivateConstructor(limit: ValueClassWithPrivateConstructor) =
225-
limit.value
322+
suspend fun valueClassWithPrivateConstructor(limit: ValueClassWithPrivateConstructor): Char {
323+
delay(1)
324+
return limit.value
325+
}
226326
}
227327

228328
private class PropertyAccessorHandler {
@@ -282,4 +382,19 @@ class InvocableHandlerMethodKotlinTests {
282382

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

385+
// Avoid adding a spring-webmvc dependency
386+
class ContinuationHandlerMethodArgumentResolver : HandlerMethodArgumentResolver {
387+
388+
override fun supportsParameter(parameter: MethodParameter) =
389+
"kotlin.coroutines.Continuation" == parameter.getParameterType().getName()
390+
391+
override fun resolveArgument(
392+
parameter: MethodParameter,
393+
mavContainer: ModelAndViewContainer?,
394+
webRequest: NativeWebRequest,
395+
binderFactory: WebDataBinderFactory?
396+
) = null
397+
398+
}
399+
285400
}

0 commit comments

Comments
 (0)