From c179fed3b4e618188a130abfe80c9d19e99ad150 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 1 Apr 2025 13:50:00 +0200 Subject: [PATCH] Add annotation for registering Servlets and Filters @ServletRegistration and @FilterRegistration can be used as an annotation-based alternative to ServletRegistrationBean and FilterRegistrationBean. Closes gh-16500 --- .../modules/how-to/pages/spring-mvc.adoc | 1 + .../modules/how-to/pages/webserver.adoc | 1 + .../modules/reference/pages/web/servlet.adoc | 3 + .../boot/web/servlet/FilterRegistration.java | 105 ++++++++++ .../web/servlet/FilterRegistrationBean.java | 1 + .../ServletContextInitializerBeans.java | 106 ++++++++-- .../boot/web/servlet/ServletRegistration.java | 90 +++++++++ .../web/servlet/ServletRegistrationBean.java | 1 + .../ServletContextInitializerBeansTests.java | 187 +++++++++++++++++- 9 files changed, 472 insertions(+), 23 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/FilterRegistration.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletRegistration.java diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc index 666d19448b7..d5278a15369 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc @@ -180,6 +180,7 @@ spring: ---- If you have additional servlets you can declare a javadoc:org.springframework.context.annotation.Bean[format=annotation] of type javadoc:jakarta.servlet.Servlet[] or javadoc:org.springframework.boot.web.servlet.ServletRegistrationBean[] for each and Spring Boot will register them transparently to the container. +It is also possible to use javadoc:org.springframework.boot.web.servlet.ServletRegistration[format=annotation] as an annotation-based alternative to javadoc:org.springframework.boot.web.servlet.ServletRegistrationBean[]. Because servlets are registered that way, they can be mapped to a sub-context of the javadoc:org.springframework.web.servlet.DispatcherServlet[] without invoking it. Configuring the javadoc:org.springframework.web.servlet.DispatcherServlet[] yourself is unusual but if you really need to do it, a javadoc:org.springframework.context.annotation.Bean[format=annotation] of type javadoc:org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath[] must be provided as well to provide the path of your custom javadoc:org.springframework.web.servlet.DispatcherServlet[]. diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc index fff0b690400..b51b7606a56 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc @@ -369,6 +369,7 @@ However, you must be very careful that they do not cause eager initialization of You can work around such restrictions by initializing the beans lazily when first used instead of on initialization. In the case of filters and servlets, you can also add mappings and init parameters by adding a javadoc:org.springframework.boot.web.servlet.FilterRegistrationBean[] or a javadoc:org.springframework.boot.web.servlet.ServletRegistrationBean[] instead of or in addition to the underlying component. +You can also use javadoc:org.springframework.boot.web.servlet.ServletRegistration[format=annotation] and javadoc:org.springframework.boot.web.servlet.FilterRegistration[format=annotation] as an annotation-based alternative to javadoc:org.springframework.boot.web.servlet.ServletRegistrationBean[] and javadoc:org.springframework.boot.web.servlet.FilterRegistrationBean[]. [NOTE] ==== diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc index b431a1519fb..ba3088f0917 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc @@ -529,11 +529,14 @@ In the case of multiple servlet beans, the bean name is used as a path prefix. Filters map to `+/*+`. If convention-based mapping is not flexible enough, you can use the javadoc:org.springframework.boot.web.servlet.ServletRegistrationBean[], javadoc:org.springframework.boot.web.servlet.FilterRegistrationBean[], and javadoc:org.springframework.boot.web.servlet.ServletListenerRegistrationBean[] classes for complete control. +If you prefer annotations over javadoc:org.springframework.boot.web.servlet.ServletRegistrationBean[] and javadoc:org.springframework.boot.web.servlet.FilterRegistrationBean[], you can also use javadoc:org.springframework.boot.web.servlet.ServletRegistration[format=annotation] and +javadoc:org.springframework.boot.web.servlet.FilterRegistration[format=annotation] as an alternative. It is usually safe to leave filter beans unordered. If a specific order is required, you should annotate the javadoc:jakarta.servlet.Filter[] with javadoc:org.springframework.core.annotation.Order[format=annotation] or make it implement javadoc:org.springframework.core.Ordered[]. You cannot configure the order of a javadoc:jakarta.servlet.Filter[] by annotating its bean method with javadoc:org.springframework.core.annotation.Order[format=annotation]. If you cannot change the javadoc:jakarta.servlet.Filter[] class to add javadoc:org.springframework.core.annotation.Order[format=annotation] or implement javadoc:org.springframework.core.Ordered[], you must define a javadoc:org.springframework.boot.web.servlet.FilterRegistrationBean[] for the javadoc:jakarta.servlet.Filter[] and set the registration bean's order using the `setOrder(int)` method. +Or, if you prefer annotations, you can also use javadoc:org.springframework.boot.web.servlet.FilterRegistration[format=annotation] and set the `order` attribute. Avoid configuring a filter that reads the request body at `Ordered.HIGHEST_PRECEDENCE`, since it might go against the character encoding configuration of your application. If a servlet filter wraps the request, it should be configured with an order that is less than or equal to `OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER`. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/FilterRegistration.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/FilterRegistration.java new file mode 100644 index 00000000000..df3f6764456 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/FilterRegistration.java @@ -0,0 +1,105 @@ +/* + * Copyright 2012-2025 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.boot.web.servlet; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.Filter; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AliasFor; +import org.springframework.core.annotation.Order; + +/** + * Registers a {@link Filter} in a Servlet 3.0+ container. Can be used as an + * annotation-based alternative to {@link FilterRegistrationBean}. + * + * @author Moritz Halbritter + * @since 3.5.0 + * @see FilterRegistrationBean + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Order +public @interface FilterRegistration { + + /** + * Whether this registration is enabled. + * @return whether this registration is enabled + */ + boolean enabled() default true; + + /** + * Order of the registration bean. + * @return the order of the registration bean + */ + @AliasFor(annotation = Order.class, attribute = "value") + int order() default Ordered.LOWEST_PRECEDENCE; + + /** + * Name of this registration. If not specified the bean name will be used. + * @return the name + */ + String name() default ""; + + /** + * Whether asynchronous operations are supported for this registration. + * @return whether asynchronous operations are supported + */ + boolean asyncSupported() default true; + + /** + * Dispatcher types that should be used with the registration. + * @return the dispatcher types + */ + DispatcherType[] dispatcherTypes() default {}; + + /** + * Whether registration failures should be ignored. If set to true, a failure will be + * logged. If set to false, an {@link IllegalStateException} will be thrown. + * @return whether registration failures should be ignored + */ + boolean ignoreRegistrationFailure() default false; + + /** + * Whether the filter mappings should be matched after any declared Filter mappings of + * the ServletContext. + * @return whether the filter mappings should be matched after any declared Filter + * mappings of the ServletContext + */ + boolean matchAfter() default false; + + /** + * Servlet names that the filter will be registered against. + * @return the servlet names + */ + String[] servletNames() default {}; + + /** + * URL patterns, as defined in the Servlet specification, that the filter will be + * registered against. + * @return the url patterns + */ + String[] urlPatterns() default {}; + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/FilterRegistrationBean.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/FilterRegistrationBean.java index 005b211fd18..977a7312fe4 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/FilterRegistrationBean.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/FilterRegistrationBean.java @@ -39,6 +39,7 @@ import org.springframework.util.Assert; * @see ServletContextInitializer * @see ServletContext#addFilter(String, Filter) * @see DelegatingFilterProxyRegistrationBean + * @see FilterRegistration */ public class FilterRegistrationBean extends AbstractFilterRegistrationBean { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletContextInitializerBeans.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletContextInitializerBeans.java index df092825df7..b0010729ada 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletContextInitializerBeans.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletContextInitializerBeans.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -20,6 +20,7 @@ import java.util.AbstractCollection; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.EnumSet; import java.util.EventListener; import java.util.HashMap; import java.util.HashSet; @@ -41,8 +42,11 @@ import org.springframework.aop.scope.ScopedProxyUtils; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.annotation.Order; +import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; /** * A collection {@link ServletContextInitializer}s obtained from a @@ -57,6 +61,7 @@ import org.springframework.util.MultiValueMap; * @author Dave Syer * @author Phillip Webb * @author Brian Clozel + * @author Moritz Halbritter * @since 1.4.0 */ public class ServletContextInitializerBeans extends AbstractCollection { @@ -150,8 +155,9 @@ public class ServletContextInitializerBeans extends AbstractCollection listenerType : ServletListenerRegistrationBean.getSupportedTypes()) { addAsRegistrationBean(beanFactory, EventListener.class, (Class) listenerType, new ServletListenerRegistrationBeanAdapter()); @@ -178,8 +184,10 @@ public class ServletContextInitializerBeans extends AbstractCollection bean = new ServletRegistrationBean<>(source, url); - bean.setName(name); + bean.setName(beanName); bean.setMultipartConfig(this.multipartConfig); + ServletRegistration registrationAnnotation = this.beanFactory.findAnnotationOnBean(beanName, + ServletRegistration.class); + if (registrationAnnotation != null) { + Order orderAnnotation = this.beanFactory.findAnnotationOnBean(beanName, Order.class); + Assert.notNull(orderAnnotation, "'orderAnnotation' must not be null"); + configureFromAnnotation(bean, registrationAnnotation, orderAnnotation); + } return bean; } + private void configureFromAnnotation(ServletRegistrationBean bean, ServletRegistration registration, + Order order) { + bean.setEnabled(registration.enabled()); + bean.setOrder(order.value()); + if (StringUtils.hasText(registration.name())) { + bean.setName(registration.name()); + } + bean.setAsyncSupported(registration.asyncSupported()); + bean.setIgnoreRegistrationFailure(registration.ignoreRegistrationFailure()); + bean.setLoadOnStartup(registration.loadOnStartup()); + if (registration.urlMappings().length > 0) { + bean.setUrlMappings(Arrays.asList(registration.urlMappings())); + } + } + } /** * {@link RegistrationBeanAdapter} for {@link Filter} beans. */ - private static final class FilterRegistrationBeanAdapter implements RegistrationBeanAdapter { + private static class FilterRegistrationBeanAdapter implements RegistrationBeanAdapter { + + private final ListableBeanFactory beanFactory; + + FilterRegistrationBeanAdapter(ListableBeanFactory beanFactory) { + this.beanFactory = beanFactory; + } @Override - public RegistrationBean createRegistrationBean(String name, Filter source, int totalNumberOfSourceBeans) { + public RegistrationBean createRegistrationBean(String beanName, Filter source, int totalNumberOfSourceBeans) { FilterRegistrationBean bean = new FilterRegistrationBean<>(source); - bean.setName(name); + bean.setName(beanName); + FilterRegistration registrationAnnotation = this.beanFactory.findAnnotationOnBean(beanName, + FilterRegistration.class); + if (registrationAnnotation != null) { + Order orderAnnotation = this.beanFactory.findAnnotationOnBean(beanName, Order.class); + Assert.notNull(orderAnnotation, "'orderAnnotation' must not be null"); + configureFromAnnotation(bean, registrationAnnotation, orderAnnotation); + } return bean; } + private void configureFromAnnotation(FilterRegistrationBean bean, FilterRegistration registration, + Order order) { + bean.setEnabled(registration.enabled()); + bean.setOrder(order.value()); + if (StringUtils.hasText(registration.name())) { + bean.setName(registration.name()); + } + bean.setAsyncSupported(registration.asyncSupported()); + if (registration.dispatcherTypes().length > 0) { + bean.setDispatcherTypes(EnumSet.copyOf(Arrays.asList(registration.dispatcherTypes()))); + } + bean.setIgnoreRegistrationFailure(registration.ignoreRegistrationFailure()); + bean.setMatchAfter(registration.matchAfter()); + if (registration.servletNames().length > 0) { + bean.setServletNames(Arrays.asList(registration.servletNames())); + } + if (registration.urlPatterns().length > 0) { + bean.setUrlPatterns(Arrays.asList(registration.urlPatterns())); + } + } + } /** @@ -304,7 +380,7 @@ public class ServletContextInitializerBeans extends AbstractCollection { @Override - public RegistrationBean createRegistrationBean(String name, EventListener source, + public RegistrationBean createRegistrationBean(String beanName, EventListener source, int totalNumberOfSourceBeans) { return new ServletListenerRegistrationBean<>(source); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletRegistration.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletRegistration.java new file mode 100644 index 00000000000..e515747295f --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletRegistration.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2025 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.boot.web.servlet; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.servlet.Servlet; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AliasFor; +import org.springframework.core.annotation.Order; + +/** + * Registers a {@link Servlet} in a Servlet 3.0+ container. Can be used as an + * annotation-based alternative to {@link ServletRegistrationBean}. + * + * @author Moritz Halbritter + * @since 3.5.0 + * @see ServletRegistrationBean + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Order +public @interface ServletRegistration { + + /** + * Whether this registration is enabled. + * @return whether this registration is enabled + */ + boolean enabled() default true; + + /** + * Order of the registration bean. + * @return the order of the registration bean + */ + @AliasFor(annotation = Order.class, attribute = "value") + int order() default Ordered.LOWEST_PRECEDENCE; + + /** + * Name of this registration. If not specified the bean name will be used. + * @return the name + */ + String name() default ""; + + /** + * Whether asynchronous operations are supported for this registration. + * @return whether asynchronous operations are supported + */ + boolean asyncSupported() default true; + + /** + * Whether registration failures should be ignored. If set to true, a failure will be + * logged. If set to false, an {@link IllegalStateException} will be thrown. + * @return whether registration failures should be ignored + */ + boolean ignoreRegistrationFailure() default false; + + /** + * URL mappings for the servlet. If not specified the mapping will default to '/'. + * @return the url mappings + */ + String[] urlMappings() default {}; + + /** + * The {@code loadOnStartup} priority. See + * {@link jakarta.servlet.ServletRegistration.Dynamic#setLoadOnStartup} for details. + * @return the {@code loadOnStartup} priority + */ + int loadOnStartup() default -1; + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletRegistrationBean.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletRegistrationBean.java index 5ef80a26c81..616d60ddfd7 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletRegistrationBean.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletRegistrationBean.java @@ -47,6 +47,7 @@ import org.springframework.util.StringUtils; * @since 1.4.0 * @see ServletContextInitializer * @see ServletContext#addServlet(String, Servlet) + * @see org.springframework.boot.web.servlet.ServletRegistration */ public class ServletRegistrationBean extends DynamicRegistrationBean { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/ServletContextInitializerBeansTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/ServletContextInitializerBeansTests.java index 62b0c1d59db..605ce7f0b87 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/ServletContextInitializerBeansTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/ServletContextInitializerBeansTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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,21 +16,24 @@ package org.springframework.boot.web.servlet; +import jakarta.servlet.DispatcherType; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletContext; -import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpSessionIdListener; +import org.assertj.core.api.ThrowingConsumer; import org.junit.jupiter.api.Test; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import static org.assertj.core.api.Assertions.assertThat; @@ -103,10 +106,91 @@ class ServletContextInitializerBeansTests { .isInstanceOf(TestServletAndFilterAndListener.class); } + @Test + @SuppressWarnings("unchecked") + void shouldApplyServletRegistrationAnnotation() { + load(ServletConfigurationWithAnnotation.class); + ServletContextInitializerBeans initializerBeans = new ServletContextInitializerBeans( + this.context.getBeanFactory(), TestServletContextInitializer.class); + assertThatSingleRegistration(initializerBeans, ServletRegistrationBean.class, (servletRegistrationBean) -> { + assertThat(servletRegistrationBean.isEnabled()).isFalse(); + assertThat(servletRegistrationBean.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE); + assertThat(servletRegistrationBean.getServletName()).isEqualTo("test"); + assertThat(servletRegistrationBean.isAsyncSupported()).isFalse(); + assertThat(servletRegistrationBean.getUrlMappings()).containsExactly("/test/*"); + }); + } + + @Test + @SuppressWarnings("unchecked") + void shouldApplyFilterRegistrationAnnotation() { + load(FilterConfigurationWithAnnotation.class); + ServletContextInitializerBeans initializerBeans = new ServletContextInitializerBeans( + this.context.getBeanFactory(), TestServletContextInitializer.class); + assertThatSingleRegistration(initializerBeans, FilterRegistrationBean.class, (filterRegistrationBean) -> { + assertThat(filterRegistrationBean.isEnabled()).isFalse(); + assertThat(filterRegistrationBean.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE); + assertThat(filterRegistrationBean.getFilterName()).isEqualTo("test"); + assertThat(filterRegistrationBean.isAsyncSupported()).isFalse(); + assertThat(filterRegistrationBean.isMatchAfter()).isTrue(); + assertThat(filterRegistrationBean.getServletNames()).containsExactly("test"); + assertThat(filterRegistrationBean.determineDispatcherTypes()).containsExactly(DispatcherType.ERROR); + assertThat(filterRegistrationBean.getUrlPatterns()).containsExactly("/test/*"); + }); + } + + @Test + void shouldApplyOrderFromBean() { + load(OrderedServletConfiguration.class); + ServletContextInitializerBeans initializerBeans = new ServletContextInitializerBeans( + this.context.getBeanFactory(), TestServletContextInitializer.class); + assertThatSingleRegistration(initializerBeans, ServletRegistrationBean.class, + (servletRegistrationBean) -> assertThat(servletRegistrationBean.getOrder()) + .isEqualTo(OrderedTestServlet.ORDER)); + } + + @Test + void shouldApplyOrderFromOrderAnnotationOnBeanMethod() { + load(ServletConfigurationWithAnnotationAndOrderAnnotation.class); + ServletContextInitializerBeans initializerBeans = new ServletContextInitializerBeans( + this.context.getBeanFactory(), TestServletContextInitializer.class); + assertThatSingleRegistration(initializerBeans, ServletRegistrationBean.class, + (servletRegistrationBean) -> assertThat(servletRegistrationBean.getOrder()) + .isEqualTo(ServletConfigurationWithAnnotationAndOrderAnnotation.ORDER)); + } + + @Test + void orderedInterfaceShouldTakePrecedenceOverOrderAnnotation() { + load(OrderedServletConfigurationWithAnnotationAndOrder.class); + ServletContextInitializerBeans initializerBeans = new ServletContextInitializerBeans( + this.context.getBeanFactory(), TestServletContextInitializer.class); + assertThatSingleRegistration(initializerBeans, ServletRegistrationBean.class, + (servletRegistrationBean) -> assertThat(servletRegistrationBean.getOrder()) + .isEqualTo(OrderedTestServlet.ORDER)); + } + + @Test + void shouldApplyOrderFromOrderAttribute() { + load(ServletConfigurationWithAnnotationAndOrder.class); + ServletContextInitializerBeans initializerBeans = new ServletContextInitializerBeans( + this.context.getBeanFactory(), TestServletContextInitializer.class); + assertThatSingleRegistration(initializerBeans, ServletRegistrationBean.class, + (servletRegistrationBean) -> assertThat(servletRegistrationBean.getOrder()) + .isEqualTo(ServletConfigurationWithAnnotationAndOrder.ORDER)); + } + private void load(Class... configuration) { this.context = new AnnotationConfigApplicationContext(configuration); } + private void assertThatSingleRegistration( + ServletContextInitializerBeans initializerBeans, Class clazz, ThrowingConsumer code) { + assertThat(initializerBeans).hasSize(1); + ServletContextInitializer initializer = initializerBeans.iterator().next(); + assertThat(initializer).isInstanceOf(clazz); + code.accept(clazz.cast(initializer)); + } + @Configuration(proxyBeanMethods = false) static class ServletConfiguration { @@ -117,6 +201,69 @@ class ServletContextInitializerBeansTests { } + @Configuration(proxyBeanMethods = false) + static class OrderedServletConfiguration { + + @Bean + OrderedTestServlet testServlet() { + return new OrderedTestServlet(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class ServletConfigurationWithAnnotation { + + @Bean + @ServletRegistration(enabled = false, name = "test", asyncSupported = false, urlMappings = "/test/*", + loadOnStartup = 1) + TestServlet testServlet() { + return new TestServlet(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class ServletConfigurationWithAnnotationAndOrderAnnotation { + + static final int ORDER = 7; + + @Bean + @ServletRegistration(name = "test") + @Order(ORDER) + TestServlet testServlet() { + return new TestServlet(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class ServletConfigurationWithAnnotationAndOrder { + + static final int ORDER = 9; + + @Bean + @ServletRegistration(name = "test", order = ORDER) + TestServlet testServlet() { + return new TestServlet(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class OrderedServletConfigurationWithAnnotationAndOrder { + + static final int ORDER = 5; + + @Bean + @ServletRegistration + @Order(ORDER) + OrderedTestServlet testServlet() { + return new OrderedTestServlet(); + } + + } + @Configuration(proxyBeanMethods = false) static class FilterConfiguration { @@ -127,6 +274,19 @@ class ServletContextInitializerBeansTests { } + @Configuration(proxyBeanMethods = false) + static class FilterConfigurationWithAnnotation { + + @Bean + @FilterRegistration(enabled = false, name = "test", asyncSupported = false, + dispatcherTypes = DispatcherType.ERROR, matchAfter = true, servletNames = "test", + urlPatterns = "/test/*") + TestFilter testFilter() { + return new TestFilter(); + } + + } + @Configuration(proxyBeanMethods = false) static class MultipleInterfacesConfiguration { @@ -172,7 +332,9 @@ class ServletContextInitializerBeansTests { } - static class TestFilter implements Filter, ServletContextInitializer { + static class OrderedTestServlet extends HttpServlet implements ServletContextInitializer, Ordered { + + static final int ORDER = 3; @Override public void onStartup(ServletContext servletContext) { @@ -180,17 +342,26 @@ class ServletContextInitializerBeansTests { } @Override - public void init(FilterConfig filterConfig) { + public int getOrder() { + return ORDER; + } + + } + + static class TestFilter implements Filter, ServletContextInitializer { + + @Override + public void onStartup(ServletContext servletContext) { } @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { + public void init(FilterConfig filterConfig) { } @Override - public void destroy() { + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { } @@ -199,7 +370,7 @@ class ServletContextInitializerBeansTests { static class TestServletContextInitializer implements ServletContextInitializer { @Override - public void onStartup(ServletContext servletContext) throws ServletException { + public void onStartup(ServletContext servletContext) { } @@ -208,7 +379,7 @@ class ServletContextInitializerBeansTests { static class OtherTestServletContextInitializer implements ServletContextInitializer { @Override - public void onStartup(ServletContext servletContext) throws ServletException { + public void onStartup(ServletContext servletContext) { }