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
This commit is contained in:
Sebastien Deleuze
2019-03-15 16:38:05 +01:00
parent 3cce85b402
commit 22cf83edba
6 changed files with 261 additions and 3 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -16,6 +16,8 @@
package org.springframework.web.reactive.result.method;
import static org.springframework.web.reactive.result.method.InvocableHandlerMethodKt.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
@@ -27,6 +29,7 @@ import java.util.stream.Stream;
import reactor.core.publisher.Mono;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.KotlinDetector;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.ReactiveAdapter;
@@ -48,6 +51,7 @@ import org.springframework.web.server.ServerWebExchange;
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @author Sebastien Deleuze
* @since 5.0
*/
public class InvocableHandlerMethod extends HandlerMethod {
@@ -136,7 +140,13 @@ public class InvocableHandlerMethod extends HandlerMethod {
Object value;
try {
ReflectionUtils.makeAccessible(getBridgedMethod());
value = getBridgedMethod().invoke(getBean(), args);
Method method = getBridgedMethod();
if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(method.getDeclaringClass())) {
value = invokeHandlerMethod(method, getBean(), args);
}
else {
value = method.invoke(getBean(), args);
}
}
catch (IllegalArgumentException ex) {
assertTargetBean(getBridgedMethod(), getBean(), args);

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2002-2019 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
*
* http://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.reactive.result.method.annotation;
import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter;
import org.springframework.web.reactive.BindingContext;
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
import org.springframework.web.server.ServerWebExchange;
/**
* No-op resolver for method arguments of type {@link kotlin.coroutines.Continuation}.
*
* @author Sebastien Deleuze
* @since 5.2
*/
public class ContinuationHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return "kotlin.coroutines.Continuation".equals(parameter.getParameterType().getName());
}
@Override
public Mono<Object> resolveArgument(MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange) {
return Mono.empty();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -32,6 +32,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.KotlinDetector;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.annotation.AnnotatedElementUtils;
@@ -190,6 +191,9 @@ class ControllerMethodResolver {
result.add(new RequestAttributeMethodArgumentResolver(beanFactory, reactiveRegistry));
// Type-based...
if (KotlinDetector.isKotlinPresent()) {
result.add(new ContinuationHandlerMethodArgumentResolver());
}
if (!readers.isEmpty()) {
result.add(new HttpEntityArgumentResolver(readers, reactiveRegistry));
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2002-2019 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
*
* http://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.reactive.result.method
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.reactor.mono
import reactor.core.publisher.onErrorMap
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import kotlin.reflect.full.callSuspend
import kotlin.reflect.jvm.kotlinFunction
/**
* Invoke an handler method converting suspending method to {@link Mono} if necessary.
*
* @author Sebastien Deleuze
* @since 5.2
*/
internal fun invokeHandlerMethod(method: Method, bean: Any, vararg args: Any?): Any? {
val function = method.kotlinFunction!!
return if (function.isSuspend) {
GlobalScope.mono { function.callSuspend(bean, *args.sliceArray(0..(args.size-2)))
.let { if (it == Unit) null else it} }
.onErrorMap(InvocationTargetException::class) { it.targetException }
}
else {
function.call(bean, *args)
}
}