Spring Boot: 3.5.4

The @FilterRegistrationBean annotation is ignored in the auto-configured MockMvc. Filters are registered, but with their default configuration. This seems to be caused by SpringBootMockMvcBuilderCustomizer using its own RegistrationBeanAdapter implementation that is not aware of the @FilterRegistrationBean annotation: https://github.com/spring-projects/spring-boot/blob/925f9bc6ba99f0eaffce1e357282d3672b88e2a5/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java#L327-L336

Compare this with the "real" FilterRegistrationBeanAdapter used during proper application start, which queries the BeanFactory for the annotation and uses it to configure the filter: https://github.com/spring-projects/spring-boot/blob/925f9bc6ba99f0eaffce1e357282d3672b88e2a5/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletContextInitializerBeans.java#L344-L355

Reproducer (extract and run mvnw test): demo2.zip

The reproducer registers two filter beans. The first one uses FilterRegistrationBean directly and maps the filter to /classic:

@Bean
FilterRegistrationBean<MyFilter> classicRegistrationBean() {
    var f = new FilterRegistrationBean<>(new MyFilter("Classic"));
    f.setUrlPatterns(List.of("/classic"));
    return f;
}

The second one uses the annotation variant and maps the filter to /annotation:

@Bean
@FilterRegistration(urlPatterns = "/annotation")
MyFilter annotationRegistrationBean() {
    return new MyFilter("Annotation");
}

The filter itself adds the string given in the constructor to the matched-filter response header. A test than issues requests to /annotation and /classic and verifies that exactly one of the filters matched by asserting that the response header contains only Classic or only Annotation:

package com.example.demo;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.assertj.MockMvcTester;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@WebMvcTest
class Demo2ApplicationTests {

    @Autowired
    private MockMvcTester mvc;

    @Test
    void annotation() {
        assertThat(mvc.get().uri("/annotation"))
                .headers()
                .containsEntry("matched-filter", List.of("Annotation")); // SUCCESS, because the non-annotation-based filter was configured correctly and, as expected, didn't apply here.
    }

    @Test
    void classic() {
        assertThat(mvc.get().uri("/classic"))
                .headers()
                .containsEntry("matched-filter", List.of("Classic")); // FAILURE, because the header _also_ contains "Annotation", even though we didn't expect the annotation-based filter to apply here.
    }

}