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 a0c622790818..cd4c324a25ea 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.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.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 @@ private static class FilterRegistrationBeans extends ServletContextInitializerBe @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 000000000000..f28f5ca4f1c8 --- /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 21e910d35b90..2303d26bc92e 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 @@ private void configureFromAnnotation(ServletRegistrationBean bean, Serv } /** - * {@link RegistrationBeanAdapter} for {@link Filter} beans. + * {@link RegistrationBeanAdapter} implementation for {@link Filter} beans. + *

+ * 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 {