Merge branch '6.1.x'
This commit is contained in:
commit
5253af1a04
|
@ -36,6 +36,7 @@ import kotlin.reflect.full.KClasses;
|
||||||
import kotlin.reflect.jvm.KCallablesJvm;
|
import kotlin.reflect.jvm.KCallablesJvm;
|
||||||
import kotlin.reflect.jvm.ReflectJvmMapping;
|
import kotlin.reflect.jvm.ReflectJvmMapping;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.scheduler.Scheduler;
|
||||||
|
|
||||||
import org.springframework.core.CoroutinesUtils;
|
import org.springframework.core.CoroutinesUtils;
|
||||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||||
|
@ -60,6 +61,12 @@ import org.springframework.web.server.ServerWebExchange;
|
||||||
* Extension of {@link HandlerMethod} that invokes the underlying method with
|
* Extension of {@link HandlerMethod} that invokes the underlying method with
|
||||||
* argument values resolved from the current HTTP request through a list of
|
* argument values resolved from the current HTTP request through a list of
|
||||||
* {@link HandlerMethodArgumentResolver}.
|
* {@link HandlerMethodArgumentResolver}.
|
||||||
|
* <p>By default, the method invocation happens on the thread from which the
|
||||||
|
* {@code Mono} was subscribed to, or in some cases the thread that emitted one
|
||||||
|
* of the resolved arguments (e.g. when the request body needs to be decoded).
|
||||||
|
* To ensure a predictable thread for the underlying method's invocation,
|
||||||
|
* a {@link Scheduler} can optionally be provided via
|
||||||
|
* {@link #setInvocationScheduler(Scheduler)}.
|
||||||
*
|
*
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
* @author Juergen Hoeller
|
* @author Juergen Hoeller
|
||||||
|
@ -86,6 +93,9 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
||||||
|
|
||||||
private Class<?>[] validationGroups = EMPTY_GROUPS;
|
private Class<?>[] validationGroups = EMPTY_GROUPS;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private Scheduler invocationScheduler;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an instance from a {@code HandlerMethod}.
|
* Create an instance from a {@code HandlerMethod}.
|
||||||
|
@ -154,6 +164,13 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
||||||
methodValidator.determineValidationGroups(getBean(), getBridgedMethod()) : EMPTY_GROUPS);
|
methodValidator.determineValidationGroups(getBean(), getBridgedMethod()) : EMPTY_GROUPS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the {@link Scheduler} on which to perform the method invocation.
|
||||||
|
* @since 6.1.6
|
||||||
|
*/
|
||||||
|
public void setInvocationScheduler(@Nullable Scheduler invocationScheduler) {
|
||||||
|
this.invocationScheduler = invocationScheduler;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoke the method for the given exchange.
|
* Invoke the method for the given exchange.
|
||||||
|
@ -166,7 +183,7 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
||||||
public Mono<HandlerResult> invoke(
|
public Mono<HandlerResult> invoke(
|
||||||
ServerWebExchange exchange, BindingContext bindingContext, Object... providedArgs) {
|
ServerWebExchange exchange, BindingContext bindingContext, Object... providedArgs) {
|
||||||
|
|
||||||
return getMethodArgumentValues(exchange, bindingContext, providedArgs).flatMap(args -> {
|
return getMethodArgumentValuesOnScheduler(exchange, bindingContext, providedArgs).flatMap(args -> {
|
||||||
if (shouldValidateArguments() && this.methodValidator != null) {
|
if (shouldValidateArguments() && this.methodValidator != null) {
|
||||||
this.methodValidator.applyArgumentValidation(
|
this.methodValidator.applyArgumentValidation(
|
||||||
getBean(), getBridgedMethod(), getMethodParameters(), args, this.validationGroups);
|
getBean(), getBridgedMethod(), getMethodParameters(), args, this.validationGroups);
|
||||||
|
@ -218,6 +235,12 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Mono<Object[]> getMethodArgumentValuesOnScheduler(
|
||||||
|
ServerWebExchange exchange, BindingContext bindingContext, Object... providedArgs) {
|
||||||
|
Mono<Object[]> argumentValuesMono = getMethodArgumentValues(exchange, bindingContext, providedArgs);
|
||||||
|
return this.invocationScheduler != null ? argumentValuesMono.publishOn(this.invocationScheduler) : argumentValuesMono;
|
||||||
|
}
|
||||||
|
|
||||||
private Mono<Object[]> getMethodArgumentValues(
|
private Mono<Object[]> getMethodArgumentValues(
|
||||||
ServerWebExchange exchange, BindingContext bindingContext, Object... providedArgs) {
|
ServerWebExchange exchange, BindingContext bindingContext, Object... providedArgs) {
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2023 the original author or authors.
|
* Copyright 2002-2024 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -28,6 +28,7 @@ import java.util.function.Predicate;
|
||||||
|
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import reactor.core.scheduler.Scheduler;
|
||||||
|
|
||||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
|
@ -103,6 +104,12 @@ class ControllerMethodResolver {
|
||||||
|
|
||||||
private final ReactiveAdapterRegistry reactiveAdapterRegistry;
|
private final ReactiveAdapterRegistry reactiveAdapterRegistry;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final Scheduler invocationScheduler;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final Predicate<? super HandlerMethod> blockingMethodPredicate;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private final MethodValidator methodValidator;
|
private final MethodValidator methodValidator;
|
||||||
|
|
||||||
|
@ -125,7 +132,9 @@ class ControllerMethodResolver {
|
||||||
ControllerMethodResolver(
|
ControllerMethodResolver(
|
||||||
ArgumentResolverConfigurer customResolvers, ReactiveAdapterRegistry adapterRegistry,
|
ArgumentResolverConfigurer customResolvers, ReactiveAdapterRegistry adapterRegistry,
|
||||||
ConfigurableApplicationContext context, List<HttpMessageReader<?>> readers,
|
ConfigurableApplicationContext context, List<HttpMessageReader<?>> readers,
|
||||||
@Nullable WebBindingInitializer webBindingInitializer) {
|
@Nullable WebBindingInitializer webBindingInitializer,
|
||||||
|
@Nullable Scheduler invocationScheduler,
|
||||||
|
@Nullable Predicate<? super HandlerMethod> blockingMethodPredicate) {
|
||||||
|
|
||||||
Assert.notNull(customResolvers, "ArgumentResolverConfigurer is required");
|
Assert.notNull(customResolvers, "ArgumentResolverConfigurer is required");
|
||||||
Assert.notNull(adapterRegistry, "ReactiveAdapterRegistry is required");
|
Assert.notNull(adapterRegistry, "ReactiveAdapterRegistry is required");
|
||||||
|
@ -137,6 +146,8 @@ class ControllerMethodResolver {
|
||||||
this.requestMappingResolvers = requestMappingResolvers(customResolvers, adapterRegistry, context, readers);
|
this.requestMappingResolvers = requestMappingResolvers(customResolvers, adapterRegistry, context, readers);
|
||||||
this.exceptionHandlerResolvers = exceptionHandlerResolvers(customResolvers, adapterRegistry, context);
|
this.exceptionHandlerResolvers = exceptionHandlerResolvers(customResolvers, adapterRegistry, context);
|
||||||
this.reactiveAdapterRegistry = adapterRegistry;
|
this.reactiveAdapterRegistry = adapterRegistry;
|
||||||
|
this.invocationScheduler = invocationScheduler;
|
||||||
|
this.blockingMethodPredicate = blockingMethodPredicate;
|
||||||
|
|
||||||
if (BEAN_VALIDATION_PRESENT) {
|
if (BEAN_VALIDATION_PRESENT) {
|
||||||
this.methodValidator = HandlerMethodValidator.from(webBindingInitializer, null,
|
this.methodValidator = HandlerMethodValidator.from(webBindingInitializer, null,
|
||||||
|
@ -287,6 +298,21 @@ class ControllerMethodResolver {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a {@link Scheduler} for the given method if it is considered
|
||||||
|
* blocking by the underlying blocking method predicate, or null if no
|
||||||
|
* particular scheduler should be used for this method invocation.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public Scheduler getSchedulerFor(HandlerMethod handlerMethod) {
|
||||||
|
if (this.invocationScheduler != null) {
|
||||||
|
Assert.state(this.blockingMethodPredicate != null, "Expected HandlerMethod Predicate");
|
||||||
|
if (this.blockingMethodPredicate.test(handlerMethod)) {
|
||||||
|
return this.invocationScheduler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an {@link InvocableHandlerMethod} for the given
|
* Return an {@link InvocableHandlerMethod} for the given
|
||||||
|
@ -297,6 +323,7 @@ class ControllerMethodResolver {
|
||||||
invocable.setArgumentResolvers(this.requestMappingResolvers);
|
invocable.setArgumentResolvers(this.requestMappingResolvers);
|
||||||
invocable.setReactiveAdapterRegistry(this.reactiveAdapterRegistry);
|
invocable.setReactiveAdapterRegistry(this.reactiveAdapterRegistry);
|
||||||
invocable.setMethodValidator(this.methodValidator);
|
invocable.setMethodValidator(this.methodValidator);
|
||||||
|
invocable.setInvocationScheduler(getSchedulerFor(handlerMethod));
|
||||||
return invocable;
|
return invocable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -225,7 +225,8 @@ public class RequestMappingHandlerAdapter
|
||||||
|
|
||||||
this.methodResolver = new ControllerMethodResolver(
|
this.methodResolver = new ControllerMethodResolver(
|
||||||
this.argumentResolverConfigurer, this.reactiveAdapterRegistry, this.applicationContext,
|
this.argumentResolverConfigurer, this.reactiveAdapterRegistry, this.applicationContext,
|
||||||
this.messageReaders, this.webBindingInitializer);
|
this.messageReaders, this.webBindingInitializer,
|
||||||
|
this.scheduler, this.blockingMethodPredicate);
|
||||||
|
|
||||||
this.modelInitializer = new ModelInitializer(this.methodResolver, this.reactiveAdapterRegistry);
|
this.modelInitializer = new ModelInitializer(this.methodResolver, this.reactiveAdapterRegistry);
|
||||||
}
|
}
|
||||||
|
@ -260,11 +261,9 @@ public class RequestMappingHandlerAdapter
|
||||||
.doOnNext(result -> result.setExceptionHandler(exceptionHandler))
|
.doOnNext(result -> result.setExceptionHandler(exceptionHandler))
|
||||||
.onErrorResume(ex -> exceptionHandler.handleError(exchange, ex));
|
.onErrorResume(ex -> exceptionHandler.handleError(exchange, ex));
|
||||||
|
|
||||||
if (this.scheduler != null) {
|
Scheduler optionalScheduler = this.methodResolver.getSchedulerFor(handlerMethod);
|
||||||
Assert.state(this.blockingMethodPredicate != null, "Expected HandlerMethod Predicate");
|
if (optionalScheduler != null) {
|
||||||
if (this.blockingMethodPredicate.test(handlerMethod)) {
|
return resultMono.subscribeOn(optionalScheduler);
|
||||||
resultMono = resultMono.subscribeOn(this.scheduler);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return resultMono;
|
return resultMono;
|
||||||
|
|
|
@ -26,6 +26,8 @@ import java.util.List;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.scheduler.Scheduler;
|
||||||
|
import reactor.core.scheduler.Schedulers;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
import org.springframework.core.io.buffer.DataBuffer;
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
@ -75,6 +77,15 @@ class InvocableHandlerMethodTests {
|
||||||
assertHandlerResultValue(mono, "success:value1");
|
assertHandlerResultValue(mono, "success:value1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveArgOnSchedulerThread() {
|
||||||
|
this.resolvers.add(stubResolver(Mono.<Object>just("success").publishOn(Schedulers.newSingle("wrong"))));
|
||||||
|
Method method = ResolvableMethod.on(TestController.class).mockCall(o -> o.singleArgThread(null)).method();
|
||||||
|
Mono<HandlerResult> mono = invokeOnScheduler(Schedulers.newSingle("good"), new TestController(), method);
|
||||||
|
|
||||||
|
assertHandlerResultValue(mono, "success on thread: good-", false);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void resolveNoArgValue() {
|
void resolveNoArgValue() {
|
||||||
this.resolvers.add(stubResolver(Mono.empty()));
|
this.resolvers.add(stubResolver(Mono.empty()));
|
||||||
|
@ -92,6 +103,14 @@ class InvocableHandlerMethodTests {
|
||||||
assertHandlerResultValue(mono, "success");
|
assertHandlerResultValue(mono, "success");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveNoArgsOnSchedulerThread() {
|
||||||
|
Method method = ResolvableMethod.on(TestController.class).mockCall(TestController::noArgsThread).method();
|
||||||
|
Mono<HandlerResult> mono = invokeOnScheduler(Schedulers.newSingle("good"), new TestController(), method);
|
||||||
|
|
||||||
|
assertHandlerResultValue(mono, "on thread: good-", false);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void cannotResolveArg() {
|
void cannotResolveArg() {
|
||||||
Method method = ResolvableMethod.on(TestController.class).mockCall(o -> o.singleArg(null)).method();
|
Method method = ResolvableMethod.on(TestController.class).mockCall(o -> o.singleArg(null)).method();
|
||||||
|
@ -229,6 +248,13 @@ class InvocableHandlerMethodTests {
|
||||||
return invocable.invoke(this.exchange, new BindingContext(), providedArgs);
|
return invocable.invoke(this.exchange, new BindingContext(), providedArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Mono<HandlerResult> invokeOnScheduler(Scheduler scheduler, Object handler, Method method, Object... providedArgs) {
|
||||||
|
InvocableHandlerMethod invocable = new InvocableHandlerMethod(handler, method);
|
||||||
|
invocable.setArgumentResolvers(this.resolvers);
|
||||||
|
invocable.setInvocationScheduler(scheduler);
|
||||||
|
return invocable.invoke(this.exchange, new BindingContext(), providedArgs);
|
||||||
|
}
|
||||||
|
|
||||||
private HandlerMethodArgumentResolver stubResolver(Object stubValue) {
|
private HandlerMethodArgumentResolver stubResolver(Object stubValue) {
|
||||||
return stubResolver(Mono.just(stubValue));
|
return stubResolver(Mono.just(stubValue));
|
||||||
}
|
}
|
||||||
|
@ -241,8 +267,19 @@ class InvocableHandlerMethodTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertHandlerResultValue(Mono<HandlerResult> mono, String expected) {
|
private void assertHandlerResultValue(Mono<HandlerResult> mono, String expected) {
|
||||||
|
this.assertHandlerResultValue(mono, expected, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertHandlerResultValue(Mono<HandlerResult> mono, String expected, boolean strict) {
|
||||||
StepVerifier.create(mono)
|
StepVerifier.create(mono)
|
||||||
.consumeNextWith(result -> assertThat(result.getReturnValue()).isEqualTo(expected))
|
.assertNext(result -> {
|
||||||
|
if (strict) {
|
||||||
|
assertThat(result.getReturnValue()).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
assertThat(String.valueOf(result.getReturnValue())).startsWith(expected);
|
||||||
|
}
|
||||||
|
})
|
||||||
.expectComplete()
|
.expectComplete()
|
||||||
.verify();
|
.verify();
|
||||||
}
|
}
|
||||||
|
@ -259,6 +296,14 @@ class InvocableHandlerMethodTests {
|
||||||
return "success";
|
return "success";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String singleArgThread(String q) {
|
||||||
|
return q + " on thread: " + Thread.currentThread().getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
String noArgsThread() {
|
||||||
|
return "on thread: " + Thread.currentThread().getName();
|
||||||
|
}
|
||||||
|
|
||||||
void exceptionMethod() {
|
void exceptionMethod() {
|
||||||
throw new IllegalStateException("boo");
|
throw new IllegalStateException("boo");
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,8 +19,11 @@ package org.springframework.web.reactive.result.method.annotation;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.scheduler.Schedulers;
|
||||||
|
|
||||||
import org.springframework.beans.FatalBeanException;
|
import org.springframework.beans.FatalBeanException;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
|
@ -28,6 +31,8 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.util.ClassUtils;
|
import org.springframework.util.ClassUtils;
|
||||||
|
@ -38,10 +43,13 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.InitBinder;
|
import org.springframework.web.bind.annotation.InitBinder;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.support.WebExchangeDataBinder;
|
import org.springframework.web.bind.support.WebExchangeDataBinder;
|
||||||
import org.springframework.web.method.HandlerMethod;
|
import org.springframework.web.method.HandlerMethod;
|
||||||
import org.springframework.web.reactive.BindingContext;
|
import org.springframework.web.reactive.BindingContext;
|
||||||
import org.springframework.web.reactive.HandlerResult;
|
import org.springframework.web.reactive.HandlerResult;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
|
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
|
||||||
import org.springframework.web.testfixture.server.MockServerWebExchange;
|
import org.springframework.web.testfixture.server.MockServerWebExchange;
|
||||||
|
|
||||||
|
@ -58,6 +66,14 @@ class ControllerAdviceTests {
|
||||||
private final MockServerWebExchange exchange =
|
private final MockServerWebExchange exchange =
|
||||||
MockServerWebExchange.from(MockServerHttpRequest.get("/"));
|
MockServerWebExchange.from(MockServerHttpRequest.get("/"));
|
||||||
|
|
||||||
|
private final MockServerWebExchange postExchange =
|
||||||
|
MockServerWebExchange.from(MockServerHttpRequest.post("/")
|
||||||
|
.body(Flux.defer(() -> {
|
||||||
|
byte[] bytes = "request body".getBytes();
|
||||||
|
DataBuffer buffer = DefaultDataBufferFactory.sharedInstance.wrap(bytes);
|
||||||
|
return Flux.just(buffer).subscribeOn(Schedulers.newSingle("bad thread"));
|
||||||
|
})));
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void resolveExceptionGlobalHandler() throws Exception {
|
void resolveExceptionGlobalHandler() throws Exception {
|
||||||
|
@ -88,15 +104,34 @@ class ControllerAdviceTests {
|
||||||
testException(exception, rootCause.toString());
|
testException(exception, rootCause.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void testException(Throwable exception, String expected) throws Exception {
|
@Test
|
||||||
|
void resolveOnAnotherThread() throws Exception {
|
||||||
ApplicationContext context = new AnnotationConfigApplicationContext(TestConfig.class);
|
ApplicationContext context = new AnnotationConfigApplicationContext(TestConfig.class);
|
||||||
RequestMappingHandlerAdapter adapter = createAdapter(context);
|
final RequestMappingHandlerAdapter adapter = createAdapterWithExecutor(context, "good thread");
|
||||||
|
|
||||||
TestController controller = context.getBean(TestController.class);
|
TestController controller = context.getBean(TestController.class);
|
||||||
controller.setException(exception);
|
|
||||||
|
|
||||||
Object actual = handle(adapter, controller, "handle").getReturnValue();
|
Object actual = handle(adapter, controller, this.postExchange, Duration.ofMillis(100),
|
||||||
assertThat(actual).isEqualTo(expected);
|
"threadWithArg", String.class).getReturnValue();
|
||||||
|
assertThat(actual).isEqualTo("request body from good thread");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveEmptyOnAnotherThread() throws Exception {
|
||||||
|
ApplicationContext context = new AnnotationConfigApplicationContext(TestConfig.class);
|
||||||
|
final RequestMappingHandlerAdapter adapter = createAdapterWithExecutor(context, "good thread");
|
||||||
|
|
||||||
|
TestController controller = context.getBean(TestController.class);
|
||||||
|
|
||||||
|
Object actual = handle(adapter, controller, this.postExchange, Duration.ofMillis(100), "thread")
|
||||||
|
.getReturnValue();
|
||||||
|
assertThat(actual).isEqualTo("hello from good thread");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveExceptionOnAnotherThread() throws Exception {
|
||||||
|
testException(new IllegalArgumentException(), "good thread",
|
||||||
|
"OneControllerAdvice: IllegalArgumentException on thread good thread");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -128,6 +163,17 @@ class ControllerAdviceTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void testException(Throwable exception, String expected) throws Exception {
|
||||||
|
ApplicationContext context = new AnnotationConfigApplicationContext(TestConfig.class);
|
||||||
|
RequestMappingHandlerAdapter adapter = createAdapter(context);
|
||||||
|
|
||||||
|
TestController controller = context.getBean(TestController.class);
|
||||||
|
controller.setException(exception);
|
||||||
|
|
||||||
|
Object actual = handle(adapter, controller, "handle").getReturnValue();
|
||||||
|
assertThat(actual).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
private RequestMappingHandlerAdapter createAdapter(ApplicationContext context) throws Exception {
|
private RequestMappingHandlerAdapter createAdapter(ApplicationContext context) throws Exception {
|
||||||
RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
|
RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
|
||||||
adapter.setApplicationContext(context);
|
adapter.setApplicationContext(context);
|
||||||
|
@ -135,12 +181,45 @@ class ControllerAdviceTests {
|
||||||
return adapter;
|
return adapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
private HandlerResult handle(RequestMappingHandlerAdapter adapter,
|
private void testException(Throwable exception, String threadName, String expected) throws Exception {
|
||||||
Object controller, String methodName) throws Exception {
|
ApplicationContext context = new AnnotationConfigApplicationContext(TestConfig.class);
|
||||||
|
RequestMappingHandlerAdapter adapter = createAdapterWithExecutor(context, threadName);
|
||||||
|
|
||||||
Method method = controller.getClass().getMethod(methodName);
|
TestController controller = context.getBean(TestController.class);
|
||||||
|
controller.setException(exception);
|
||||||
|
|
||||||
|
Object actual = handle(adapter, controller, this.postExchange, Duration.ofMillis(100)
|
||||||
|
, "threadWithArg", String.class).getReturnValue();
|
||||||
|
assertThat(actual).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RequestMappingHandlerAdapter createAdapterWithExecutor(ApplicationContext context, String threadName) throws Exception {
|
||||||
|
RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
|
||||||
|
adapter.setApplicationContext(context);
|
||||||
|
adapter.setBlockingExecutor(Executors.newSingleThreadExecutor(r -> {
|
||||||
|
Thread t = new Thread(r);
|
||||||
|
t.setName(threadName);
|
||||||
|
return t;
|
||||||
|
}));
|
||||||
|
adapter.setBlockingMethodPredicate(m -> true);
|
||||||
|
adapter.afterPropertiesSet();
|
||||||
|
return adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HandlerResult handle(RequestMappingHandlerAdapter adapter,
|
||||||
|
Object controller, String methodName, Class<?>... parameterTypes) throws Exception {
|
||||||
|
return handle(adapter, controller, this.exchange, Duration.ZERO, methodName, parameterTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HandlerResult handle(RequestMappingHandlerAdapter adapter,
|
||||||
|
Object controller, ServerWebExchange exchange, Duration timeout,
|
||||||
|
String methodName, Class<?>... parameterTypes) throws Exception {
|
||||||
|
|
||||||
|
Method method = controller.getClass().getMethod(methodName, parameterTypes);
|
||||||
HandlerMethod handlerMethod = new HandlerMethod(controller, method);
|
HandlerMethod handlerMethod = new HandlerMethod(controller, method);
|
||||||
return adapter.handle(this.exchange, handlerMethod).block(Duration.ZERO);
|
HandlerResult handlerResult = adapter.handle(exchange, handlerMethod).block(timeout);
|
||||||
|
assertThat(handlerResult).isNotNull();
|
||||||
|
return handlerResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -198,6 +277,18 @@ class ControllerAdviceTests {
|
||||||
throw this.exception;
|
throw this.exception;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public String threadWithArg(@RequestBody String body) throws Throwable {
|
||||||
|
handle();
|
||||||
|
return body + " from " + Thread.currentThread().getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public String thread() throws Throwable {
|
||||||
|
handle();
|
||||||
|
return "hello from " + Thread.currentThread().getName();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ControllerAdvice
|
@ControllerAdvice
|
||||||
|
@ -220,6 +311,12 @@ class ControllerAdviceTests {
|
||||||
return "HandlerMethod: " + handlerMethod.getMethod().getName();
|
return "HandlerMethod: " + handlerMethod.getMethod().getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(IllegalArgumentException.class)
|
||||||
|
public String handleOnThread(IllegalArgumentException ex) {
|
||||||
|
return "OneControllerAdvice: " + ClassUtils.getShortName(ex.getClass()) +
|
||||||
|
" on thread " + Thread.currentThread().getName();
|
||||||
|
}
|
||||||
|
|
||||||
@ExceptionHandler(AssertionError.class)
|
@ExceptionHandler(AssertionError.class)
|
||||||
public String handleAssertionError(Error err) {
|
public String handleAssertionError(Error err) {
|
||||||
return err.toString();
|
return err.toString();
|
||||||
|
|
|
@ -77,7 +77,7 @@ class ControllerMethodResolverTests {
|
||||||
|
|
||||||
this.methodResolver = new ControllerMethodResolver(
|
this.methodResolver = new ControllerMethodResolver(
|
||||||
resolvers, ReactiveAdapterRegistry.getSharedInstance(), applicationContext,
|
resolvers, ReactiveAdapterRegistry.getSharedInstance(), applicationContext,
|
||||||
codecs.getReaders(), null);
|
codecs.getReaders(), null, null, null);
|
||||||
|
|
||||||
Method method = ResolvableMethod.on(TestController.class).mockCall(TestController::handle).method();
|
Method method = ResolvableMethod.on(TestController.class).mockCall(TestController::handle).method();
|
||||||
this.handlerMethod = new HandlerMethod(new TestController(), method);
|
this.handlerMethod = new HandlerMethod(new TestController(), method);
|
||||||
|
|
|
@ -80,7 +80,7 @@ class ModelInitializerTests {
|
||||||
|
|
||||||
ControllerMethodResolver methodResolver = new ControllerMethodResolver(
|
ControllerMethodResolver methodResolver = new ControllerMethodResolver(
|
||||||
resolverConfigurer, adapterRegistry, new StaticApplicationContext(),
|
resolverConfigurer, adapterRegistry, new StaticApplicationContext(),
|
||||||
Collections.emptyList(), null);
|
Collections.emptyList(), null, null, null);
|
||||||
|
|
||||||
this.modelInitializer = new ModelInitializer(methodResolver, adapterRegistry);
|
this.modelInitializer = new ModelInitializer(methodResolver, adapterRegistry);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2023 the original author or authors.
|
* Copyright 2002-2024 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -78,8 +78,8 @@ class RequestMappingIntegrationTests extends AbstractRequestMappingIntegrationTe
|
||||||
url += "/";
|
url += "/";
|
||||||
assertThat(getRestTemplate().getForObject(url, String.class)).isEqualTo("root");
|
assertThat(getRestTemplate().getForObject(url, String.class)).isEqualTo("root");
|
||||||
|
|
||||||
assertThat(getApplicationContext().getBean(TestExecutor.class).invocationCount.get()).isEqualTo(2);
|
assertThat(getApplicationContext().getBean(TestExecutor.class).invocationCount.get()).isEqualTo(4);
|
||||||
assertThat(getApplicationContext().getBean(TestPredicate.class).invocationCount.get()).isEqualTo(2);
|
assertThat(getApplicationContext().getBean(TestPredicate.class).invocationCount.get()).isEqualTo(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedHttpServerTest
|
@ParameterizedHttpServerTest
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2023 the original author or authors.
|
* Copyright 2002-2024 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -53,7 +53,7 @@ class ModelInitializerKotlinTests {
|
||||||
val resolverConfigurer = ArgumentResolverConfigurer()
|
val resolverConfigurer = ArgumentResolverConfigurer()
|
||||||
resolverConfigurer.addCustomResolver(ModelMethodArgumentResolver(adapterRegistry))
|
resolverConfigurer.addCustomResolver(ModelMethodArgumentResolver(adapterRegistry))
|
||||||
val methodResolver = ControllerMethodResolver(resolverConfigurer, adapterRegistry, StaticApplicationContext(),
|
val methodResolver = ControllerMethodResolver(resolverConfigurer, adapterRegistry, StaticApplicationContext(),
|
||||||
emptyList(), null)
|
emptyList(), null, null, null)
|
||||||
modelInitializer = ModelInitializer(methodResolver, adapterRegistry)
|
modelInitializer = ModelInitializer(methodResolver, adapterRegistry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2022 the original author or authors.
|
* Copyright 2002-2024 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -27,14 +27,11 @@ import org.springframework.http.ProblemDetail;
|
||||||
import org.springframework.web.ErrorResponse;
|
import org.springframework.web.ErrorResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* By default, when the DispatcherServlet can't find a handler for a request it
|
* Thrown when the {@link DispatcherServlet} can't find a handler for a request,
|
||||||
* sends a 404 response. However, if its property "throwExceptionIfNoHandlerFound"
|
* which may be handled with a configured {@link HandlerExceptionResolver}.
|
||||||
* is set to {@code true} this exception is raised and may be handled with
|
|
||||||
* a configured HandlerExceptionResolver.
|
|
||||||
*
|
*
|
||||||
* @author Brian Clozel
|
* @author Brian Clozel
|
||||||
* @since 4.0
|
* @since 4.0
|
||||||
* @see DispatcherServlet#setThrowExceptionIfNoHandlerFound(boolean)
|
|
||||||
* @see DispatcherServlet#noHandlerFound(HttpServletRequest, HttpServletResponse)
|
* @see DispatcherServlet#noHandlerFound(HttpServletRequest, HttpServletResponse)
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("serial")
|
@SuppressWarnings("serial")
|
||||||
|
|
Loading…
Reference in New Issue