From bd0f58d67d4c45ad941752a6adb463423e05b06a Mon Sep 17 00:00:00 2001
From: Dmytro Nosan
Date: Mon, 28 Jul 2025 21:28:43 +0300
Subject: [PATCH] Add support for the @FilterRegistration annotation with
@WebMvcTest
See gh-46605
Signed-off-by: Dmytro Nosan
---
.../SpringBootMockMvcBuilderCustomizer.java | 42 ++++++-
...letFilterRegistrationIntegrationTests.java | 116 ++++++++++++++++++
.../ServletContextInitializerBeans.java | 8 +-
3 files changed, 162 insertions(+), 4 deletions(-)
create mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/WebMvcTestServletFilterRegistrationIntegrationTests.java
diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java
index a0c62279081..cd4c324a25e 100644
--- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java
+++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java
@@ -20,10 +20,13 @@ import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
+import java.util.EnumSet;
import java.util.List;
import jakarta.servlet.Filter;
+import jakarta.servlet.annotation.WebInitParam;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -31,11 +34,13 @@ import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.web.servlet.AbstractFilterRegistrationBean;
import org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean;
+import org.springframework.boot.web.servlet.FilterRegistration;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.RegistrationBean;
import org.springframework.boot.web.servlet.ServletContextInitializerBeans;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.annotation.Order;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultHandler;
import org.springframework.test.web.servlet.result.PrintingResultHandler;
@@ -330,18 +335,49 @@ public class SpringBootMockMvcBuilderCustomizer implements MockMvcBuilderCustomi
@Override
protected void addAdaptableBeans(ListableBeanFactory beanFactory) {
- addAsRegistrationBean(beanFactory, Filter.class, new FilterRegistrationBeanAdapter());
+ addAsRegistrationBean(beanFactory, Filter.class, new FilterRegistrationBeanAdapter(beanFactory));
}
private static final class FilterRegistrationBeanAdapter implements RegistrationBeanAdapter {
+ private final ListableBeanFactory beanFactory;
+
+ private 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) {
+ // Supports @Order annotation on @Bean methods
+ Order orderAnnotation = this.beanFactory.findAnnotationOnBean(beanName, Order.class);
+ Assert.state(orderAnnotation != null, "'orderAnnotation' must not be null");
+ configureFromAnnotation(bean, registrationAnnotation, orderAnnotation);
+ }
return bean;
}
+ private void configureFromAnnotation(FilterRegistrationBean bean,
+ FilterRegistration registrationAnnotation, Order orderAnnotation) {
+ bean.setEnabled(registrationAnnotation.enabled());
+ bean.setOrder(orderAnnotation.value());
+ if (StringUtils.hasText(registrationAnnotation.name())) {
+ bean.setName(registrationAnnotation.name());
+ }
+ if (registrationAnnotation.dispatcherTypes().length > 0) {
+ bean.setDispatcherTypes(EnumSet.copyOf(Arrays.asList(registrationAnnotation.dispatcherTypes())));
+ }
+ for (WebInitParam param : registrationAnnotation.initParameters()) {
+ bean.addInitParameter(param.name(), param.value());
+ }
+ bean.setUrlPatterns(Arrays.asList(registrationAnnotation.urlPatterns()));
+ }
+
}
}
diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/WebMvcTestServletFilterRegistrationIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/WebMvcTestServletFilterRegistrationIntegrationTests.java
new file mode 100644
index 00000000000..f28f5ca4f1c
--- /dev/null
+++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/WebMvcTestServletFilterRegistrationIntegrationTests.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2012-present 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.test.autoconfigure.web.servlet.mockmvc;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebInitParam;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.security.SecurityProperties;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.web.servlet.FilterRegistration;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.annotation.Order;
+import org.springframework.test.web.servlet.assertj.MockMvcTester;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link FilterRegistration} and {@link FilterRegistrationBean} with
+ * {@link WebMvcTest @WebMvcTest}.
+ *
+ * @author Dmytro Nosan
+ */
+@WebMvcTest
+class WebMvcTestServletFilterRegistrationIntegrationTests {
+
+ @Autowired
+ private MockMvcTester mvc;
+
+ @Test
+ void annotation() {
+ assertThat(this.mvc.get().uri("/annotation")).headers()
+ .hasValue("name", "annotation")
+ .hasValue("param1", "value1")
+ .hasValue("param2", "value2")
+ .doesNotContainHeader("param3")
+ .doesNotContainHeader("param4");
+ }
+
+ @Test
+ void registration() {
+ assertThat(this.mvc.get().uri("/registration")).headers()
+ .hasValue("name", "registration")
+ .hasValue("param3", "value3")
+ .hasValue("param4", "value4")
+ .doesNotContainHeader("param1")
+ .doesNotContainHeader("param2");
+ }
+
+ @TestConfiguration(proxyBeanMethods = false)
+ static class FilterRegistrationConfiguration {
+
+ @Bean
+ @FilterRegistration(name = "annotation", urlPatterns = "/annotation",
+ initParameters = { @WebInitParam(name = "param1", value = "value1"),
+ @WebInitParam(name = "param2", value = "value2") })
+ @Order(SecurityProperties.DEFAULT_FILTER_ORDER - 1)
+ TestFilter testFilterAnnotationBean() {
+ return new TestFilter();
+ }
+
+ @Bean
+ FilterRegistrationBean testFilterRegistrationBean() {
+ FilterRegistrationBean registration = new FilterRegistrationBean<>(new TestFilter());
+ registration.setName("registration");
+ registration.addUrlPatterns("/registration");
+ registration.setInitParameters(Map.of("param3", "value3", "param4", "value4"));
+ registration.setOrder(SecurityProperties.DEFAULT_FILTER_ORDER - 1);
+ return registration;
+ }
+
+ }
+
+ private static final class TestFilter extends OncePerRequestFilter {
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
+ FilterChain filterChain) throws ServletException, IOException {
+ response.addHeader("name", getFilterName());
+ FilterConfig config = getFilterConfig();
+ if (config != null) {
+ Collections.list(config.getInitParameterNames())
+ .forEach((name) -> response.addHeader(name, config.getInitParameter(name)));
+ }
+ filterChain.doFilter(request, response);
+ }
+
+ }
+
+}
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 21e910d35b9..2303d26bc92 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
@@ -330,7 +330,13 @@ public class ServletContextInitializerBeans extends AbstractCollection
+ * NOTE: A similar implementation is used in
+ * {@code SpringBootMockMvcBuilderCustomizer} for registering
+ * {@code @FilterRegistration} beans with {@code @MockMvc}. If you modify this class,
+ * please also update {@code SpringBootMockMvcBuilderCustomizer} if needed.
+ *
*/
private static class FilterRegistrationBeanAdapter implements RegistrationBeanAdapter {