Skip to content

Commit c179fed

Browse files
committed
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
1 parent 740fe4b commit c179fed

File tree

9 files changed

+472
-23
lines changed

9 files changed

+472
-23
lines changed

Diff for: spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc

+1
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ spring:
180180
----
181181

182182
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.
183+
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[].
183184
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.
184185

185186
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 for: spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc

+1
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ However, you must be very careful that they do not cause eager initialization of
369369
You can work around such restrictions by initializing the beans lazily when first used instead of on initialization.
370370

371371
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.
372+
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[].
372373

373374
[NOTE]
374375
====

Diff for: spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc

+3
Original file line numberDiff line numberDiff line change
@@ -529,11 +529,14 @@ In the case of multiple servlet beans, the bean name is used as a path prefix.
529529
Filters map to `+/*+`.
530530

531531
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.
532+
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
533+
javadoc:org.springframework.boot.web.servlet.FilterRegistration[format=annotation] as an alternative.
532534

533535
It is usually safe to leave filter beans unordered.
534536
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[].
535537
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].
536538
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.
539+
Or, if you prefer annotations, you can also use javadoc:org.springframework.boot.web.servlet.FilterRegistration[format=annotation] and set the `order` attribute.
537540
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.
538541
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`.
539542

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.web.servlet;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
import jakarta.servlet.DispatcherType;
26+
import jakarta.servlet.Filter;
27+
28+
import org.springframework.core.Ordered;
29+
import org.springframework.core.annotation.AliasFor;
30+
import org.springframework.core.annotation.Order;
31+
32+
/**
33+
* Registers a {@link Filter} in a Servlet 3.0+ container. Can be used as an
34+
* annotation-based alternative to {@link FilterRegistrationBean}.
35+
*
36+
* @author Moritz Halbritter
37+
* @since 3.5.0
38+
* @see FilterRegistrationBean
39+
*/
40+
@Target({ ElementType.METHOD, ElementType.TYPE })
41+
@Retention(RetentionPolicy.RUNTIME)
42+
@Documented
43+
@Order
44+
public @interface FilterRegistration {
45+
46+
/**
47+
* Whether this registration is enabled.
48+
* @return whether this registration is enabled
49+
*/
50+
boolean enabled() default true;
51+
52+
/**
53+
* Order of the registration bean.
54+
* @return the order of the registration bean
55+
*/
56+
@AliasFor(annotation = Order.class, attribute = "value")
57+
int order() default Ordered.LOWEST_PRECEDENCE;
58+
59+
/**
60+
* Name of this registration. If not specified the bean name will be used.
61+
* @return the name
62+
*/
63+
String name() default "";
64+
65+
/**
66+
* Whether asynchronous operations are supported for this registration.
67+
* @return whether asynchronous operations are supported
68+
*/
69+
boolean asyncSupported() default true;
70+
71+
/**
72+
* Dispatcher types that should be used with the registration.
73+
* @return the dispatcher types
74+
*/
75+
DispatcherType[] dispatcherTypes() default {};
76+
77+
/**
78+
* Whether registration failures should be ignored. If set to true, a failure will be
79+
* logged. If set to false, an {@link IllegalStateException} will be thrown.
80+
* @return whether registration failures should be ignored
81+
*/
82+
boolean ignoreRegistrationFailure() default false;
83+
84+
/**
85+
* Whether the filter mappings should be matched after any declared Filter mappings of
86+
* the ServletContext.
87+
* @return whether the filter mappings should be matched after any declared Filter
88+
* mappings of the ServletContext
89+
*/
90+
boolean matchAfter() default false;
91+
92+
/**
93+
* Servlet names that the filter will be registered against.
94+
* @return the servlet names
95+
*/
96+
String[] servletNames() default {};
97+
98+
/**
99+
* URL patterns, as defined in the Servlet specification, that the filter will be
100+
* registered against.
101+
* @return the url patterns
102+
*/
103+
String[] urlPatterns() default {};
104+
105+
}

Diff for: spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/FilterRegistrationBean.java

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
* @see ServletContextInitializer
4040
* @see ServletContext#addFilter(String, Filter)
4141
* @see DelegatingFilterProxyRegistrationBean
42+
* @see FilterRegistration
4243
*/
4344
public class FilterRegistrationBean<T extends Filter> extends AbstractFilterRegistrationBean<T> {
4445

Diff for: spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletContextInitializerBeans.java

+91-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@
2020
import java.util.ArrayList;
2121
import java.util.Arrays;
2222
import java.util.Collections;
23+
import java.util.EnumSet;
2324
import java.util.EventListener;
2425
import java.util.HashMap;
2526
import java.util.HashSet;
@@ -41,8 +42,11 @@
4142
import org.springframework.beans.factory.ListableBeanFactory;
4243
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
4344
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
45+
import org.springframework.core.annotation.Order;
46+
import org.springframework.util.Assert;
4447
import org.springframework.util.LinkedMultiValueMap;
4548
import org.springframework.util.MultiValueMap;
49+
import org.springframework.util.StringUtils;
4650

4751
/**
4852
* A collection {@link ServletContextInitializer}s obtained from a
@@ -57,6 +61,7 @@
5761
* @author Dave Syer
5862
* @author Phillip Webb
5963
* @author Brian Clozel
64+
* @author Moritz Halbritter
6065
* @since 1.4.0
6166
*/
6267
public class ServletContextInitializerBeans extends AbstractCollection<ServletContextInitializer> {
@@ -150,8 +155,9 @@ private String getResourceDescription(String beanName, ListableBeanFactory beanF
150155
@SuppressWarnings("unchecked")
151156
protected void addAdaptableBeans(ListableBeanFactory beanFactory) {
152157
MultipartConfigElement multipartConfig = getMultipartConfig(beanFactory);
153-
addAsRegistrationBean(beanFactory, Servlet.class, new ServletRegistrationBeanAdapter(multipartConfig));
154-
addAsRegistrationBean(beanFactory, Filter.class, new FilterRegistrationBeanAdapter());
158+
addAsRegistrationBean(beanFactory, Servlet.class,
159+
new ServletRegistrationBeanAdapter(multipartConfig, beanFactory));
160+
addAsRegistrationBean(beanFactory, Filter.class, new FilterRegistrationBeanAdapter(beanFactory));
155161
for (Class<?> listenerType : ServletListenerRegistrationBean.getSupportedTypes()) {
156162
addAsRegistrationBean(beanFactory, EventListener.class, (Class<EventListener>) listenerType,
157163
new ServletListenerRegistrationBeanAdapter());
@@ -178,8 +184,10 @@ private <T, B extends T> void addAsRegistrationBean(ListableBeanFactory beanFact
178184
if (this.seen.add(type, bean)) {
179185
// One that we haven't already seen
180186
RegistrationBean registration = adapter.createRegistrationBean(beanName, bean, entries.size());
181-
int order = getOrder(bean);
182-
registration.setOrder(order);
187+
Integer order = findOrder(bean);
188+
if (order != null) {
189+
registration.setOrder(order);
190+
}
183191
this.initializers.add(type, registration);
184192
if (logger.isTraceEnabled()) {
185193
logger.trace("Created " + type.getSimpleName() + " initializer for bean '" + beanName + "'; order="
@@ -198,6 +206,15 @@ public int getOrder(Object obj) {
198206
}.getOrder(value);
199207
}
200208

209+
private Integer findOrder(Object value) {
210+
return new AnnotationAwareOrderComparator() {
211+
@Override
212+
public Integer findOrder(Object obj) {
213+
return super.findOrder(obj);
214+
}
215+
}.findOrder(value);
216+
}
217+
201218
private <T> List<Entry<String, T>> getOrderedBeansOfType(ListableBeanFactory beanFactory, Class<T> type) {
202219
return getOrderedBeansOfType(beanFactory, type, Seen.empty());
203220
}
@@ -254,7 +271,7 @@ public int size() {
254271
@FunctionalInterface
255272
protected interface RegistrationBeanAdapter<T> {
256273

257-
RegistrationBean createRegistrationBean(String name, T source, int totalNumberOfSourceBeans);
274+
RegistrationBean createRegistrationBean(String beanName, T source, int totalNumberOfSourceBeans);
258275

259276
}
260277

@@ -265,36 +282,95 @@ private static class ServletRegistrationBeanAdapter implements RegistrationBeanA
265282

266283
private final MultipartConfigElement multipartConfig;
267284

268-
ServletRegistrationBeanAdapter(MultipartConfigElement multipartConfig) {
285+
private final ListableBeanFactory beanFactory;
286+
287+
ServletRegistrationBeanAdapter(MultipartConfigElement multipartConfig, ListableBeanFactory beanFactory) {
269288
this.multipartConfig = multipartConfig;
289+
this.beanFactory = beanFactory;
270290
}
271291

272292
@Override
273-
public RegistrationBean createRegistrationBean(String name, Servlet source, int totalNumberOfSourceBeans) {
274-
String url = (totalNumberOfSourceBeans != 1) ? "/" + name + "/" : "/";
275-
if (name.equals(DISPATCHER_SERVLET_NAME)) {
293+
public RegistrationBean createRegistrationBean(String beanName, Servlet source, int totalNumberOfSourceBeans) {
294+
String url = (totalNumberOfSourceBeans != 1) ? "/" + beanName + "/" : "/";
295+
if (beanName.equals(DISPATCHER_SERVLET_NAME)) {
276296
url = "/"; // always map the main dispatcherServlet to "/"
277297
}
278298
ServletRegistrationBean<Servlet> bean = new ServletRegistrationBean<>(source, url);
279-
bean.setName(name);
299+
bean.setName(beanName);
280300
bean.setMultipartConfig(this.multipartConfig);
301+
ServletRegistration registrationAnnotation = this.beanFactory.findAnnotationOnBean(beanName,
302+
ServletRegistration.class);
303+
if (registrationAnnotation != null) {
304+
Order orderAnnotation = this.beanFactory.findAnnotationOnBean(beanName, Order.class);
305+
Assert.notNull(orderAnnotation, "'orderAnnotation' must not be null");
306+
configureFromAnnotation(bean, registrationAnnotation, orderAnnotation);
307+
}
281308
return bean;
282309
}
283310

311+
private void configureFromAnnotation(ServletRegistrationBean<Servlet> bean, ServletRegistration registration,
312+
Order order) {
313+
bean.setEnabled(registration.enabled());
314+
bean.setOrder(order.value());
315+
if (StringUtils.hasText(registration.name())) {
316+
bean.setName(registration.name());
317+
}
318+
bean.setAsyncSupported(registration.asyncSupported());
319+
bean.setIgnoreRegistrationFailure(registration.ignoreRegistrationFailure());
320+
bean.setLoadOnStartup(registration.loadOnStartup());
321+
if (registration.urlMappings().length > 0) {
322+
bean.setUrlMappings(Arrays.asList(registration.urlMappings()));
323+
}
324+
}
325+
284326
}
285327

286328
/**
287329
* {@link RegistrationBeanAdapter} for {@link Filter} beans.
288330
*/
289-
private static final class FilterRegistrationBeanAdapter implements RegistrationBeanAdapter<Filter> {
331+
private static class FilterRegistrationBeanAdapter implements RegistrationBeanAdapter<Filter> {
332+
333+
private final ListableBeanFactory beanFactory;
334+
335+
FilterRegistrationBeanAdapter(ListableBeanFactory beanFactory) {
336+
this.beanFactory = beanFactory;
337+
}
290338

291339
@Override
292-
public RegistrationBean createRegistrationBean(String name, Filter source, int totalNumberOfSourceBeans) {
340+
public RegistrationBean createRegistrationBean(String beanName, Filter source, int totalNumberOfSourceBeans) {
293341
FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>(source);
294-
bean.setName(name);
342+
bean.setName(beanName);
343+
FilterRegistration registrationAnnotation = this.beanFactory.findAnnotationOnBean(beanName,
344+
FilterRegistration.class);
345+
if (registrationAnnotation != null) {
346+
Order orderAnnotation = this.beanFactory.findAnnotationOnBean(beanName, Order.class);
347+
Assert.notNull(orderAnnotation, "'orderAnnotation' must not be null");
348+
configureFromAnnotation(bean, registrationAnnotation, orderAnnotation);
349+
}
295350
return bean;
296351
}
297352

353+
private void configureFromAnnotation(FilterRegistrationBean<Filter> bean, FilterRegistration registration,
354+
Order order) {
355+
bean.setEnabled(registration.enabled());
356+
bean.setOrder(order.value());
357+
if (StringUtils.hasText(registration.name())) {
358+
bean.setName(registration.name());
359+
}
360+
bean.setAsyncSupported(registration.asyncSupported());
361+
if (registration.dispatcherTypes().length > 0) {
362+
bean.setDispatcherTypes(EnumSet.copyOf(Arrays.asList(registration.dispatcherTypes())));
363+
}
364+
bean.setIgnoreRegistrationFailure(registration.ignoreRegistrationFailure());
365+
bean.setMatchAfter(registration.matchAfter());
366+
if (registration.servletNames().length > 0) {
367+
bean.setServletNames(Arrays.asList(registration.servletNames()));
368+
}
369+
if (registration.urlPatterns().length > 0) {
370+
bean.setUrlPatterns(Arrays.asList(registration.urlPatterns()));
371+
}
372+
}
373+
298374
}
299375

300376
/**
@@ -304,7 +380,7 @@ private static final class ServletListenerRegistrationBeanAdapter
304380
implements RegistrationBeanAdapter<EventListener> {
305381

306382
@Override
307-
public RegistrationBean createRegistrationBean(String name, EventListener source,
383+
public RegistrationBean createRegistrationBean(String beanName, EventListener source,
308384
int totalNumberOfSourceBeans) {
309385
return new ServletListenerRegistrationBean<>(source);
310386
}

0 commit comments

Comments
 (0)