Support for @ControllerAdvice in WebFlux
Issue: SPR-15132
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* Copyright 2002-2017 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 java.lang.reflect.Method;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.FatalBeanException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.InitBinder;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.support.WebExchangeDataBinder;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.reactive.BindingContext;
|
||||
import org.springframework.web.reactive.HandlerResult;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.adapter.DefaultServerWebExchange;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* {@code @ControllerAdvice} related tests for {@link RequestMappingHandlerAdapter}.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class ControllerAdviceTests {
|
||||
|
||||
private ServerWebExchange exchange;
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/").build();
|
||||
MockServerHttpResponse response = new MockServerHttpResponse();
|
||||
this.exchange = new DefaultServerWebExchange(request, response);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void resolveExceptionGlobalHandler() throws Exception {
|
||||
testException(new IllegalAccessException(), "SecondControllerAdvice: IllegalAccessException");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveExceptionGlobalHandlerOrdered() throws Exception {
|
||||
testException(new IllegalStateException(), "OneControllerAdvice: IllegalStateException");
|
||||
}
|
||||
|
||||
@Test // SPR-12605
|
||||
public void resolveExceptionWithHandlerMethodArg() throws Exception {
|
||||
testException(new ArrayIndexOutOfBoundsException(), "HandlerMethod: handle");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveExceptionWithAssertionError() throws Exception {
|
||||
AssertionError error = new AssertionError("argh");
|
||||
testException(error, error.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveExceptionWithAssertionErrorAsRootCause() throws Exception {
|
||||
AssertionError cause = new AssertionError("argh");
|
||||
FatalBeanException exception = new FatalBeanException("wrapped", cause);
|
||||
testException(exception, cause.toString());
|
||||
}
|
||||
|
||||
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().orElse(null);
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void modelAttributeAdvice() throws Exception {
|
||||
ApplicationContext context = new AnnotationConfigApplicationContext(TestConfig.class);
|
||||
RequestMappingHandlerAdapter adapter = createAdapter(context);
|
||||
TestController controller = context.getBean(TestController.class);
|
||||
|
||||
Model model = handle(adapter, controller, "handle").getModel();
|
||||
|
||||
assertEquals(2, model.asMap().size());
|
||||
assertEquals("lAttr1", model.asMap().get("attr1"));
|
||||
assertEquals("gAttr2", model.asMap().get("attr2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initBinderAdvice() throws Exception {
|
||||
ApplicationContext context = new AnnotationConfigApplicationContext(TestConfig.class);
|
||||
RequestMappingHandlerAdapter adapter = createAdapter(context);
|
||||
TestController controller = context.getBean(TestController.class);
|
||||
|
||||
Validator validator = mock(Validator.class);
|
||||
controller.setValidator(validator);
|
||||
|
||||
BindingContext bindingContext = handle(adapter, controller, "handle").getBindingContext();
|
||||
|
||||
WebExchangeDataBinder binder = bindingContext.createDataBinder(this.exchange, "name");
|
||||
assertEquals(Collections.singletonList(validator), binder.getValidators());
|
||||
}
|
||||
|
||||
|
||||
private RequestMappingHandlerAdapter createAdapter(ApplicationContext context) throws Exception {
|
||||
RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
|
||||
adapter.setApplicationContext(context);
|
||||
adapter.afterPropertiesSet();
|
||||
return adapter;
|
||||
}
|
||||
|
||||
private HandlerResult handle(RequestMappingHandlerAdapter adapter,
|
||||
Object controller, String methodName) throws Exception {
|
||||
|
||||
Method method = controller.getClass().getMethod(methodName);
|
||||
HandlerMethod handlerMethod = new HandlerMethod(controller, method);
|
||||
return adapter.handle(this.exchange, handlerMethod).block(Duration.ZERO);
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
static class TestConfig {
|
||||
|
||||
@Bean
|
||||
public TestController testController() {
|
||||
return new TestController();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OneControllerAdvice testExceptionResolver() {
|
||||
return new OneControllerAdvice();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecondControllerAdvice anotherTestExceptionResolver() {
|
||||
return new SecondControllerAdvice();
|
||||
}
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class TestController {
|
||||
|
||||
private Validator validator;
|
||||
|
||||
private Throwable exception;
|
||||
|
||||
|
||||
void setValidator(Validator validator) {
|
||||
this.validator = validator;
|
||||
}
|
||||
|
||||
void setException(Throwable exception) {
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
|
||||
@InitBinder
|
||||
public void initDataBinder(WebDataBinder dataBinder) {
|
||||
if (this.validator != null) {
|
||||
dataBinder.addValidators(this.validator);
|
||||
}
|
||||
}
|
||||
|
||||
@ModelAttribute
|
||||
public void addAttributes(Model model) {
|
||||
model.addAttribute("attr1", "lAttr1");
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public void handle() throws Throwable {
|
||||
if (this.exception != null) {
|
||||
throw this.exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ControllerAdvice
|
||||
@Order(1)
|
||||
static class OneControllerAdvice {
|
||||
|
||||
@ModelAttribute
|
||||
public void addAttributes(Model model) {
|
||||
model.addAttribute("attr1", "gAttr1");
|
||||
model.addAttribute("attr2", "gAttr2");
|
||||
}
|
||||
|
||||
@ExceptionHandler
|
||||
public String handleException(IllegalStateException ex) {
|
||||
return "OneControllerAdvice: " + ClassUtils.getShortName(ex.getClass());
|
||||
}
|
||||
|
||||
@ExceptionHandler(ArrayIndexOutOfBoundsException.class)
|
||||
public String handleWithHandlerMethod(HandlerMethod handlerMethod) {
|
||||
return "HandlerMethod: " + handlerMethod.getMethod().getName();
|
||||
}
|
||||
|
||||
@ExceptionHandler(AssertionError.class)
|
||||
public String handleAssertionError(Error err) {
|
||||
return err.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@ControllerAdvice
|
||||
@Order(2)
|
||||
static class SecondControllerAdvice {
|
||||
|
||||
@ExceptionHandler({IllegalStateException.class, IllegalAccessException.class})
|
||||
public String handleException(Exception ex) {
|
||||
return "SecondControllerAdvice: " + ClassUtils.getShortName(ex.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,7 +31,6 @@ import org.springframework.core.ReactiveAdapterRegistry;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.annotation.InitBinder;
|
||||
@@ -48,8 +47,8 @@ import org.springframework.web.server.adapter.DefaultServerWebExchange;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter.BINDER_METHODS;
|
||||
import static org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter.ATTRIBUTE_METHODS;
|
||||
import static org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter.BINDER_METHODS;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ModelInitializer}.
|
||||
@@ -76,8 +75,10 @@ public class ModelInitializerTests {
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void basic() throws Exception {
|
||||
TestController controller = new TestController();
|
||||
|
||||
Validator validator = mock(Validator.class);
|
||||
Object controller = new TestController(validator);
|
||||
controller.setValidator(validator);
|
||||
|
||||
List<SyncInvocableHandlerMethod> binderMethods = getBinderMethods(controller);
|
||||
List<InvocableHandlerMethod> attributeMethods = getAttributeMethods(controller);
|
||||
@@ -131,16 +132,18 @@ public class ModelInitializerTests {
|
||||
@SuppressWarnings("unused")
|
||||
private static class TestController {
|
||||
|
||||
private Validator[] validators;
|
||||
private Validator validator;
|
||||
|
||||
public TestController(Validator... validators) {
|
||||
this.validators = validators;
|
||||
|
||||
void setValidator(Validator validator) {
|
||||
this.validator = validator;
|
||||
}
|
||||
|
||||
|
||||
@InitBinder
|
||||
public void initDataBinder(WebDataBinder dataBinder) {
|
||||
if (!ObjectUtils.isEmpty(this.validators)) {
|
||||
dataBinder.addValidators(this.validators);
|
||||
if (this.validator != null) {
|
||||
dataBinder.addValidators(this.validator);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user