Skip to content

Commit 22cf83e

Browse files
committed
Add support for suspending handler methods in WebFlux
This commit turns Coroutines suspending methods to `Mono` which can be handled natively by WebFlux. See gh-19975
1 parent 3cce85b commit 22cf83e

File tree

6 files changed

+261
-3
lines changed

6 files changed

+261
-3
lines changed

spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.web.reactive.result.method;
1818

19+
import static org.springframework.web.reactive.result.method.InvocableHandlerMethodKt.*;
20+
1921
import java.lang.reflect.InvocationTargetException;
2022
import java.lang.reflect.Method;
2123
import java.lang.reflect.ParameterizedType;
@@ -27,6 +29,7 @@
2729
import reactor.core.publisher.Mono;
2830

2931
import org.springframework.core.DefaultParameterNameDiscoverer;
32+
import org.springframework.core.KotlinDetector;
3033
import org.springframework.core.MethodParameter;
3134
import org.springframework.core.ParameterNameDiscoverer;
3235
import org.springframework.core.ReactiveAdapter;
@@ -48,6 +51,7 @@
4851
*
4952
* @author Rossen Stoyanchev
5053
* @author Juergen Hoeller
54+
* @author Sebastien Deleuze
5155
* @since 5.0
5256
*/
5357
public class InvocableHandlerMethod extends HandlerMethod {
@@ -136,7 +140,13 @@ public Mono<HandlerResult> invoke(
136140
Object value;
137141
try {
138142
ReflectionUtils.makeAccessible(getBridgedMethod());
139-
value = getBridgedMethod().invoke(getBean(), args);
143+
Method method = getBridgedMethod();
144+
if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(method.getDeclaringClass())) {
145+
value = invokeHandlerMethod(method, getBean(), args);
146+
}
147+
else {
148+
value = method.invoke(getBean(), args);
149+
}
140150
}
141151
catch (IllegalArgumentException ex) {
142152
assertTargetBean(getBridgedMethod(), getBean(), args);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2002-2019 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+
* http://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.reactive.result.method.annotation;
18+
19+
import reactor.core.publisher.Mono;
20+
21+
import org.springframework.core.MethodParameter;
22+
import org.springframework.web.reactive.BindingContext;
23+
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
24+
import org.springframework.web.server.ServerWebExchange;
25+
26+
/**
27+
* No-op resolver for method arguments of type {@link kotlin.coroutines.Continuation}.
28+
*
29+
* @author Sebastien Deleuze
30+
* @since 5.2
31+
*/
32+
public class ContinuationHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
33+
34+
@Override
35+
public boolean supportsParameter(MethodParameter parameter) {
36+
return "kotlin.coroutines.Continuation".equals(parameter.getParameterType().getName());
37+
}
38+
39+
@Override
40+
public Mono<Object> resolveArgument(MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange) {
41+
return Mono.empty();
42+
}
43+
}

spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -32,6 +32,7 @@
3232
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
3333
import org.springframework.context.ApplicationContext;
3434
import org.springframework.context.ConfigurableApplicationContext;
35+
import org.springframework.core.KotlinDetector;
3536
import org.springframework.core.MethodIntrospector;
3637
import org.springframework.core.ReactiveAdapterRegistry;
3738
import org.springframework.core.annotation.AnnotatedElementUtils;
@@ -190,6 +191,9 @@ private static List<HandlerMethodArgumentResolver> initResolvers(ArgumentResolve
190191
result.add(new RequestAttributeMethodArgumentResolver(beanFactory, reactiveRegistry));
191192

192193
// Type-based...
194+
if (KotlinDetector.isKotlinPresent()) {
195+
result.add(new ContinuationHandlerMethodArgumentResolver());
196+
}
193197
if (!readers.isEmpty()) {
194198
result.add(new HttpEntityArgumentResolver(readers, reactiveRegistry));
195199
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2002-2019 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+
* http://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.reactive.result.method
18+
19+
import kotlinx.coroutines.GlobalScope
20+
import kotlinx.coroutines.reactor.mono
21+
import reactor.core.publisher.onErrorMap
22+
import java.lang.reflect.InvocationTargetException
23+
import java.lang.reflect.Method
24+
import kotlin.reflect.full.callSuspend
25+
import kotlin.reflect.jvm.kotlinFunction
26+
27+
/**
28+
* Invoke an handler method converting suspending method to {@link Mono} if necessary.
29+
*
30+
* @author Sebastien Deleuze
31+
* @since 5.2
32+
*/
33+
internal fun invokeHandlerMethod(method: Method, bean: Any, vararg args: Any?): Any? {
34+
val function = method.kotlinFunction!!
35+
return if (function.isSuspend) {
36+
GlobalScope.mono { function.callSuspend(bean, *args.sliceArray(0..(args.size-2)))
37+
.let { if (it == Unit) null else it} }
38+
.onErrorMap(InvocationTargetException::class) { it.targetException }
39+
}
40+
else {
41+
function.call(bean, *args)
42+
}
43+
}

spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolverTests.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ public void requestMappingArgumentResolvers() {
105105
assertEquals(SessionAttributeMethodArgumentResolver.class, next(resolvers, index).getClass());
106106
assertEquals(RequestAttributeMethodArgumentResolver.class, next(resolvers, index).getClass());
107107

108+
assertEquals(ContinuationHandlerMethodArgumentResolver.class, next(resolvers, index).getClass());
108109
assertEquals(HttpEntityArgumentResolver.class, next(resolvers, index).getClass());
109110
assertEquals(ModelArgumentResolver.class, next(resolvers, index).getClass());
110111
assertEquals(ErrorsMethodArgumentResolver.class, next(resolvers, index).getClass());
@@ -143,6 +144,7 @@ public void modelAttributeArgumentResolvers() {
143144
assertEquals(SessionAttributeMethodArgumentResolver.class, next(resolvers, index).getClass());
144145
assertEquals(RequestAttributeMethodArgumentResolver.class, next(resolvers, index).getClass());
145146

147+
assertEquals(ContinuationHandlerMethodArgumentResolver.class, next(resolvers, index).getClass());
146148
assertEquals(ModelArgumentResolver.class, next(resolvers, index).getClass());
147149
assertEquals(ErrorsMethodArgumentResolver.class, next(resolvers, index).getClass());
148150
assertEquals(ServerWebExchangeArgumentResolver.class, next(resolvers, index).getClass());
@@ -209,6 +211,7 @@ public void exceptionHandlerArgumentResolvers() {
209211
assertEquals(SessionAttributeMethodArgumentResolver.class, next(resolvers, index).getClass());
210212
assertEquals(RequestAttributeMethodArgumentResolver.class, next(resolvers, index).getClass());
211213

214+
assertEquals(ContinuationHandlerMethodArgumentResolver.class, next(resolvers, index).getClass());
212215
assertEquals(ModelArgumentResolver.class, next(resolvers, index).getClass());
213216
assertEquals(ServerWebExchangeArgumentResolver.class, next(resolvers, index).getClass());
214217
assertEquals(PrincipalArgumentResolver.class, next(resolvers, index).getClass());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
* Copyright 2002-2019 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+
* http://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.reactive.result
18+
19+
import io.mockk.every
20+
import io.mockk.mockk
21+
import kotlinx.coroutines.delay
22+
import org.hamcrest.CoreMatchers.`is`
23+
import org.hamcrest.MatcherAssert.assertThat
24+
import org.junit.Assert.assertEquals
25+
import org.junit.Test
26+
import org.springframework.http.HttpStatus
27+
import org.springframework.http.server.reactive.ServerHttpResponse
28+
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest.get
29+
import org.springframework.mock.web.test.server.MockServerWebExchange
30+
import org.springframework.web.bind.annotation.ResponseStatus
31+
import org.springframework.web.reactive.BindingContext
32+
import org.springframework.web.reactive.HandlerResult
33+
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver
34+
import org.springframework.web.reactive.result.method.InvocableHandlerMethod
35+
import org.springframework.web.reactive.result.method.annotation.ContinuationHandlerMethodArgumentResolver
36+
import reactor.core.publisher.Mono
37+
import reactor.test.StepVerifier
38+
import reactor.test.expectError
39+
import java.lang.reflect.Method
40+
import kotlin.reflect.jvm.javaMethod
41+
42+
class KotlinInvocableHandlerMethodTests {
43+
44+
private val exchange = MockServerWebExchange.from(get("http://localhost:8080/path"))
45+
46+
private val resolvers = mutableListOf<HandlerMethodArgumentResolver>(ContinuationHandlerMethodArgumentResolver())
47+
48+
@Test
49+
fun resolveNoArg() {
50+
this.resolvers.add(stubResolver(Mono.empty()))
51+
val method = CoroutinesController::singleArg.javaMethod!!
52+
val result = invoke(CoroutinesController(), method, null)
53+
assertHandlerResultValue(result, "success:null")
54+
}
55+
56+
@Test
57+
fun resolveArg() {
58+
this.resolvers.add(stubResolver("foo"))
59+
val method = CoroutinesController::singleArg.javaMethod!!
60+
val result = invoke(CoroutinesController(), method,"foo")
61+
assertHandlerResultValue(result, "success:foo")
62+
}
63+
64+
@Test
65+
fun resolveNoArgs() {
66+
val method = CoroutinesController::noArgs.javaMethod!!
67+
val result = invoke(CoroutinesController(), method)
68+
assertHandlerResultValue(result, "success")
69+
}
70+
71+
@Test
72+
fun invocationTargetException() {
73+
val method = CoroutinesController::exceptionMethod.javaMethod!!
74+
val result = invoke(CoroutinesController(), method)
75+
76+
StepVerifier.create(result)
77+
.consumeNextWith { StepVerifier.create(it.returnValue as Mono<*>).expectError(IllegalStateException::class).verify() }
78+
.verifyComplete()
79+
}
80+
81+
@Test
82+
fun responseStatusAnnotation() {
83+
val method = CoroutinesController::created.javaMethod!!
84+
val result = invoke(CoroutinesController(), method)
85+
86+
assertHandlerResultValue(result, "created")
87+
assertThat<HttpStatus>(this.exchange.response.statusCode, `is`(HttpStatus.CREATED))
88+
}
89+
90+
@Test
91+
fun voidMethodWithResponseArg() {
92+
val response = this.exchange.response
93+
this.resolvers.add(stubResolver(response))
94+
val method = CoroutinesController::response.javaMethod!!
95+
val result = invoke(CoroutinesController(), method)
96+
97+
StepVerifier.create(result)
98+
.consumeNextWith { StepVerifier.create(it.returnValue as Mono<*>).verifyComplete() }
99+
.verifyComplete()
100+
assertEquals("bar", this.exchange.response.headers.getFirst("foo"))
101+
}
102+
103+
private fun invoke(handler: Any, method: Method, vararg providedArgs: Any?): Mono<HandlerResult> {
104+
val invocable = InvocableHandlerMethod(handler, method)
105+
invocable.setArgumentResolvers(this.resolvers)
106+
return invocable.invoke(this.exchange, BindingContext(), *providedArgs)
107+
}
108+
109+
private fun stubResolver(stubValue: Any?): HandlerMethodArgumentResolver {
110+
return stubResolver(Mono.justOrEmpty(stubValue))
111+
}
112+
113+
private fun stubResolver(stubValue: Mono<Any>): HandlerMethodArgumentResolver {
114+
val resolver = mockk<HandlerMethodArgumentResolver>()
115+
every { resolver.supportsParameter(any()) } returns true
116+
every { resolver.resolveArgument(any(), any(), any()) } returns stubValue
117+
return resolver
118+
}
119+
120+
private fun assertHandlerResultValue(mono: Mono<HandlerResult>, expected: String) {
121+
StepVerifier.create(mono)
122+
.consumeNextWith { StepVerifier.create(it.returnValue as Mono<*>).expectNext(expected).verifyComplete() }
123+
.verifyComplete()
124+
}
125+
126+
class CoroutinesController {
127+
128+
suspend fun singleArg(q: String?): String {
129+
delay(10)
130+
return "success:$q"
131+
}
132+
133+
suspend fun noArgs(): String {
134+
delay(10)
135+
return "success"
136+
}
137+
138+
suspend fun exceptionMethod() {
139+
throw IllegalStateException("boo")
140+
}
141+
142+
@ResponseStatus(HttpStatus.CREATED)
143+
suspend fun created(): String {
144+
delay(10)
145+
return "created"
146+
}
147+
148+
suspend fun response(response: ServerHttpResponse) {
149+
delay(10)
150+
response.headers.add("foo", "bar")
151+
}
152+
153+
154+
}
155+
}

0 commit comments

Comments
 (0)