You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring测试因请求缺少末尾斜杠返回404失败问题

末尾斜杠兼容问题排查与解决(Spring 2.7.5)

问题背景

通过主包下的TrailingSlashRedirectingFilter实现请求末尾斜杠兼容,生产环境运行正常,但在UserControllerTest中:

  1. 已通过excludeFilters排除原过滤器并导入TestConfig重新注册,请求不带末尾斜杠仍返回404
  2. 改用实现WebMvcConfigurer的WebConfiguration设置setUseTrailingSlashMatch(true),问题依旧

代码片段

原过滤器实现

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TrailingSlashRedirectingFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String fullURL = ServletUriComponentsBuilder
                .fromRequest(request)
                .build()
                .toString();
        String uri = request.getRequestURI();
        if (fullURL.endsWith("/") && !uri.equals("/")) {
            fullURL = fullURL.substring(0, fullURL.length() - 1);
            response.setStatus(HttpStatus.MOVED_PERMANENTLY.value());
            response.setHeader(HttpHeaders.LOCATION, fullURL);
        } else if(uri.equals("")){
            response.setStatus(HttpStatus.MOVED_PERMANENTLY.value());
            response.setHeader(HttpHeaders.LOCATION, fullURL + "/");
        } else {
            filterChain.doFilter(request, response);
        }
    }
}

测试配置类

@Configuration
public class TestConfig {

    @Bean
    public FilterRegistrationBean<TrailingSlashRedirectingFilter> trailingSlashRedirectingFilter() {
        FilterRegistrationBean<TrailingSlashRedirectingFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new TrailingSlashRedirectingFilter());
        registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        registrationBean.addUrlPatterns("/api/*");
        return registrationBean;
    }
}

测试类

@WebMvcTest(controllers = UserController.class,
        excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = TrailingSlashRedirectingFilter.class))
@Import(TestConfig.class)
@ExtendWith(MockitoExtension.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class UserControllerTest extends BaseTestConfig {

    UUID userId;
    User userRequest;
    User userResponse;
    String jsonRequest;
    String jsonResponse;

    @MockBean
    UserService userService;

    @Test
    void testCreateUser() throws Exception {
        User user = userResponse;
        when(userService.createUser(any())).thenReturn(user);

        mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/ht/users").content(jsonRequest).contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().is2xxSuccessful())
                .andExpect(content().json(jsonResponse))
                .andDo(document("create-user", resource("Create a user")))
                .andReturn();
    }
}

路径匹配配置类

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
      configurer.setUseTrailingSlashMatch(true);
    }
}

问题分析

  1. 过滤器逻辑缺陷:原过滤器仅处理带末尾斜杠的请求(重定向到无斜杠地址)和空URI场景,未覆盖无斜杠请求匹配带斜杠控制器映射的情况,完全依赖Spring MVC路径匹配兜底。
  2. 测试配置加载问题:@WebMvcTest默认禁用全量组件扫描,若未显式导入WebConfiguration,setUseTrailingSlashMatch(true)配置不会生效。
  3. 过滤器注册冲突:excludeFilters仅排除通过@Component注册的过滤器,但TestConfig中通过FilterRegistrationBean重新注册的过滤器,可能因测试环境Filter链顺序、URL匹配规则未正确生效。

解决方案

方案一:修复过滤器逻辑,实现双向兼容

修改过滤器,同时处理无斜杠请求的适配(改写请求URI而非重定向,避免POST请求丢失方法):

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TrailingSlashRedirectingFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String uri = request.getRequestURI();
        
        // 带斜杠的非根路径:重定向到无斜杠地址
        if (uri.endsWith("/") && !uri.equals("/")) {
            String newUri = uri.substring(0, uri.length() - 1);
            response.setStatus(HttpStatus.MOVED_PERMANENTLY.value());
            response.setHeader(HttpHeaders.LOCATION, buildFullUrl(request, newUri));
        } 
        // 无斜杠的非根路径:改写请求URI为带斜杠,适配控制器映射
        else if (!uri.equals("/") && !uri.isEmpty()) {
            HttpServletRequest wrappedRequest = new HttpServletRequestWrapper(request) {
                @Override
                public String getRequestURI() {
                    return uri + "/";
                }
            };
            filterChain.doFilter(wrappedRequest, response);
        } else {
            filterChain.doFilter(request, response);
        }
    }

    private String buildFullUrl(HttpServletRequest request, String newUri) {
        return ServletUriComponentsBuilder.fromRequest(request)
                .replacePath(newUri)
                .build()
                .toString();
    }
}

方案二:确保测试环境加载路径匹配配置

在测试类中显式导入WebConfiguration,让斜杠匹配规则生效:

@WebMvcTest(controllers = UserController.class,
        excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = TrailingSlashRedirectingFilter.class))
@Import({TestConfig.class, WebConfiguration.class}) // 新增导入WebConfiguration
@ExtendWith(MockitoExtension.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class UserControllerTest extends BaseTestConfig {
    // 原有测试代码不变
}

方案三:简化测试配置,直接启用路径匹配

若无需在测试中验证过滤器逻辑,可直接在测试配置中设置路径匹配规则:

@Configuration
public class TestWebConfig implements WebMvcConfigurer {
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.setUseTrailingSlashMatch(true);
    }
}

然后在测试类中导入该配置并排除原过滤器:

@WebMvcTest(controllers = UserController.class,
        excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = TrailingSlashRedirectingFilter.class))
@Import(TestWebConfig.class)
@ExtendWith(MockitoExtension.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class UserControllerTest extends BaseTestConfig {
    // 原有测试代码不变
}

关键注意事项

  • Spring 2.7.x中setUseTrailingSlashMatch(true)有效,但该属性在Spring Boot 3.x已被移除,后续升级需改用过滤器或路径改写方案。
  • @WebMvcTest环境下,过滤器注册优先级、URL匹配规则与生产环境可能存在差异,需确保测试配置与生产逻辑一致。
  • 若使用重定向处理斜杠,POST请求会被转为GET请求,可能引发业务异常,优先选择改写请求URI的方式。

内容的提问来源于stack exchange,提问作者Francislainy Campos

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 01:52:56