Skip to content

Commit ab33d71

Browse files
committed
Merge branch '6.2.x'
2 parents 80f63d8 + 1aede29 commit ab33d71

File tree

6 files changed

+320
-62
lines changed

6 files changed

+320
-62
lines changed

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
}

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

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
}

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)