Skip to content

Commit 98f1287

Browse files
committed
Fix HttpServiceMethod support for suspending functions
This commit fixes nested type handling for suspending functions in HttpServiceMethod. Closes gh-30266
1 parent 8234fa2 commit 98f1287

File tree

2 files changed

+109
-11
lines changed

2 files changed

+109
-11
lines changed

spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
* by delegating to an {@link HttpClientAdapter} to perform actual requests.
5353
*
5454
* @author Rossen Stoyanchev
55+
* @author Sebastien Deleuze
5556
* @since 6.0
5657
*/
5758
final class HttpServiceMethod {
@@ -311,14 +312,15 @@ public static ResponseFunction create(
311312

312313
MethodParameter returnParam = new MethodParameter(method, -1);
313314
Class<?> returnType = returnParam.getParameterType();
314-
if (KotlinDetector.isSuspendingFunction(method)) {
315+
boolean isSuspending = KotlinDetector.isSuspendingFunction(method);
316+
if (isSuspending) {
315317
returnType = Mono.class;
316318
}
317319

318320
ReactiveAdapter reactiveAdapter = reactiveRegistry.getAdapter(returnType);
319321

320322
MethodParameter actualParam = (reactiveAdapter != null ? returnParam.nested() : returnParam.nestedIfOptional());
321-
Class<?> actualType = actualParam.getNestedParameterType();
323+
Class<?> actualType = isSuspending ? actualParam.getParameterType() : actualParam.getNestedParameterType();
322324

323325
Function<HttpRequestValues, Publisher<?>> responseFunction;
324326
if (actualType.equals(void.class) || actualType.equals(Void.class)) {
@@ -331,27 +333,27 @@ else if (actualType.equals(HttpHeaders.class)) {
331333
responseFunction = client::requestToHeaders;
332334
}
333335
else if (actualType.equals(ResponseEntity.class)) {
334-
MethodParameter bodyParam = actualParam.nested();
336+
MethodParameter bodyParam = isSuspending ? actualParam : actualParam.nested();
335337
Class<?> bodyType = bodyParam.getNestedParameterType();
336338
if (bodyType.equals(Void.class)) {
337339
responseFunction = client::requestToBodilessEntity;
338340
}
339341
else {
340342
ReactiveAdapter bodyAdapter = reactiveRegistry.getAdapter(bodyType);
341-
responseFunction = initResponseEntityFunction(client, bodyParam, bodyAdapter);
343+
responseFunction = initResponseEntityFunction(client, bodyParam, bodyAdapter, isSuspending);
342344
}
343345
}
344346
else {
345-
responseFunction = initBodyFunction(client, actualParam, reactiveAdapter);
347+
responseFunction = initBodyFunction(client, actualParam, reactiveAdapter, isSuspending);
346348
}
347349

348350
boolean blockForOptional = returnType.equals(Optional.class);
349351
return new ResponseFunction(responseFunction, reactiveAdapter, blockForOptional, blockTimeout);
350352
}
351353

352354
@SuppressWarnings("ConstantConditions")
353-
private static Function<HttpRequestValues, Publisher<?>> initResponseEntityFunction(
354-
HttpClientAdapter client, MethodParameter methodParam, @Nullable ReactiveAdapter reactiveAdapter) {
355+
private static Function<HttpRequestValues, Publisher<?>> initResponseEntityFunction(HttpClientAdapter client,
356+
MethodParameter methodParam, @Nullable ReactiveAdapter reactiveAdapter, boolean isSuspending) {
355357

356358
if (reactiveAdapter == null) {
357359
return request -> client.requestToEntity(
@@ -362,7 +364,8 @@ private static Function<HttpRequestValues, Publisher<?>> initResponseEntityFunct
362364
"ResponseEntity body must be a concrete value or a multi-value Publisher");
363365

364366
ParameterizedTypeReference<?> bodyType =
365-
ParameterizedTypeReference.forType(methodParam.nested().getNestedGenericParameterType());
367+
ParameterizedTypeReference.forType(isSuspending ? methodParam.nested().getGenericParameterType() :
368+
methodParam.nested().getNestedGenericParameterType());
366369

367370
// Shortcut for Flux
368371
if (reactiveAdapter.getReactiveType().equals(Flux.class)) {
@@ -376,11 +379,12 @@ private static Function<HttpRequestValues, Publisher<?>> initResponseEntityFunct
376379
});
377380
}
378381

379-
private static Function<HttpRequestValues, Publisher<?>> initBodyFunction(
380-
HttpClientAdapter client, MethodParameter methodParam, @Nullable ReactiveAdapter reactiveAdapter) {
382+
private static Function<HttpRequestValues, Publisher<?>> initBodyFunction(HttpClientAdapter client,
383+
MethodParameter methodParam, @Nullable ReactiveAdapter reactiveAdapter, boolean isSuspending) {
381384

382385
ParameterizedTypeReference<?> bodyType =
383-
ParameterizedTypeReference.forType(methodParam.getNestedGenericParameterType());
386+
ParameterizedTypeReference.forType(isSuspending ? methodParam.getGenericParameterType() :
387+
methodParam.getNestedGenericParameterType());
384388

385389
return (reactiveAdapter != null && reactiveAdapter.isMultiValue() ?
386390
request -> client.requestToBodyFlux(request, bodyType) :
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.service.invoker
18+
19+
import kotlinx.coroutines.flow.Flow
20+
import kotlinx.coroutines.flow.toList
21+
import kotlinx.coroutines.runBlocking
22+
import org.assertj.core.api.Assertions.assertThat
23+
import org.junit.jupiter.api.Test
24+
import org.springframework.core.ParameterizedTypeReference
25+
import org.springframework.http.HttpStatus
26+
import org.springframework.http.ResponseEntity
27+
import org.springframework.lang.Nullable
28+
import org.springframework.web.service.annotation.GetExchange
29+
30+
/**
31+
* Kotlin tests for [HttpServiceMethod].
32+
*
33+
* @author Sebastien Deleuze
34+
*/
35+
class KotlinHttpServiceMethodTests {
36+
37+
private val client = TestHttpClientAdapter()
38+
private val proxyFactory = HttpServiceProxyFactory.builder(client).build()
39+
40+
@Test
41+
fun coroutinesService(): Unit = runBlocking {
42+
val service = proxyFactory.createClient(CoroutinesService::class.java)
43+
44+
val stringBody = service.stringBody()
45+
assertThat(stringBody).isEqualTo("requestToBody")
46+
verifyClientInvocation("requestToBody", object : ParameterizedTypeReference<String>() {})
47+
48+
service.listBody()
49+
verifyClientInvocation("requestToBody", object : ParameterizedTypeReference<MutableList<String>>() {})
50+
51+
val flowBody = service.flowBody()
52+
assertThat(flowBody.toList()).containsExactly("request", "To", "Body", "Flux")
53+
verifyClientInvocation("requestToBodyFlux", object : ParameterizedTypeReference<String>() {})
54+
55+
val stringEntity = service.stringEntity()
56+
assertThat(stringEntity).isEqualTo(ResponseEntity.ok<String>("requestToEntity"))
57+
verifyClientInvocation("requestToEntity", object : ParameterizedTypeReference<String>() {})
58+
59+
service.listEntity()
60+
verifyClientInvocation("requestToEntity", object : ParameterizedTypeReference<MutableList<String>>() {})
61+
62+
val flowEntity = service.flowEntity()
63+
assertThat(flowEntity.statusCode).isEqualTo(HttpStatus.OK)
64+
assertThat(flowEntity.body!!.toList()).containsExactly("request", "To", "Entity", "Flux")
65+
verifyClientInvocation("requestToEntityFlux", object : ParameterizedTypeReference<String>() {})
66+
}
67+
68+
private fun verifyClientInvocation(methodName: String, @Nullable expectedBodyType: ParameterizedTypeReference<*>) {
69+
assertThat(client.invokedMethodName).isEqualTo(methodName)
70+
assertThat(client.bodyType).isEqualTo(expectedBodyType)
71+
}
72+
73+
private interface CoroutinesService {
74+
75+
@GetExchange
76+
suspend fun stringBody(): String
77+
78+
@GetExchange
79+
suspend fun listBody(): MutableList<String>
80+
81+
@GetExchange
82+
fun flowBody(): Flow<String>
83+
84+
@GetExchange
85+
suspend fun stringEntity(): ResponseEntity<String>
86+
87+
@GetExchange
88+
suspend fun listEntity(): ResponseEntity<MutableList<String>>
89+
90+
@GetExchange
91+
fun flowEntity(): ResponseEntity<Flow<String>>
92+
}
93+
94+
}

0 commit comments

Comments
 (0)