Spring Boot: 3.5.4

The @FilterRegistration 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 @FilterRegistration 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 then 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.
    }

}

Comment From: nosan

I've prepared the fix in my branch

Comment From: wilkinsona

Thanks, @nosan. That looks good to me. I wondered a little about introducing some public API for deriving a FilterRegistrationBean from a @FilterRegistration. I think copy-paste is probably better here but I'd like to double-check with the rest of the team.

Comment From: nosan

I was thinking of reusing ServletContextInitializerBeans$FilterRegistrationBeanAdapter, but I came to the conclusion that it’s not worth it. For example, ServletRegistrationBean is not going to be used for MockMvc, along with other attributes as well. So, I decided to duplicate the code here.

Comment From: wilkinsona

We discussed this today and are going to go with copy-paste. We think it's worth adding a comment to FilterRegistrationBeanAdapter that mentions the need to also update the MockMvc side of things.

@nosan, would you like to open a PR with your changes?

Comment From: nosan

PR has been created, @wilkinsona