diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/ControllerAdvice.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/ControllerAdvice.java index 47ebcd6314873ca1079f3725632ebf047bb9800e..decfce3eb1af8691ca95b343dc5b122af7b0dce4 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/ControllerAdvice.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/ControllerAdvice.java @@ -42,10 +42,12 @@ import org.springframework.stereotype.Component; * Note, however, that {@code @ControllerAdvice} beans that implement * {@link org.springframework.core.PriorityOrdered PriorityOrdered} are not * given priority over {@code @ControllerAdvice} beans that implement {@code Ordered}. - * For handling exceptions, an {@code @ExceptionHandler} will be picked on the - * first advice with a matching exception handler method. For model attributes - * and {@code InitBinder} initialization, {@code @ModelAttribute} and - * {@code @InitBinder} methods will also follow {@code @ControllerAdvice} order. + * In addition, {@code Ordered} is not honored for scoped {@code @ControllerAdvice} + * beans — for example if such a bean has been configured as a request-scoped + * or session-scoped bean. For handling exceptions, an {@code @ExceptionHandler} + * will be picked on the first advice with a matching exception handler method. For + * model attributes and data binding initialization, {@code @ModelAttribute} and + * {@code @InitBinder} methods will follow {@code @ControllerAdvice} order. * *

Note: For {@code @ExceptionHandler} methods, a root exception match will be * preferred to just matching a cause of the current exception, among the handler diff --git a/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java b/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java index 90b59765d9ac8721bb90183750aa15ba80d87856..8ec98e0ee7b0ee6d63375f3015e546694f39fb39 100644 --- a/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java +++ b/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java @@ -19,6 +19,7 @@ package org.springframework.web.method; import java.util.ArrayList; import java.util.List; +import org.springframework.aop.scope.ScopedProxyUtils; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.context.ApplicationContext; @@ -124,19 +125,42 @@ public class ControllerAdviceBean implements Ordered { /** * Get the order value for the contained bean. *

As of Spring Framework 5.2, the order value is lazily retrieved using - * the following algorithm and cached. + * the following algorithm and cached. Note, however, that a + * {@link ControllerAdvice @ControllerAdvice} bean that is configured as a + * scoped bean — for example, as a request-scoped or session-scoped + * bean — will not be eagerly resolved. Consequently, {@link Ordered} is + * not honored for scoped {@code @ControllerAdvice} beans. *

* @see #resolveBean() */ @Override public int getOrder() { if (this.order == null) { - Object resolvedBean = resolveBean(); + Object resolvedBean = null; + if (this.beanOrName instanceof String) { + String beanName = (String) this.beanOrName; + String targetBeanName = ScopedProxyUtils.getTargetBeanName(beanName); + boolean isScopedProxy = this.beanFactory.containsBean(targetBeanName); + // Avoid eager @ControllerAdvice bean resolution for scoped proxies, + // since attempting to do so during context initialization would result + // in an exception due to the current absence of the scope. For example, + // an HTTP request or session scope is not active during initialization. + if (!isScopedProxy && !ScopedProxyUtils.isScopedTarget(beanName)) { + resolvedBean = resolveBean(); + } + } + else { + resolvedBean = resolveBean(); + } + if (resolvedBean instanceof Ordered) { this.order = ((Ordered) resolvedBean).getOrder(); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestScopedControllerAdviceIntegrationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestScopedControllerAdviceIntegrationTests.java new file mode 100644 index 0000000000000000000000000000000000000000..14b812987afa5d62289db26a7381eb15378dadbb --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestScopedControllerAdviceIntegrationTests.java @@ -0,0 +1,91 @@ +/* + * 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 + * + * 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 java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.mock.web.test.MockServletContext; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.context.annotation.RequestScope; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.method.ControllerAdviceBean; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * Integration tests for request-scoped {@link ControllerAdvice @ControllerAdvice} beans. + * + * @author Sam Brannen + * @since 5.2.2 + */ +class RequestScopedControllerAdviceIntegrationTests { + + @Test // gh-23985 + @SuppressWarnings({ "rawtypes", "unchecked" }) + void loadContextWithRequestScopedControllerAdvice() { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.setServletContext(new MockServletContext()); + context.register(Config.class); + + assertThatCode(context::refresh).doesNotThrowAnyException(); + + // Until gh-24017 is fixed, we expect the RequestScopedControllerAdvice to show up twice. + Class[] expectedTypes = { RequestScopedControllerAdvice.class, RequestScopedControllerAdvice.class }; + + List adviceBeans = ControllerAdviceBean.findAnnotatedBeans(context); + assertThat(adviceBeans)// + .extracting(ControllerAdviceBean::getBeanType)// + .containsExactly(expectedTypes); + + assertThat(adviceBeans)// + .extracting(ControllerAdviceBean::getOrder)// + .containsExactly(42, 42); + + context.close(); + } + + + @Configuration + @EnableWebMvc + static class Config { + + @Bean + @RequestScope + RequestScopedControllerAdvice requestScopedControllerAdvice() { + return new RequestScopedControllerAdvice(); + } + } + + @ControllerAdvice + @Order(42) + static class RequestScopedControllerAdvice implements Ordered { + + @Override + public int getOrder() { + return 99; + } + } + +}