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
This commit is contained in:
Moritz Halbritter
2025-04-01 13:50:00 +02:00
parent 740fe4b28b
commit c179fed3b4
9 changed files with 473 additions and 24 deletions

View File

@@ -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 {};
}

View File

@@ -39,6 +39,7 @@ import org.springframework.util.Assert;
* @see ServletContextInitializer
* @see ServletContext#addFilter(String, Filter)
* @see DelegatingFilterProxyRegistrationBean
* @see FilterRegistration
*/
public class FilterRegistrationBean<T extends Filter> extends AbstractFilterRegistrationBean<T> {

View File

@@ -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<ServletContextInitializer> {
@@ -150,8 +155,9 @@ public class ServletContextInitializerBeans extends AbstractCollection<ServletCo
@SuppressWarnings("unchecked")
protected void addAdaptableBeans(ListableBeanFactory beanFactory) {
MultipartConfigElement multipartConfig = getMultipartConfig(beanFactory);
addAsRegistrationBean(beanFactory, Servlet.class, new ServletRegistrationBeanAdapter(multipartConfig));
addAsRegistrationBean(beanFactory, Filter.class, new FilterRegistrationBeanAdapter());
addAsRegistrationBean(beanFactory, Servlet.class,
new ServletRegistrationBeanAdapter(multipartConfig, beanFactory));
addAsRegistrationBean(beanFactory, Filter.class, new FilterRegistrationBeanAdapter(beanFactory));
for (Class<?> listenerType : ServletListenerRegistrationBean.getSupportedTypes()) {
addAsRegistrationBean(beanFactory, EventListener.class, (Class<EventListener>) listenerType,
new ServletListenerRegistrationBeanAdapter());
@@ -178,8 +184,10 @@ public class ServletContextInitializerBeans extends AbstractCollection<ServletCo
if (this.seen.add(type, bean)) {
// One that we haven't already seen
RegistrationBean registration = adapter.createRegistrationBean(beanName, bean, entries.size());
int order = getOrder(bean);
registration.setOrder(order);
Integer order = findOrder(bean);
if (order != null) {
registration.setOrder(order);
}
this.initializers.add(type, registration);
if (logger.isTraceEnabled()) {
logger.trace("Created " + type.getSimpleName() + " initializer for bean '" + beanName + "'; order="
@@ -198,6 +206,15 @@ public class ServletContextInitializerBeans extends AbstractCollection<ServletCo
}.getOrder(value);
}
private Integer findOrder(Object value) {
return new AnnotationAwareOrderComparator() {
@Override
public Integer findOrder(Object obj) {
return super.findOrder(obj);
}
}.findOrder(value);
}
private <T> List<Entry<String, T>> getOrderedBeansOfType(ListableBeanFactory beanFactory, Class<T> type) {
return getOrderedBeansOfType(beanFactory, type, Seen.empty());
}
@@ -254,7 +271,7 @@ public class ServletContextInitializerBeans extends AbstractCollection<ServletCo
@FunctionalInterface
protected interface RegistrationBeanAdapter<T> {
RegistrationBean createRegistrationBean(String name, T source, int totalNumberOfSourceBeans);
RegistrationBean createRegistrationBean(String beanName, T source, int totalNumberOfSourceBeans);
}
@@ -265,36 +282,95 @@ public class ServletContextInitializerBeans extends AbstractCollection<ServletCo
private final MultipartConfigElement multipartConfig;
ServletRegistrationBeanAdapter(MultipartConfigElement multipartConfig) {
private final ListableBeanFactory beanFactory;
ServletRegistrationBeanAdapter(MultipartConfigElement multipartConfig, ListableBeanFactory beanFactory) {
this.multipartConfig = multipartConfig;
this.beanFactory = beanFactory;
}
@Override
public RegistrationBean createRegistrationBean(String name, Servlet source, int totalNumberOfSourceBeans) {
String url = (totalNumberOfSourceBeans != 1) ? "/" + name + "/" : "/";
if (name.equals(DISPATCHER_SERVLET_NAME)) {
public RegistrationBean createRegistrationBean(String beanName, Servlet source, int totalNumberOfSourceBeans) {
String url = (totalNumberOfSourceBeans != 1) ? "/" + beanName + "/" : "/";
if (beanName.equals(DISPATCHER_SERVLET_NAME)) {
url = "/"; // always map the main dispatcherServlet to "/"
}
ServletRegistrationBean<Servlet> 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<Servlet> 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<Filter> {
private static class FilterRegistrationBeanAdapter implements RegistrationBeanAdapter<Filter> {
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<Filter> 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<Filter> 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<ServletCo
implements RegistrationBeanAdapter<EventListener> {
@Override
public RegistrationBean createRegistrationBean(String name, EventListener source,
public RegistrationBean createRegistrationBean(String beanName, EventListener source,
int totalNumberOfSourceBeans) {
return new ServletListenerRegistrationBean<>(source);
}

View File

@@ -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;
}

View File

@@ -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<T extends Servlet> extends DynamicRegistrationBean<ServletRegistration.Dynamic> {

View File

@@ -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 <T extends RegistrationBean> void assertThatSingleRegistration(
ServletContextInitializerBeans initializerBeans, Class<T> clazz, ThrowingConsumer<T> 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,6 +332,22 @@ class ServletContextInitializerBeansTests {
}
static class OrderedTestServlet extends HttpServlet implements ServletContextInitializer, Ordered {
static final int ORDER = 3;
@Override
public void onStartup(ServletContext servletContext) {
}
@Override
public int getOrder() {
return ORDER;
}
}
static class TestFilter implements Filter, ServletContextInitializer {
@Override
@@ -189,17 +365,12 @@ class ServletContextInitializerBeansTests {
}
@Override
public void destroy() {
}
}
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) {
}