diff --git a/spring-core/kotlin-coroutines/src/main/kotlin/org/springframework/core/CoroutinesUtils.kt b/spring-core/kotlin-coroutines/src/main/kotlin/org/springframework/core/CoroutinesUtils.kt index edfd76d8c9..04a3817278 100644 --- a/spring-core/kotlin-coroutines/src/main/kotlin/org/springframework/core/CoroutinesUtils.kt +++ b/spring-core/kotlin-coroutines/src/main/kotlin/org/springframework/core/CoroutinesUtils.kt @@ -51,14 +51,6 @@ internal fun deferredToMono(source: Deferred) = internal fun monoToDeferred(source: Mono) = 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 } diff --git a/spring-core/src/main/java/org/springframework/core/KotlinDetector.java b/spring-core/src/main/java/org/springframework/core/KotlinDetector.java index 3be5346c7c..b1f35c6d59 100644 --- a/spring-core/src/main/java/org/springframework/core/KotlinDetector.java +++ b/spring-core/src/main/java/org/springframework/core/KotlinDetector.java @@ -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; + } + } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethod.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethod.java index 8ded74c07d..d4dfe15306 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethod.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethod.java @@ -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); } diff --git a/spring-web/spring-web.gradle b/spring-web/spring-web.gradle index 3ab5eddabe..3cb1801ad7 100644 --- a/spring-web/spring-web.gradle +++ b/spring-web/spring-web.gradle @@ -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")) diff --git a/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java b/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java index e5c71b44f3..e94d685a28 100644 --- a/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java +++ b/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java @@ -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); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java index 95d6357717..018a993498 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java @@ -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 { diff --git a/spring-webmvc/spring-webmvc.gradle b/spring-webmvc/spring-webmvc.gradle index be74d8baf2..92deba6aa2 100644 --- a/spring-webmvc/spring-webmvc.gradle +++ b/spring-webmvc/spring-webmvc.gradle @@ -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") diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java index b88c11c718..e8f842b35d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java @@ -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 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); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ContinuationHandlerMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ContinuationHandlerMethodArgumentResolver.java new file mode 100644 index 0000000000..82604d6d07 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ContinuationHandlerMethodArgumentResolver.java @@ -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; + } +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index 64f7799787..12a3229404 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -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) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java index 12ab262310..8cbdb10549 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java @@ -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()); } diff --git a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodKotlinTests.kt b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodKotlinTests.kt index aa44f91597..812704263a 100644 --- a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodKotlinTests.kt +++ b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodKotlinTests.kt @@ -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" + } + + } + } diff --git a/src/docs/asciidoc/languages/kotlin.adoc b/src/docs/asciidoc/languages/kotlin.adoc index 1acf4bb8cf..eeda18601f 100644 --- a/src/docs/asciidoc/languages/kotlin.adoc +++ b/src/docs/asciidoc/languages/kotlin.adoc @@ -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?