Support suspending handler methods in Spring MVC
This commit adds support for Kotlin Coroutines suspending functions to Spring MVC, by converting those to a Mono that can then be handled by the asynchronous request processing feature. It also optimizes Coroutines detection with the introduction of an optimized KotlinDetector.isSuspendingFunction() method that does not require kotlin-reflect. Closes gh-23611
This commit is contained in:
parent
fad7243733
commit
94a42a3086
|
|
@ -51,14 +51,6 @@ internal fun <T: Any> deferredToMono(source: Deferred<T>) =
|
|||
internal fun <T: Any> monoToDeferred(source: Mono<T>) =
|
||||
GlobalScope.async(Dispatchers.Unconfined) { source.awaitFirstOrNull() }
|
||||
|
||||
/**
|
||||
* Return {@code true} if the method is a suspending function.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.2.2
|
||||
*/
|
||||
internal fun isSuspendingFunction(method: Method) = method.kotlinFunction!!.isSuspend
|
||||
|
||||
/**
|
||||
* Invoke a suspending function and converts it to [Mono] or [reactor.core.publisher.Flux].
|
||||
*
|
||||
|
|
@ -66,7 +58,7 @@ internal fun isSuspendingFunction(method: Method) = method.kotlinFunction!!.isSu
|
|||
* @since 5.2
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
internal fun invokeSuspendingFunction(method: Method, bean: Any, vararg args: Any?): Publisher<*> {
|
||||
fun invokeSuspendingFunction(method: Method, bean: Any, vararg args: Any?): Publisher<*> {
|
||||
val function = method.kotlinFunction!!
|
||||
val mono = mono(Dispatchers.Unconfined) {
|
||||
function.callSuspend(bean, *args.sliceArray(0..(args.size-2))).let { if (it == Unit) null else it }
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package org.springframework.core;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
|
@ -74,4 +75,20 @@ public abstract class KotlinDetector {
|
|||
return (kotlinMetadata != null && clazz.getDeclaredAnnotation(kotlinMetadata) != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@code true} if the method is a suspending function.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.3
|
||||
*/
|
||||
public static boolean isSuspendingFunction(Method method) {
|
||||
if (KotlinDetector.isKotlinType(method.getDeclaringClass())) {
|
||||
Class<?>[] types = method.getParameterTypes();
|
||||
if ((types.length > 0) && "kotlin.coroutines.Continuation".equals(types[types.length - 1].getName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,8 +131,7 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
|||
try {
|
||||
Method method = getBridgedMethod();
|
||||
ReflectionUtils.makeAccessible(method);
|
||||
if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(method.getDeclaringClass())
|
||||
&& CoroutinesUtils.isSuspendingFunction(method)) {
|
||||
if (KotlinDetector.isSuspendingFunction(method)) {
|
||||
isSuspendingFunction = true;
|
||||
value = CoroutinesUtils.invokeSuspendingFunction(method, getBean(), args);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ apply plugin: "kotlinx-serialization"
|
|||
dependencies {
|
||||
compile(project(":spring-beans"))
|
||||
compile(project(":spring-core"))
|
||||
compileOnly(project(":kotlin-coroutines"))
|
||||
optional(project(":spring-aop"))
|
||||
optional(project(":spring-context"))
|
||||
optional(project(":spring-oxm"))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -20,7 +20,9 @@ import java.lang.reflect.InvocationTargetException;
|
|||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.springframework.core.CoroutinesUtils;
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.KotlinDetector;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
|
@ -39,6 +41,7 @@ import org.springframework.web.method.HandlerMethod;
|
|||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Juergen Hoeller
|
||||
* @author Sebastien Deleuze
|
||||
* @since 3.1
|
||||
*/
|
||||
public class InvocableHandlerMethod extends HandlerMethod {
|
||||
|
|
@ -185,12 +188,16 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
|||
*/
|
||||
@Nullable
|
||||
protected Object doInvoke(Object... args) throws Exception {
|
||||
ReflectionUtils.makeAccessible(getBridgedMethod());
|
||||
Method method = getBridgedMethod();
|
||||
ReflectionUtils.makeAccessible(method);
|
||||
try {
|
||||
return getBridgedMethod().invoke(getBean(), args);
|
||||
if (KotlinDetector.isSuspendingFunction(method)) {
|
||||
return CoroutinesUtils.invokeSuspendingFunction(method, getBean(), args);
|
||||
}
|
||||
return method.invoke(getBean(), args);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
assertTargetBean(getBridgedMethod(), getBean(), args);
|
||||
assertTargetBean(method, getBean(), args);
|
||||
String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
|
||||
throw new IllegalStateException(formatInvokeError(text, args), ex);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,8 +139,7 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
|||
try {
|
||||
ReflectionUtils.makeAccessible(getBridgedMethod());
|
||||
Method method = getBridgedMethod();
|
||||
if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(method.getDeclaringClass())
|
||||
&& CoroutinesUtils.isSuspendingFunction(method)) {
|
||||
if (KotlinDetector.isSuspendingFunction(method)) {
|
||||
value = CoroutinesUtils.invokeSuspendingFunction(method, getBean(), args);
|
||||
}
|
||||
else {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ dependencies {
|
|||
optional("org.jetbrains.kotlin:kotlin-reflect")
|
||||
optional("org.jetbrains.kotlin:kotlin-stdlib")
|
||||
optional("org.reactivestreams:reactive-streams")
|
||||
optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
|
||||
testCompile(project(":kotlin-coroutines"))
|
||||
testCompile(testFixtures(project(":spring-beans")))
|
||||
testCompile(testFixtures(project(":spring-core")))
|
||||
testCompile(testFixtures(project(":spring-context")))
|
||||
|
|
@ -62,7 +64,6 @@ dependencies {
|
|||
testCompile("io.reactivex.rxjava3:rxjava")
|
||||
testCompile("org.jetbrains.kotlin:kotlin-script-runtime")
|
||||
testRuntime("org.jetbrains.kotlin:kotlin-scripting-jsr223")
|
||||
testRuntime("org.jetbrains.kotlinx:kotlinx-coroutines-core") // https://github.com/gradle/gradle/issues/14017
|
||||
testRuntime("org.jruby:jruby")
|
||||
testRuntime("org.python:jython-standalone")
|
||||
testRuntime("org.webjars:underscorejs")
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ import javax.servlet.http.HttpServletRequest;
|
|||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.core.KotlinDetector;
|
||||
import org.springframework.core.MethodIntrospector;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
|
@ -606,13 +605,6 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
|
|||
}
|
||||
|
||||
public void register(T mapping, Object handler, Method method) {
|
||||
// Assert that the handler method is not a suspending one.
|
||||
if (KotlinDetector.isKotlinType(method.getDeclaringClass())) {
|
||||
Class<?>[] types = method.getParameterTypes();
|
||||
if ((types.length > 0) && "kotlin.coroutines.Continuation".equals(types[types.length - 1].getName())) {
|
||||
throw new IllegalStateException("Unsupported suspending handler method detected: " + method);
|
||||
}
|
||||
}
|
||||
this.readWriteLock.writeLock().lock();
|
||||
try {
|
||||
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.servlet.mvc.method.annotation;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
|
||||
/**
|
||||
* No-op resolver for method arguments of type {@link kotlin.coroutines.Continuation}.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.3
|
||||
*/
|
||||
public class ContinuationHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return "kotlin.coroutines.Continuation".equals(parameter.getParameterType().getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@ import org.springframework.beans.factory.BeanFactoryAware;
|
|||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.KotlinDetector;
|
||||
import org.springframework.core.MethodIntrospector;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.core.ReactiveAdapterRegistry;
|
||||
|
|
@ -670,6 +671,9 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
|
|||
resolvers.add(new ErrorsMethodArgumentResolver());
|
||||
resolvers.add(new SessionStatusMethodArgumentResolver());
|
||||
resolvers.add(new UriComponentsBuilderMethodArgumentResolver());
|
||||
if (KotlinDetector.isKotlinPresent()) {
|
||||
resolvers.add(new ContinuationHandlerMethodArgumentResolver());
|
||||
}
|
||||
|
||||
// Custom arguments
|
||||
if (getCustomArgumentResolvers() != null) {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import java.util.concurrent.Callable;
|
|||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.core.KotlinDetector;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
|
@ -271,6 +272,8 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
|
|||
this.returnValue = returnValue;
|
||||
this.returnType = (returnValue instanceof ReactiveTypeHandler.CollectedValuesList ?
|
||||
((ReactiveTypeHandler.CollectedValuesList) returnValue).getReturnType() :
|
||||
KotlinDetector.isSuspendingFunction(super.getMethod()) ?
|
||||
ResolvableType.forMethodParameter(getReturnType()) :
|
||||
ResolvableType.forType(super.getGenericParameterType()).getGeneric());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,11 @@
|
|||
|
||||
package org.springframework.web.servlet.mvc.method.annotation
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.context.request.async.WebAsyncUtils
|
||||
import org.springframework.web.servlet.handler.PathPatternsParameterizedTest
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletResponse
|
||||
|
|
@ -71,6 +73,17 @@ class ServletAnnotationControllerHandlerMethodKotlinTests : AbstractServletHandl
|
|||
assertThat(response.contentAsString).isEqualTo("value1-12")
|
||||
}
|
||||
|
||||
@PathPatternsParameterizedTest
|
||||
fun suspendingMethod(usePathPatterns: Boolean) {
|
||||
initDispatcherServlet(CoroutinesController::class.java, usePathPatterns)
|
||||
|
||||
val request = MockHttpServletRequest("GET", "/suspending")
|
||||
request.isAsyncSupported = true
|
||||
val response = MockHttpServletResponse()
|
||||
servlet.service(request, response)
|
||||
assertThat(WebAsyncUtils.getAsyncManager(request).concurrentResult).isEqualTo("foo")
|
||||
}
|
||||
|
||||
|
||||
data class DataClass(val param1: String, val param2: Int)
|
||||
|
||||
|
|
@ -86,4 +99,15 @@ class ServletAnnotationControllerHandlerMethodKotlinTests : AbstractServletHandl
|
|||
fun handle(data: DataClassWithOptionalParameter) = "${data.param1}-${data.param2}"
|
||||
}
|
||||
|
||||
@RestController
|
||||
class CoroutinesController {
|
||||
|
||||
@Suppress("RedundantSuspendModifier")
|
||||
@RequestMapping("/suspending")
|
||||
suspend fun handle(): String {
|
||||
return "foo"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -412,8 +412,8 @@ and types like https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-co
|
|||
|
||||
Spring Framework provides support for Coroutines on the following scope:
|
||||
|
||||
* https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html[Deferred] and https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html[Flow] return values support in Spring WebFlux annotated `@Controller`
|
||||
* Suspending function support in Spring WebFlux annotated `@Controller`
|
||||
* https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html[Deferred] and https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html[Flow] return values support in Spring MVC and WebFlux annotated `@Controller`
|
||||
* Suspending function support in Spring MVC and WebFlux annotated `@Controller`
|
||||
* Extensions for WebFlux {doc-root}/spring-framework/docs/{spring-version}/kdoc-api/spring-framework/org.springframework.web.reactive.function.client/index.html[client] and {doc-root}/spring-framework/docs/{spring-version}/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/index.html[server] functional API.
|
||||
* WebFlux.fn {doc-root}/spring-framework/docs/{spring-version}/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/co-router.html[coRouter { }] DSL
|
||||
* Suspending function and `Flow` support in RSocket `@MessageMapping` annotated methods
|
||||
|
|
@ -434,7 +434,7 @@ dependencies {
|
|||
}
|
||||
----
|
||||
|
||||
Version `1.3.0` and above are supported.
|
||||
Version `1.3.9` and above are supported.
|
||||
|
||||
=== How Reactive translates to Coroutines?
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue