From 9955ee7e8a46e0774b106f10bb0e17e0b332b20e Mon Sep 17 00:00:00 2001 From: Maurice Zeijen Date: Mon, 10 Jul 2023 10:59:06 +0200 Subject: [PATCH] Order auto-configured ProblemDetailsExceptionHandler beans Add `@Order(0)` to the WebMVC and Webflux `ProblemDetailsExceptionHandler` beans. This makes it easier to create custom `@ControllerAdvice` beans that must be ordered before or after the `ProblemDetailsExceptionHandler`. See gh-36288 --- .../reactive/WebFluxAutoConfiguration.java | 1 + .../web/servlet/WebMvcAutoConfiguration.java | 1 + .../WebFluxAutoConfigurationTests.java | 39 ++++++++++++++++++ .../servlet/WebMvcAutoConfigurationTests.java | 40 +++++++++++++++++++ 4 files changed, 81 insertions(+) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java index c3d77d94fad..e9ebef36249 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java @@ -357,6 +357,7 @@ public class WebFluxAutoConfiguration { @Bean @ConditionalOnMissingBean(ResponseEntityExceptionHandler.class) + @Order(0) ProblemDetailsExceptionHandler problemDetailsExceptionHandler() { return new ProblemDetailsExceptionHandler(); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java index f3e8097248f..a6df6386d7e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java @@ -662,6 +662,7 @@ public class WebMvcAutoConfiguration { @Bean @ConditionalOnMissingBean(ResponseEntityExceptionHandler.class) + @Order(0) ProblemDetailsExceptionHandler problemDetailsExceptionHandler() { return new ProblemDetailsExceptionHandler(); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java index 65be8c0f79e..d6e16cfe151 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java @@ -31,6 +31,7 @@ import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import java.util.stream.Collectors; import jakarta.validation.ValidatorFactory; import org.aspectj.lang.JoinPoint; @@ -80,6 +81,7 @@ import org.springframework.validation.Validator; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.filter.reactive.HiddenHttpMethodFilter; +import org.springframework.web.method.ControllerAdviceBean; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.config.BlockingExecutionConfigurer; @@ -671,6 +673,24 @@ class WebFluxAutoConfigurationTests { .hasSingleBean(CustomExceptionHandler.class)); } + @Test + void problemDetailsIsOrderedBetweenLowestAndHighestOrderedControllerHandlers() { + this.contextRunner.withPropertyValues("spring.webflux.problemdetails.enabled:true") + .withUserConfiguration(OrderedControllerAdviceBeansConfiguration.class) + .run((context) -> { + + List> controllerAdviceClasses = ControllerAdviceBean.findAnnotatedBeans(context) + .stream() + .map(ControllerAdviceBean::getBeanType) + .collect(Collectors.toList()); + + assertThat(controllerAdviceClasses).containsExactly( + OrderedControllerAdviceBeansConfiguration.HighestOrderedControllerAdvice.class, + ProblemDetailsExceptionHandler.class, + OrderedControllerAdviceBeansConfiguration.LowestOrderedControllerAdvice.class); + }); + } + @Test void asyncTaskExecutorWithApplicationTaskExecutor() { this.contextRunner.withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class)) @@ -1016,6 +1036,25 @@ class WebFluxAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + @Import({ OrderedControllerAdviceBeansConfiguration.LowestOrderedControllerAdvice.class, + OrderedControllerAdviceBeansConfiguration.HighestOrderedControllerAdvice.class }) + static class OrderedControllerAdviceBeansConfiguration { + + @ControllerAdvice + @Order + static class LowestOrderedControllerAdvice { + + } + + @ControllerAdvice + @Order(Ordered.HIGHEST_PRECEDENCE) + static class HighestOrderedControllerAdvice { + + } + + } + @Aspect static class ExceptionHandlerInterceptor { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java index c2ebbb6d158..5d89e0ec0a9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java @@ -32,6 +32,7 @@ import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import java.util.stream.Collectors; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -51,6 +52,8 @@ import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguratio import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfigurationTests.OrderedControllerAdviceBeansConfiguration.HighestOrderedControllerAdvice; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfigurationTests.OrderedControllerAdviceBeansConfiguration.LowestOrderedControllerAdvice; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; @@ -65,6 +68,8 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.core.convert.ConversionService; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; @@ -89,6 +94,7 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.FormContentFilter; import org.springframework.web.filter.HiddenHttpMethodFilter; import org.springframework.web.filter.RequestContextFilter; +import org.springframework.web.method.ControllerAdviceBean; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.FlashMap; import org.springframework.web.servlet.FlashMapManager; @@ -972,6 +978,22 @@ class WebMvcAutoConfigurationTests { .hasSingleBean(CustomExceptionHandler.class)); } + @Test + void problemDetailsIsOrderedBetweenLowestAndHighestOrderedControllerHandlers() { + this.contextRunner.withPropertyValues("spring.mvc.problemdetails.enabled:true") + .withUserConfiguration(OrderedControllerAdviceBeansConfiguration.class) + .run((context) -> { + + List> controllerAdviceClasses = ControllerAdviceBean.findAnnotatedBeans(context) + .stream() + .map(ControllerAdviceBean::getBeanType) + .collect(Collectors.toList()); + + assertThat(controllerAdviceClasses).containsExactly(HighestOrderedControllerAdvice.class, + ProblemDetailsExceptionHandler.class, LowestOrderedControllerAdvice.class); + }); + } + private void assertResourceHttpRequestHandler(AssertableWebApplicationContext context, Consumer handlerConsumer) { Map handlerMap = getHandlerMap(context.getBean("resourceHandlerMapping", HandlerMapping.class)); @@ -1496,6 +1518,24 @@ class WebMvcAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + @Import({ LowestOrderedControllerAdvice.class, HighestOrderedControllerAdvice.class }) + static class OrderedControllerAdviceBeansConfiguration { + + @ControllerAdvice + @Order + static class LowestOrderedControllerAdvice { + + } + + @ControllerAdvice + @Order(Ordered.HIGHEST_PRECEDENCE) + static class HighestOrderedControllerAdvice { + + } + + } + @ControllerAdvice static class CustomExceptionHandler extends ResponseEntityExceptionHandler {