Spring MVC 6.x路径匹配严格化深度解析

一、问题现象与核心变化

1.1 典型兼容性问题场景

// 场景1:尾随斜杠问题 - Spring MVC 6.x不再默认匹配
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @GetMapping("/{id}")  // ❌ Spring 6.x: /api/users/123 可访问,/api/users/123/ 返回404
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
    
    @GetMapping("/list")  // ❌ Spring 6.x: /api/users/list 可访问,/api/users/list/ 返回404
    public List<User> listUsers() {
        return userService.findAll();
    }
}

// 场景2:路径变量中的点号问题
@RestController
public class FileController {
    
    @GetMapping("/files/{filename}")  
    // ❌ Spring 6.x: /files/document.pdf 匹配失败
    // ✅ Spring 5.x: /files/document.pdf 匹配成功,filename = "document.pdf"
    // ❌ Spring 6.x: /files/document.pdf 被解析为 filename = "document.pdf"(可能被截断)
    public FileInfo getFile(@PathVariable String filename) {
        return fileService.getInfo(filename);
    }
}

// 场景3:Ant风格通配符变化
@RestController
public class WildcardController {
    
    @GetMapping("/resources/**")  
    // ❌ Spring 6.x: /resources/static/js/app.js 可能匹配失败
    // ✅ Spring 5.x: /resources/static/js/app.js 匹配成功
    public Resource getResource(HttpServletRequest request) {
        String path = (String) request.getAttribute(
            HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE
        );
        // Spring 6.x中path可能为空或不同
    }
}

1.2 错误信息示例

// 错误1:404 找不到资源
2024-01-20T10:30:00.000Z  WARN 12345 --- [nio-8080-exec-1] o.s.web.servlet.PageNotFound             : 
No mapping for GET /api/users/123/

// 错误2:路径变量解析失败
2024-01-20T10:30:00.000Z ERROR 12345 --- [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : 
Resolved [org.springframework.web.bind.MissingPathVariableException: 
Required URI template variable 'filename' for method parameter type String is not present]

// 错误3:通配符匹配异常
2024-01-20T10:30:00.000Z  WARN 12345 --- [nio-8080-exec-3] o.s.web.servlet.PageNotFound             : 
No mapping for GET /resources/static/js/app.js

1.3 Spring MVC 6.x路径匹配核心变化

特性Spring MVC 5.xSpring MVC 6.x影响
尾随斜杠匹配默认开启默认关闭API版本变更可能中断
路径变量点号处理默认截断点号后内容默认包含点号文件扩展名处理变化
Ant风格通配符宽松匹配严格匹配静态资源路径可能失效
路径解析策略AntPathMatcherPathPatternParser性能提升但行为变化
矩阵变量需要显式启用默认启用可能意外解析URL参数

二、诊断工具与方法

2.1 启用详细路径匹配日志

# application.yml
logging:
  level:
    org.springframework.web.servlet.mvc.method.annotation: DEBUG
    org.springframework.web.servlet.handler: DEBUG
    org.springframework.web.util: DEBUG
    org.springframework.web.servlet.HandlerMapping: TRACE
  pattern:
    console: "%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n"

spring:
  mvc:
    log-request-details: true  # 启用请求详情日志

2.2 路径匹配诊断工具

@Component
public class PathMatchingDiagnostic implements ApplicationRunner {
    
    private static final Logger log = LoggerFactory.getLogger(PathMatchingDiagnostic.class);
    
    @Autowired
    private RequestMappingHandlerMapping handlerMapping;
    
    @Autowired
    private ApplicationContext context;
    
    @Override
    public void run(ApplicationArguments args) {
        diagnosePathMatchingConfiguration();
        analyzeRegisteredMappings();
        testCommonPathPatterns();
        generateCompatibilityReport();
    }
    
    private void diagnosePathMatchingConfiguration() {
        log.info("=== Spring MVC路径匹配配置诊断 ===");
        
        // 检查PathMatchConfigurer配置
        Map<String, PathMatchConfigurer> configurers = 
            context.getBeansOfType(PathMatchConfigurer.class);
        
        configurers.forEach((name, configurer) -> {
            log.info("PathMatchConfigurer: {}", name);
            
            try {
                Field patternParserField = PathMatchConfigurer.class
                    .getDeclaredField("patternParser");
                patternParserField.setAccessible(true);
                Object patternParser = patternParserField.get(configurer);
                
                if (patternParser instanceof PathPatternParser) {
                    PathPatternParser parser = (PathPatternParser) patternParser;
                    log.info("  使用PathPatternParser");
                    log.info("  默认选项: {}", parser.getDefaultOptions());
                } else {
                    log.info("  使用AntPathMatcher");
                }
            } catch (Exception e) {
                log.warn("无法获取PathMatchConfigurer内部状态", e);
            }
        });
        
        // 检查WebMvcConfigurer配置
        Map<String, WebMvcConfigurer> configurers2 = 
            context.getBeansOfType(WebMvcConfigurer.class);
        
        configurers2.forEach((name, configurer) -> {
            log.info("WebMvcConfigurer: {}", name);
        });
    }
    
    private void analyzeRegisteredMappings() {
        log.info("=== 已注册的请求映射分析 ===");
        
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = 
            handlerMapping.getHandlerMethods();
        
        log.info("总映射数量: {}", handlerMethods.size());
        
        handlerMethods.forEach((info, method) -> {
            log.info("映射模式: {}", info.getPatternsCondition());
            log.info("  处理器: {}.{}", 
                method.getBeanType().getSimpleName(),
                method.getMethod().getName());
            
            // 检查可能的问题模式
            checkProblematicPatterns(info.getPatternsCondition());
        });
    }
    
    private void checkProblematicPatterns(PatternsRequestCondition patterns) {
        Set<String> patternSet = patterns.getPatterns();
        
        for (String pattern : patternSet) {
            // 检查尾随斜杠
            if (pattern.endsWith("/")) {
                log.warn("  ⚠️ 映射模式以斜杠结尾: {}", pattern);
                log.warn("    在Spring MVC 6.x中,/api/users/ 不会匹配 /api/users");
            }
            
            // 检查路径变量中的点号
            if (pattern.contains("{") && pattern.contains("}")) {
                int start = pattern.indexOf("{");
                int end = pattern.indexOf("}");
                if (start < end) {
                    String pathVar = pattern.substring(start + 1, end);
                    if (pathVar.contains(".") || pattern.contains(".")) {
                        log.warn("  ⚠️ 路径变量可能包含点号: {}", pattern);
                        log.warn("    在Spring MVC 6.x中,点号默认作为路径变量的一部分");
                    }
                }
            }
            
            // 检查Ant风格通配符
            if (pattern.contains("*") || pattern.contains("?")) {
                log.info("  ℹ️ Ant风格通配符模式: {}", pattern);
                log.info("    在Spring MVC 6.x中,通配符匹配更严格");
            }
        }
    }
    
    private void testCommonPathPatterns() {
        log.info("=== 常见路径模式测试 ===");
        
        // 测试用例
        Map<String, List<String>> testCases = Map.of(
            "/api/users/{id}", 
            List.of("/api/users/123", "/api/users/123/", "/api/users/123.json"),
            
            "/files/{filename:.+}", 
            List.of("/files/document.pdf", "/files/report.csv"),
            
            "/static/**", 
            List.of("/static/css/app.css", "/static/js/main.js", "/static/")
        );
        
        testCases.forEach((pattern, urls) -> {
            log.info("测试模式: {}", pattern);
            
            PatternPathContainer.PathPattern parsedPattern = 
                PathPatternParser.defaultInstance.parse(pattern);
            
            urls.forEach(url -> {
                PatternPathContainer.PathContainer pathContainer = 
                    PathPatternParser.defaultInstance.parse(url);
                
                boolean matches = parsedPattern.matches(pathContainer);
                log.info("  {} -> {}: {}", url, pattern, matches ? "✅ 匹配" : "❌ 不匹配");
            });
        });
    }
    
    private void generateCompatibilityReport() {
        log.info("=== Spring MVC 6.x路径匹配兼容性报告 ===");
        
        List<String> recommendations = new ArrayList<>();
        
        // 检查尾随斜杠问题
        recommendations.add("1. 尾随斜杠处理:");
        recommendations.add("   - 旧行为: /api/users 和 /api/users/ 都匹配");
        recommendations.add("   - 新行为: /api/users 匹配,/api/users/ 不匹配(除非明确配置)");
        recommendations.add("   - 建议: 使用setUseTrailingSlashMatch(true)或更新客户端");
        
        // 检查路径变量点号问题
        recommendations.add("2. 路径变量中的点号:");
        recommendations.add("   - 旧行为: /files/document.pdf 中 filename = 'document'");
        recommendations.add("   - 新行为: /files/document.pdf 中 filename = 'document.pdf'");
        recommendations.add("   - 建议: 使用正则表达式约束 {filename:.+}");
        
        // 检查通配符匹配
        recommendations.add("3. Ant风格通配符:");
        recommendations.add("   - 旧行为: /** 匹配所有路径,包括空路径");
        recommendations.add("   - 新行为: /** 匹配所有路径,但行为更严格");
        recommendations.add("   - 建议: 测试所有通配符模式");
        
        recommendations.forEach(log::info);
    }
}

2.3 请求映射跟踪拦截器

@Component
public class RequestMappingInterceptor implements HandlerInterceptor {
    
    private static final Logger log = LoggerFactory.getLogger(RequestMappingInterceptor.class);
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, 
                             Object handler) throws Exception {
        
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            
            String requestURI = request.getRequestURI();
            String method = request.getMethod();
            
            log.debug("请求映射匹配:");
            log.debug("  URI: {}", requestURI);
            log.debug("  方法: {}", method);
            log.debug("  处理器: {}.{}", 
                handlerMethod.getBeanType().getSimpleName(),
                handlerMethod.getMethod().getName());
            
            // 记录路径变量
            Map<String, String> pathVariables = (Map<String, String>) 
                request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
            
            if (pathVariables != null && !pathVariables.isEmpty()) {
                log.debug("  路径变量: {}", pathVariables);
            }
            
            // 记录矩阵变量
            Map<String, List<String>> matrixVariables = (Map<String, List<String>>)
                request.getAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE);
            
            if (matrixVariables != null && !matrixVariables.isEmpty()) {
                log.debug("  矩阵变量: {}", matrixVariables);
            }
        }
        
        return true;
    }
    
    @Configuration
    public static class InterceptorConfig implements WebMvcConfigurer {
        
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new RequestMappingInterceptor())
                    .addPathPatterns("/**")
                    .order(Ordered.HIGHEST_PRECEDENCE);
        }
    }
}

三、解决方案

3.1 解决方案1:配置兼容性适配

3.1.1 全局路径匹配配置
@Configuration
public class PathMatchingCompatibilityConfig implements WebMvcConfigurer {
    
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        log.info("配置路径匹配兼容性...");
        
        // 方案1:启用尾随斜杠匹配(恢复Spring 5.x行为)
        configurer.setUseTrailingSlashMatch(true);  // ✅ 关键配置
        
        // 方案2:配置路径解析选项
        PathPatternParser patternParser = new PathPatternParser();
        
        // 配置PathPattern选项
        patternParser.setMatchOptionalTrailingSeparator(true);  // 匹配可选尾随分隔符
        
        // 设置路径选项
        PathPatternParser defaultParser = PathPatternParser.defaultInstance;
        try {
            // 通过反射设置默认选项(如果需要)
            Field optionsField = PathPatternParser.class.getDeclaredField("defaultOptions");
            optionsField.setAccessible(true);
            PathPattern.Options options = (PathPattern.Options) optionsField.get(defaultParser);
            
            log.info("当前PathPattern选项: {}", options);
        } catch (Exception e) {
            log.warn("无法获取PathPattern选项", e);
        }
        
        configurer.setPatternParser(patternParser);
        
        // 方案3:设置路径匹配前缀(如果需要)
        // configurer.addPathPrefix("/api/v2", 
        //     HandlerTypePredicate.forAnnotation(RestController.class));
    }
    
    // 方案4:配置URL路径帮助器
    @Bean
    public UrlPathHelper urlPathHelper() {
        UrlPathHelper helper = new UrlPathHelper();
        
        // 保持与Spring 5.x兼容的行为
        helper.setAlwaysUseFullPath(true);
        helper.setUrlDecode(false);  // 不自动解码URL
        helper.setRemoveSemicolonContent(true);  // 移除分号内容
        
        // 对于路径变量中的点号,可以配置
        helper.setDefaultEncoding("UTF-8");
        
        return helper;
    }
    
    // 方案5:自定义RequestMappingHandlerMapping
    @Bean
    public RequestMappingHandlerMapping requestMappingHandlerMapping(
            @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
            @Qualifier("mvcConversionService") FormattingConversionService conversionService,
            @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {
        
        RequestMappingHandlerMapping handlerMapping = new RequestMappingHandlerMapping();
        
        // 配置顺序
        handlerMapping.setOrder(0);
        
        // 配置拦截器
        handlerMapping.setInterceptors(
            getInterceptors(conversionService, resourceUrlProvider)
        );
        
        // 配置内容协商
        handlerMapping.setContentNegotiationManager(contentNegotiationManager);
        
        // 配置路径匹配
        handlerMapping.setUseTrailingSlashMatch(true);  // ✅ 启用尾随斜杠匹配
        handlerMapping.setUseSuffixPatternMatch(false);  // 禁用后缀模式匹配
        
        // 配置路径变量中的点号处理
        handlerMapping.setUrlPathHelper(urlPathHelper());
        
        // 配置路径解析器
        PathPatternParser patternParser = new PathPatternParser();
        patternParser.setMatchOptionalTrailingSeparator(true);
        handlerMapping.setPatternParser(patternParser);
        
        return handlerMapping;
    }
}
3.1.2 条件化配置(根据版本)
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
public class ConditionalPathMatchingConfig {
    
    // 根据Spring版本选择配置策略
    @Bean
    @ConditionalOnSpringVersion(range = "[6.0.0,)")  // Spring 6.x+
    public PathMatchConfigurer spring6PathMatchConfigurer() {
        return new PathMatchConfigurer() {
            @Override
            public void configurePathMatch(PathMatchConfigurer configurer) {
                log.info("应用Spring 6.x路径匹配配置");
                
                // Spring 6.x特定配置
                configurer.setUseTrailingSlashMatch(true);
                
                PathPatternParser parser = new PathPatternParser();
                parser.setMatchOptionalTrailingSeparator(true);
                configurer.setPatternParser(parser);
                
                // 禁用后缀模式匹配
                configurer.setUseSuffixPatternMatch(false);
            }
        };
    }
    
    @Bean
    @ConditionalOnSpringVersion(range = "[5.0.0,6.0.0)")  // Spring 5.x
    public PathMatchConfigurer spring5PathMatchConfigurer() {
        return new PathMatchConfigurer() {
            @Override
            public void configurePathMatch(PathMatchConfigurer configurer) {
                log.info("应用Spring 5.x兼容配置");
                // Spring 5.x通常不需要特殊配置
            }
        };
    }
    
    // 自定义条件注解
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Conditional(OnSpringVersionCondition.class)
    public @interface ConditionalOnSpringVersion {
        String range();
    }
    
    static class OnSpringVersionCondition implements Condition {
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            Map<String, Object> attributes = metadata.getAnnotationAttributes(
                ConditionalOnSpringVersion.class.getName());
            
            if (attributes != null) {
                String range = (String) attributes.get("range");
                String springVersion = SpringVersion.getVersion();
                
                return isVersionInRange(springVersion, range);
            }
            
            return false;
        }
        
        private boolean isVersionInRange(String version, String range) {
            // 简化版本检查
            return true; // 实际实现需要解析版本范围
        }
    }
}

3.2 解决方案2:修复代码中的路径模式

3.2.1 修复尾随斜杠问题
// ❌ 问题代码
@RestController
@RequestMapping("/api")
public class ProblematicController {
    
    @GetMapping("/users/{id}")  // 只匹配 /api/users/123
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
}

// ✅ 解决方案1:明确注册两个路径
@RestController
@RequestMapping("/api")
public class FixedController1 {
    
    @GetMapping({"/users/{id}", "/users/{id}/"})  // 明确两个路径
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
    
    @GetMapping({"/users/list", "/users/list/"})
    public List<User> listUsers() {
        return userService.findAll();
    }
}

// ✅ 解决方案2:使用正则表达式
@RestController
@RequestMapping("/api")
public class FixedController2 {
    
    @GetMapping("/users/{id:.*}")  // 使用正则表达式匹配可能包含斜杠的情况
    public User getUser(@PathVariable String id) {
        // 手动处理可能的尾随斜杠
        String cleanId = id.endsWith("/") ? id.substring(0, id.length() - 1) : id;
        return userService.findById(Long.parseLong(cleanId));
    }
}

// ✅ 解决方案3:创建路径工具类
@Component
public class PathUtils {
    
    public static String normalizePath(String path) {
        if (path == null) return null;
        
        // 移除尾随斜杠,但保留根路径
        if (path.endsWith("/") && path.length() > 1) {
            return path.substring(0, path.length() - 1);
        }
        
        return path;
    }
    
    public static String ensureTrailingSlash(String path) {
        if (path == null) return "/";
        
        if (!path.endsWith("/")) {
            return path + "/";
        }
        
        return path;
    }
}

// 在拦截器中使用
@Component
public class PathNormalizationInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, 
                             Object handler) throws Exception {
        
        // 规范化请求路径
        String requestURI = request.getRequestURI();
        String normalizedURI = PathUtils.normalizePath(requestURI);
        
        if (!requestURI.equals(normalizedURI)) {
            log.debug("规范化路径: {} -> {}", requestURI, normalizedURI);
            
            // 可以使用请求包装器
            request = new PathNormalizedRequestWrapper(request, normalizedURI);
        }
        
        return true;
    }
    
    static class PathNormalizedRequestWrapper extends HttpServletRequestWrapper {
        private final String normalizedPath;
        
        public PathNormalizedRequestWrapper(HttpServletRequest request, String normalizedPath) {
            super(request);
            this.normalizedPath = normalizedPath;
        }
        
        @Override
        public String getRequestURI() {
            return normalizedPath;
        }
        
        @Override
        public StringBuffer getRequestURL() {
            StringBuffer url = new StringBuffer();
            String scheme = getScheme();
            int port = getServerPort();
            
            url.append(scheme).append("://").append(getServerName());
            
            if (("http".equals(scheme) && port != 80) || 
                ("https".equals(scheme) && port != 443)) {
                url.append(':').append(port);
            }
            
            url.append(normalizedPath);
            
            return url;
        }
    }
}
3.2.2 修复路径变量点号问题
// ❌ 问题代码
@RestController
public class FileController {
    
    @GetMapping("/files/{filename}")  
    // Spring 6.x: /files/document.pdf 匹配,但filename可能被截断
    public ResponseEntity<Resource> getFile(@PathVariable String filename) {
        // filename可能是"document"而不是"document.pdf"
        return fileService.getFile(filename);
    }
}

// ✅ 解决方案1:使用正则表达式明确包含点号
@RestController
public class FixedFileController1 {
    
    @GetMapping("/files/{filename:.+}")  // 正则表达式 .+ 匹配至少一个字符,包括点号
    public ResponseEntity<Resource> getFile(@PathVariable String filename) {
        // filename现在包含完整文件名,如"document.pdf"
        return fileService.getFile(filename);
    }
    
    @GetMapping("/download/{filepath:.*}")  // .* 匹配零个或多个字符
    public ResponseEntity<Resource> downloadFile(@PathVariable String filepath) {
        // 可以匹配多级路径,如"documents/reports/q1.pdf"
        return fileService.downloadFile(filepath);
    }
}

// ✅ 解决方案2:使用矩阵变量处理扩展名
@RestController
public class FixedFileController2 {
    
    @GetMapping("/files/{name}")  
    public ResponseEntity<Resource> getFile(
            @PathVariable String name,
            @MatrixVariable(required = false) String ext) {
        
        String filename = name;
        if (ext != null) {
            filename = name + "." + ext;
        }
        
        return fileService.getFile(filename);
    }
    
    // 访问方式: /files/document;ext=pdf
}

// ✅ 解决方案3:使用自定义参数解析器
@RestController
public class FixedFileController3 {
    
    @GetMapping("/files/{filename}")  
    public ResponseEntity<Resource> getFile(
            @PathVariable("filename") @FileExtension String filename) {
        // 使用自定义注解处理文件名
        return fileService.getFile(filename);
    }
}

// 自定义注解和解析器
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FileExtension {
}

@Component
public class FileExtensionArgumentResolver implements HandlerMethodArgumentResolver {
    
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(FileExtension.class);
    }
    
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        
        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        if (request != null) {
            // 获取路径变量
            Map<String, String> pathVariables = (Map<String, String>)
                request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
            
            if (pathVariables != null) {
                String filename = pathVariables.get("filename");
                if (filename != null) {
                    // 确保包含扩展名
                    String requestURI = request.getRequestURI();
                    String path = extractPathFromUri(requestURI, filename);
                    
                    return path; // 返回完整路径
                }
            }
        }
        
        throw new IllegalArgumentException("无法解析文件名");
    }
    
    private String extractPathFromUri(String uri, String filename) {
        // 从URI中提取完整路径
        int filenameIndex = uri.lastIndexOf(filename);
        if (filenameIndex != -1) {
            return uri.substring(filenameIndex);
        }
        return filename;
    }
}

// 注册参数解析器
@Configuration
public class ArgumentResolverConfig implements WebMvcConfigurer {
    
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new FileExtensionArgumentResolver());
    }
}
3.2.3 修复通配符匹配问题
// ❌ 问题代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // Spring 6.x中可能匹配失败
        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/static/");
        
        // 旧版本可以,新版本可能有问题
        registry.addResourceHandler("/docs/**")
                .addResourceLocations("file:/var/www/docs/");
    }
}

// ✅ 解决方案1:明确配置通配符
@Configuration
public class FixedWebConfig implements WebMvcConfigurer {
    
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 明确指定匹配模式
        registry.addResourceHandler(
            "/static/",          // 目录
            "/static/**"         // 目录下的所有内容
        ).addResourceLocations("classpath:/static/");
        
        // 对于文件路径,使用更明确的模式
        registry.addResourceHandler(
            "/docs/",
            "/docs/**",
            "/docs/*.html",      // 明确的HTML文件
            "/docs/*.pdf"        // 明确的PDF文件
        ).addResourceLocations("file:/var/www/docs/");
        
        // 对于API通配符,使用正则表达式
        registry.addResourceHandler("/api/{version}/**")
                .addResourceLocations("classpath:/api/");
    }
    
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        // 确保通配符匹配正常工作
        PathPatternParser parser = new PathPatternParser();
        configurer.setPatternParser(parser);
    }
}

// ✅ 解决方案2:自定义ResourceResolver
@Configuration
public class AdvancedResourceConfig implements WebMvcConfigurer {
    
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/assets/**")
                .addResourceLocations("classpath:/assets/")
                .resourceChain(true)
                .addResolver(new PathPatternResourceResolver());
    }
    
    static class PathPatternResourceResolver extends PathResourceResolver {
        
        @Override
        protected Resource getResource(String resourcePath, Resource location) throws IOException {
            // 自定义资源解析逻辑
            Resource resource = location.createRelative(resourcePath);
            
            if (resource.exists() && resource.isReadable()) {
                return resource;
            }
            
            // 尝试其他变体
            if (resourcePath.endsWith("/")) {
                // 如果是目录,尝试index.html
                Resource indexResource = location.createRelative(resourcePath + "index.html");
                if (indexResource.exists()) {
                    return indexResource;
                }
            }
            
            return null;
        }
    }
}

// ✅ 解决方案3:使用Controller处理通配符路径
@RestController
@RequestMapping("/api")
public class WildcardApiController {
    
    // 处理动态路径
    @GetMapping("/{service}/**")
    public ResponseEntity<?> handleWildcard(
            @PathVariable String service,
            HttpServletRequest request) {
        
        // 获取通配符部分
        String path = extractWildcardPath(request, service);
        
        // 根据service和path路由请求
        return routeRequest(service, path, request);
    }
    
    private String extractWildcardPath(HttpServletRequest request, String service) {
        String requestURI = request.getRequestURI();
        String basePath = "/api/" + service + "/";
        
        if (requestURI.startsWith(basePath)) {
            return requestURI.substring(basePath.length());
        }
        
        return "";
    }
    
    private ResponseEntity<?> routeRequest(String service, String path, 
                                           HttpServletRequest request) {
        // 路由逻辑
        switch (service) {
            case "users":
                return userService.handle(path, request);
            case "products":
                return productService.handle(path, request);
            default:
                return ResponseEntity.notFound().build();
        }
    }
}

3.3 解决方案3:迁移工具和自动化修复

3.3.1 路径模式迁移扫描器
@Component
public class PathPatternMigrationScanner implements BeanPostProcessor {
    
    private static final Logger log = LoggerFactory.getLogger(PathPatternMigrationScanner.class);
    
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) 
            throws BeansException {
        
        // 扫描Controller类
        if (bean.getClass().isAnnotationPresent(Controller.class) ||
            bean.getClass().isAnnotationPresent(RestController.class)) {
            
            scanControllerForPathPatterns(bean);
        }
        
        // 扫描WebMvcConfigurer
        if (bean instanceof WebMvcConfigurer) {
            scanWebMvcConfigurer((WebMvcConfigurer) bean);
        }
        
        return bean;
    }
    
    private void scanControllerForPathPatterns(Object controller) {
        Class<?> controllerClass = controller.getClass();
        log.info("扫描Controller: {}", controllerClass.getName());
        
        // 扫描类级别的@RequestMapping
        RequestMapping classMapping = controllerClass.getAnnotation(RequestMapping.class);
        if (classMapping != null) {
            analyzePathPatterns(classMapping.value(), "类级别", controllerClass);
        }
        
        // 扫描方法级别的映射注解
        for (Method method : controllerClass.getDeclaredMethods()) {
            scanMethodForMappings(method, controllerClass);
        }
    }
    
    private void scanMethodForMappings(Method method, Class<?> controllerClass) {
        // 检查所有可能的映射注解
        Set<Annotation> mappingAnnotations = new HashSet<>();
        
        if (method.isAnnotationPresent(RequestMapping.class)) {
            mappingAnnotations.add(method.getAnnotation(RequestMapping.class));
        }
        if (method.isAnnotationPresent(GetMapping.class)) {
            mappingAnnotations.add(method.getAnnotation(GetMapping.class));
        }
        if (method.isAnnotationPresent(PostMapping.class)) {
            mappingAnnotations.add(method.getAnnotation(PostMapping.class));
        }
        if (method.isAnnotationPresent(PutMapping.class)) {
            mappingAnnotations.add(method.getAnnotation(PutMapping.class));
        }
        if (method.isAnnotationPresent(DeleteMapping.class)) {
            mappingAnnotations.add(method.getAnnotation(DeleteMapping.class));
        }
        if (method.isAnnotationPresent(PatchMapping.class)) {
            mappingAnnotations.add(method.getAnnotation(PatchMapping.class));
        }
        
        for (Annotation annotation : mappingAnnotations) {
            analyzeMappingAnnotation(annotation, method, controllerClass);
        }
    }
    
    private void analyzeMappingAnnotation(Annotation annotation, Method method, 
                                          Class<?> controllerClass) {
        String[] paths = extractPathsFromAnnotation(annotation);
        
        if (paths.length > 0) {
            analyzePathPatterns(paths, 
                String.format("方法 %s.%s", controllerClass.getSimpleName(), method.getName()),
                controllerClass);
        }
    }
    
    private String[] extractPathsFromAnnotation(Annotation annotation) {
        try {
            Method valueMethod = annotation.annotationType().getMethod("value");
            Method pathMethod = annotation.annotationType().getMethod("path");
            
            String[] valuePaths = (String[]) valueMethod.invoke(annotation);
            String[] pathPaths = (String[]) pathMethod.invoke(annotation);
            
            // 合并两个属性
            Set<String> allPaths = new HashSet<>();
            if (valuePaths.length > 0) allPaths.addAll(Arrays.asList(valuePaths));
            if (pathPaths.length > 0) allPaths.addAll(Arrays.asList(pathPaths));
            
            return allPaths.toArray(new String[0]);
            
        } catch (Exception e) {
            return new String[0];
        }
    }
    
    private void analyzePathPatterns(String[] paths, String location, Class<?> controllerClass) {
        for (String path : paths) {
            if (path == null || path.isEmpty()) continue;
            
            List<String> issues = detectPathPatternIssues(path);
            
            if (!issues.isEmpty()) {
                log.warn("⚠️ 发现路径模式问题 [{}]: {}", location, path);
                issues.forEach(issue -> log.warn("   - {}", issue));
                
                // 生成修复建议
                List<String> fixes = generateFixes(path, controllerClass);
                fixes.forEach(fix -> log.info("   ✅ 建议: {}", fix));
            }
        }
    }
    
    private List<String> detectPathPatternIssues(String path) {
        List<String> issues = new ArrayList<>();
        
        // 1. 检查尾随斜杠问题
        if (path.endsWith("/") && path.length() > 1) {
            issues.add("尾随斜杠: Spring MVC 6.x默认不匹配尾随斜杠");
        }
        
        // 2. 检查路径变量中的点号
        Pattern pathVarPattern = Pattern.compile("\\{([^}]+)\\}");
        Matcher matcher = pathVarPattern.matcher(path);
        
        while (matcher.find()) {
            String pathVar = matcher.group(1);
            if (pathVar.contains(".") || path.contains(".")) {
                issues.add("路径变量可能包含点号: Spring MVC 6.x默认包含点号");
            }
        }
        
        // 3. 检查通配符模式
        if (path.contains("*") || path.contains("?")) {
            issues.add("Ant风格通配符: Spring MVC 6.x中匹配更严格");
        }
        
        // 4. 检查矩阵变量语法
        if (path.contains(";")) {
            issues.add("矩阵变量: Spring MVC 6.x默认启用矩阵变量");
        }
        
        return issues;
    }
    
    private List<String> generateFixes(String originalPath, Class<?> controllerClass) {
        List<String> fixes = new ArrayList<>();
        
        // 尾随斜杠修复
        if (originalPath.endsWith("/") && originalPath.length() > 1) {
            String withoutSlash = originalPath.substring(0, originalPath.length() - 1);
            fixes.add(String.format("使用双路径: {\"%s\", \"%s\"}", withoutSlash, originalPath));
            fixes.add(String.format("或使用正则表达式: %s{id:.*}", withoutSlash));
        }
        
        // 路径变量点号修复
        if (originalPath.contains("{") && originalPath.contains("}")) {
            String fixedPath = originalPath.replaceAll("\\{([^}]+)\\}", "{$1:.+}");
            if (!fixedPath.equals(originalPath)) {
                fixes.add(String.format("使用正则表达式: %s", fixedPath));
            }
        }
        
        // 通配符修复
        if (originalPath.contains("**")) {
            fixes.add("确保通配符模式在Spring MVC 6.x中正常工作");
            fixes.add("考虑使用PathPatternParser进行测试");
        }
        
        return fixes;
    }
    
    private void scanWebMvcConfigurer(WebMvcConfigurer configurer) {
        log.info("扫描WebMvcConfigurer: {}", configurer.getClass().getName());
        
        // 可以检查ResourceHandlerRegistry等配置
        // 实际实现需要更复杂的分析
    }
}
3.3.2 自动化迁移脚本
@Component
public class PathPatternMigrationTool {
    
    private static final Logger log = LoggerFactory.getLogger(PathPatternMigrationTool.class);
    
    // 生成迁移报告
    public void generateMigrationReport() {
        log.info("=== Spring MVC 6.x路径匹配迁移报告 ===");
        
        Map<String, List<String>> migrationRules = new LinkedHashMap<>();
        
        // 规则1:尾随斜杠
        migrationRules.put("尾随斜杠匹配", Arrays.asList(
            "问题: Spring MVC 6.x默认不匹配尾随斜杠",
            "示例: @GetMapping(\"/api/users/{id}\") 不匹配 /api/users/123/",
            "解决方案:",
            "  1. 配置 setUseTrailingSlashMatch(true)",
            "  2. 明确声明两个路径: {\"/api/users/{id}\", \"/api/users/{id}/\"}",
            "  3. 使用正则表达式: @GetMapping(\"/api/users/{id:.*}\")"
        ));
        
        // 规则2:路径变量点号
        migrationRules.put("路径变量中的点号", Arrays.asList(
            "问题: Spring MVC 6.x默认包含点号在路径变量中",
            "示例: @GetMapping(\"/files/{filename}\") 匹配 /files/document.pdf",
            "  Spring 5.x: filename = 'document'",
            "  Spring 6.x: filename = 'document.pdf'",
            "解决方案:",
            "  1. 使用正则表达式: @GetMapping(\"/files/{filename:.+}\")",
            "  2. 配置UrlPathHelper.setRemoveSemicolonContent(false)",
            "  3. 使用矩阵变量: /files/document;ext=pdf"
        ));
        
        // 规则3:通配符匹配
        migrationRules.put("Ant风格通配符", Arrays.asList(
            "问题: Spring MVC 6.x使用PathPatternParser,匹配更严格",
            "示例: @GetMapping(\"/api/**\") 可能匹配行为变化",
            "解决方案:",
            "  1. 测试所有通配符模式",
            "  2. 考虑使用正则表达式替代",
            "  3. 配置PathPatternParser选项"
        ));
        
        // 规则4:矩阵变量
        migrationRules.put("矩阵变量", Arrays.asList(
            "问题: Spring MVC 6.x默认启用矩阵变量",
            "示例: /api/users;version=2 可能被解析为矩阵变量",
            "解决方案:",
            "  1. 明确处理矩阵变量",
            "  2. 配置移除分号内容: UrlPathHelper.setRemoveSemicolonContent(true)",
            "  3. 更新URL设计避免冲突"
        ));
        
        migrationRules.forEach((title, rules) -> {
            log.info("\n{}:", title);
            rules.forEach(rule -> log.info("  {}", rule));
        });
        
        generateCodeExamples();
    }
    
    private void generateCodeExamples() {
        log.info("\n=== 代码示例 ===");
        
        log.info("\n1. 兼容性配置:");
        log.info("""
            @Configuration
            public class PathMatchingConfig implements WebMvcConfigurer {
                @Override
                public void configurePathMatch(PathMatchConfigurer configurer) {
                    // 启用尾随斜杠匹配(Spring 5.x行为)
                    configurer.setUseTrailingSlashMatch(true);
                    
                    // 配置PathPatternParser
                    PathPatternParser parser = new PathPatternParser();
                    parser.setMatchOptionalTrailingSeparator(true);
                    configurer.setPatternParser(parser);
                    
                    // 禁用后缀模式匹配
                    configurer.setUseSuffixPatternMatch(false);
                }
            }
            """);
        
        log.info("\n2. Controller修复示例:");
        log.info("""
            // 修复前
            @GetMapping("/files/{filename}")
            public File getFile(@PathVariable String filename) {
                // filename可能不包含扩展名
            }
            
            // 修复后
            @GetMapping("/files/{filename:.+}")
            public File getFile(@PathVariable String filename) {
                // filename包含完整文件名
            }
            
            // 或使用双路径
            @GetMapping({"/api/users/{id}", "/api/users/{id}/"})
            public User getUser(@PathVariable Long id) {
                return userService.findById(id);
            }
            """);
        
        log.info("\n3. 资源处理器配置:");
        log.info("""
            @Configuration
            public class ResourceConfig implements WebMvcConfigurer {
                @Override
                public void addResourceHandlers(ResourceHandlerRegistry registry) {
                    // 明确指定多个模式
                    registry.addResourceHandler(
                        "/static/",
                        "/static/**"
                    ).addResourceLocations("classpath:/static/");
                }
            }
            """);
    }
    
    // 自动化修复工具
    public void autoFixSourceFiles(File sourceDir) throws IOException {
        log.info("开始自动化修复源文件...");
        
        Files.walk(sourceDir.toPath())
            .filter(path -> path.toString().endsWith(".java"))
            .forEach(this::processJavaFile);
        
        log.info("自动化修复完成");
    }
    
    private void processJavaFile(Path javaFile) {
        try {
            String content = Files.readString(javaFile);
            String originalContent = content;
            
            // 修复1:添加尾随斜杠变体
            content = fixTrailingSlashPatterns(content);
            
            // 修复2:修复路径变量正则表达式
            content = fixPathVariablePatterns(content);
            
            // 修复3:更新通配符注释
            content = addWildcardWarnings(content);
            
            if (!content.equals(originalContent)) {
                Files.writeString(javaFile, content);
                log.info("修复文件: {}", javaFile);
            }
            
        } catch (IOException e) {
            log.error("处理文件失败: {}", javaFile, e);
        }
    }
    
    private String fixTrailingSlashPatterns(String content) {
        // 查找 @GetMapping, @PostMapping 等注解
        Pattern pattern = Pattern.compile(
            "@(?:Get|Post|Put|Delete|Patch|RequestMapping)\\([^)]*\"(/[^\"]*/)[^)]*\"[^)]*\\)"
        );
        
        return pattern.matcher(content).replaceAll(match -> {
            String originalPath = match.group(1);
            if (originalPath.endsWith("/") && originalPath.length() > 2) {
                String withoutSlash = originalPath.substring(0, originalPath.length() - 1);
                return match.group().replace(
                    "\"" + originalPath + "\"",
                    "{\"" + withoutSlash + "\", \"" + originalPath + "\"}"
                );
            }
            return match.group();
        });
    }
    
    private String fixPathVariablePatterns(String content) {
        // 查找包含点号的路径变量
        Pattern pattern = Pattern.compile(
            "@(?:Get|Post|Put|Delete|Patch|RequestMapping)\\([^)]*\"(/[^\"]*\\{[^}]*\\}[^\"]*)\"[^)]*\\)"
        );
        
        return pattern.matcher(content).replaceAll(match -> {
            String mapping = match.group(1);
            if (mapping.contains(".")) {
                // 添加正则表达式修饰符
                String fixed = mapping.replaceAll("\\{([^}]+)\\}", "{$1:.+}");
                return match.group().replace("\"" + mapping + "\"", "\"" + fixed + "\"");
            }
            return match.group();
        });
    }
    
    private String addWildcardWarnings(String content) {
        // 添加通配符警告注释
        if (content.contains("**")) {
            String warning = "\n    // 注意: Spring MVC 6.x中通配符匹配更严格,请测试此模式\n";
            
            // 在包含**的注解后添加注释
            return content.replaceAll(
                "(@(?:Get|Post|Put|Delete|Patch|RequestMapping)\\([^)]*\"[^\"]*\\*\\*[^\"]*\"[^)]*\\))",
                "$1" + warning
            );
        }
        return content;
    }
}

3.4 解决方案4:测试策略和验证

3.4.1 路径匹配测试套件
@SpringBootTest
@AutoConfigureMockMvc
class PathMatchingCompatibilityTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private WebApplicationContext context;
    
    @Test
    void testTrailingSlashCompatibility() throws Exception {
        log.info("测试尾随斜杠兼容性...");
        
        // 测试用例:有和没有尾随斜杠
        Map<String, Boolean> testCases = Map.of(
            "/api/users/123", true,      // 应该成功
            "/api/users/123/", true,     // 如果配置正确应该成功
            "/api/users/list", true,     // 应该成功
            "/api/users/list/", true     // 如果配置正确应该成功
        );
        
        testCases.forEach((path, shouldSucceed) -> {
            try {
                mockMvc.perform(get(path))
                    .andDo(result -> {
                        int status = result.getResponse().getStatus();
                        boolean success = status >= 200 && status < 300;
                        
                        if (shouldSucceed && !success) {
                            log.error("❌ 路径 {} 应该成功但失败,状态: {}", path, status);
                        } else if (!shouldSucceed && success) {
                            log.error("❌ 路径 {} 应该失败但成功,状态: {}", path, status);
                        } else {
                            log.info("✅ 路径 {}: {}", path, success ? "成功" : "失败(预期)");
                        }
                    });
            } catch (Exception e) {
                if (shouldSucceed) {
                    log.error("❌ 路径 {} 异常: {}", path, e.getMessage());
                }
            }
        });
    }
    
    @Test
    void testPathVariableWithDots() throws Exception {
        log.info("测试路径变量中的点号...");
        
        // 测试文件路径
        Map<String, String> testCases = Map.of(
            "/files/document.pdf", "document.pdf",
            "/files/report.csv", "report.csv",
            "/files/image.png", "image.png"
        );
        
        testCases.forEach((path, expectedFilename) -> {
            try {
                mockMvc.perform(get(path))
                    .andExpect(status().isOk())
                    .andDo(result -> {
                        String content = result.getResponse().getContentAsString();
                        log.info("✅ 路径 {} -> {}", path, content);
                        
                        // 验证返回的文件名
                        assertTrue(content.contains(expectedFilename),
                            "响应应包含文件名: " + expectedFilename);
                    });
            } catch (Exception e) {
                log.error("❌ 路径 {} 失败: {}", path, e.getMessage());
            }
        });
    }
    
    @Test
    void testWildcardPatterns() throws Exception {
        log.info("测试通配符模式...");
        
        Map<String, Boolean> testCases = Map.of(
            "/static/css/app.css", true,
            "/static/js/main.js", true,
            "/static/images/logo.png", true,
            "/static/", true,
            "/static", true  // 可能需要重定向
        );
        
        testCases.forEach((path, shouldSucceed) -> {
            try {
                mockMvc.perform(get(path))
                    .andDo(result -> {
                        int status = result.getResponse().getStatus();
                        boolean success = status >= 200 && status < 300 || status == 302;
                        
                        if (shouldSucceed && !success) {
                            log.error("❌ 通配符路径 {} 应该成功但失败,状态: {}", path, status);
                        } else {
                            log.info("✅ 通配符路径 {}: {}", path, success ? "成功" : "失败(可能预期)");
                        }
                    });
            } catch (Exception e) {
                log.error("❌ 通配符路径 {} 异常: {}", path, e.getMessage());
            }
        });
    }
    
    @Test
    void testMatrixVariables() throws Exception {
        log.info("测试矩阵变量...");
        
        // Spring MVC 6.x默认启用矩阵变量
        mockMvc.perform(get("/api/users;version=2"))
            .andDo(result -> {
                log.info("矩阵变量测试状态: {}", result.getResponse().getStatus());
                
                // 检查是否正确处理矩阵变量
                String content = result.getResponse().getContentAsString();
                if (content.contains("version=2")) {
                    log.info("✅ 矩阵变量被正确处理");
                } else {
                    log.warn("⚠️ 矩阵变量可能未被处理");
                }
            });
    }
    
    @Test
    void testAllRegisteredMappings() {
        log.info("测试所有已注册的映射...");
        
        RequestMappingHandlerMapping handlerMapping = context.getBean(
            RequestMappingHandlerMapping.class);
        
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = 
            handlerMapping.getHandlerMethods();
        
        log.info("发现 {} 个请求映射", handlerMethods.size());
        
        handlerMethods.forEach((info, method) -> {
            Set<String> patterns = info.getPatternsCondition().getPatterns();
            
            patterns.forEach(pattern -> {
                log.info("测试模式: {}", pattern);
                
                // 生成测试URL
                List<String> testUrls = generateTestUrls(pattern);
                
                testUrls.forEach(url -> {
                    try {
                        mockMvc.perform(get(url))
                            .andDo(result -> {
                                int status = result.getResponse().getStatus();
                                if (status == 404) {
                                    log.warn("⚠️ 模式 {} 的URL {} 返回404", pattern, url);
                                }
                            });
                    } catch (Exception e) {
                        log.warn("测试URL {} 异常: {}", url, e.getMessage());
                    }
                });
            });
        });
    }
    
    private List<String> generateTestUrls(String pattern) {
        List<String> urls = new ArrayList<>();
        
        // 基本URL
        urls.add(pattern);
        
        // 添加尾随斜杠变体
        if (!pattern.endsWith("/") && !pattern.contains("{")) {
            urls.add(pattern + "/");
        }
        
        // 为路径变量生成示例值
        if (pattern.contains("{")) {
            String example = pattern
                .replace("{id}", "123")
                .replace("{filename}", "test.pdf")
                .replace("{name}", "test")
                .replace("{.*}", "example");
            
            urls.add(example);
            
            // 带尾随斜杠的示例
            if (!example.endsWith("/")) {
                urls.add(example + "/");
            }
        }
        
        return urls;
    }
    
    @Configuration
    @RestController
    static class TestControllers {
        
        @GetMapping("/api/users/{id}")
        public String getUser(@PathVariable Long id) {
            return "User " + id;
        }
        
        @GetMapping({"/api/users/list", "/api/users/list/"})
        public String listUsers() {
            return "User list";
        }
        
        @GetMapping("/files/{filename:.+}")
        public String getFile(@PathVariable String filename) {
            return "File: " + filename;
        }
        
        @GetMapping("/api/users")
        public String getUsersWithMatrix(@MatrixVariable(required = false) String version) {
            return "Users v" + (version != null ? version : "1");
        }
    }
}
3.4.2 集成测试配置
@SpringBootTest
@AutoConfigureMockMvc
@Import(TestSecurityConfig.class)
@TestPropertySource(properties = {
    "spring.mvc.pathmatch.matching-strategy=ant_path_matcher",
    "spring.mvc.pathmatch.use-suffix-pattern=false",
    "spring.mvc.pathmatch.use-registered-suffix-pattern=false",
    "spring.mvc.pathmatch.use-trailing-slash-match=true"
})
class PathMatchingIntegrationTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @LocalServerPort
    private int port;
    
    @Test
    void testRealHttpRequests() throws Exception {
        log.info("测试真实HTTP请求...");
        
        // 使用TestRestTemplate进行端到端测试
        TestRestTemplate restTemplate = new TestRestTemplate();
        
        // 测试各种路径模式
        testPathPatterns(restTemplate);
        
        // 测试重定向
        testRedirects(restTemplate);
        
        // 测试静态资源
        testStaticResources(restTemplate);
    }
    
    private void testPathPatterns(TestRestTemplate restTemplate) {
        String baseUrl = "http://localhost:" + port;
        
        Map<String, HttpStatus> expectedStatus = Map.of(
            "/api/users/123", HttpStatus.OK,
            "/api/users/123/", HttpStatus.OK,  // 应该也成功
            "/api/nonexistent", HttpStatus.NOT_FOUND
        );
        
        expectedStatus.forEach((path, expected) -> {
            ResponseEntity<String> response = restTemplate.getForEntity(
                baseUrl + path, String.class);
            
            assertEquals(expected, response.getStatusCode(),
                "路径 " + path + " 的状态码不正确");
            
            log.info("✅ 路径 {}: {}", path, response.getStatusCode());
        });
    }
    
    private void testRedirects(TestRestTemplate restTemplate) {
        String baseUrl = "http://localhost:" + port;
        
        // 测试目录重定向(如 /static 重定向到 /static/)
        ResponseEntity<String> response = restTemplate.getForEntity(
            baseUrl + "/static", String.class);
        
        if (response.getStatusCode() == HttpStatus.MOVED_PERMANENTLY ||
            response.getStatusCode() == HttpStatus.FOUND) {
            log.info("✅ 目录重定向正常工作");
        } else {
            log.warn("⚠️ 目录重定向可能未按预期工作");
        }
    }
    
    private void testStaticResources(TestRestTemplate restTemplate) {
        String baseUrl = "http://localhost:" + port;
        
        // 假设存在这些静态资源
        String[] staticResources = {
            "/favicon.ico",
            "/robots.txt",
            "/static/css/style.css",
            "/static/js/app.js"
        };
        
        for (String resource : staticResources) {
            try {
                ResponseEntity<byte[]> response = restTemplate.getForEntity(
                    baseUrl + resource, byte[].class);
                
                if (response.getStatusCode() == HttpStatus.OK) {
                    log.info("✅ 静态资源 {} 可访问", resource);
                } else {
                    log.warn("⚠️ 静态资源 {} 状态码: {}", resource, response.getStatusCode());
                }
            } catch (Exception e) {
                log.warn("⚠️ 静态资源 {} 异常: {}", resource, e.getMessage());
            }
        }
    }
    
    @Test
    void testPathPatternParserVsAntPathMatcher() {
        log.info("比较PathPatternParser和AntPathMatcher...");
        
        // 创建两个匹配器
        PathPatternParser pathPatternParser = new PathPatternParser();
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        
        // 测试用例
        Map<String, String[]> testCases = Map.of(
            "/api/users/{id}", new String[]{"/api/users/123", "/api/users/123/"},
            "/files/{filename}", new String[]{"/files/test.pdf", "/files/test"},
            "/static/**", new String[]{"/static/", "/static/css/style.css"}
        );
        
        testCases.forEach((pattern, urls) -> {
            log.info("模式: {}", pattern);
            
            // 解析路径模式
            PathPattern pathPattern = pathPatternParser.parse(pattern);
            
            for (String url : urls) {
                // PathPatternParser匹配
                PathContainer pathContainer = PathPatternParser.defaultInstance.parse(url);
                boolean pathPatternMatch = pathPattern.matches(pathContainer);
                
                // AntPathMatcher匹配
                boolean antMatcherMatch = antPathMatcher.match(pattern, url);
                
                log.info("  URL: {}", url);
                log.info("    PathPatternParser: {}", pathPatternMatch ? "✅ 匹配" : "❌ 不匹配");
                log.info("    AntPathMatcher: {}", antMatcherMatch ? "✅ 匹配" : "❌ 不匹配");
                
                // 如果不一致,记录警告
                if (pathPatternMatch != antMatcherMatch) {
                    log.warn("    ⚠️ 匹配结果不一致!这可能导致迁移问题");
                }
            }
        });
    }
}

四、最佳实践总结

4.1 Spring MVC 6.x路径匹配迁移检查清单

# Spring MVC 6.x路径匹配迁移检查清单
path-matching-migration-checklist:
  
  configuration-changes:
    - item: "启用尾随斜杠匹配"
      config: "spring.mvc.pathmatch.use-trailing-slash-match=true"
      code: "configurer.setUseTrailingSlashMatch(true)"
      priority: "高"
    
    - item: "配置PathPatternParser"
      config: "使用PathPatternParser并设置选项"
      code: |
        PathPatternParser parser = new PathPatternParser();
        parser.setMatchOptionalTrailingSeparator(true);
        configurer.setPatternParser(parser);
      priority: "中"
    
    - item: "禁用后缀模式匹配"
      config: "spring.mvc.pathmatch.use-suffix-pattern=false"
      code: "configurer.setUseSuffixPatternMatch(false)"
      priority: "中"
  
  code-changes:
    - item: "修复路径变量中的点号"
      pattern: "@GetMapping(\"/files/{filename}\")"
      fix: "@GetMapping(\"/files/{filename:.+}\")"
      priority: "高"
    
    - item: "处理尾随斜杠"
      pattern: "@GetMapping(\"/api/users/{id}\")"
      fix: "@GetMapping({\"/api/users/{id}\", \"/api/users/{id}/\"})"
      priority: "高"
    
    - item: "验证通配符模式"
      pattern: "@GetMapping(\"/api/**\")"
      action: "测试所有通配符变体"
      priority: "中"
    
    - item: "处理矩阵变量"
      pattern: "URL中的分号"
      action: "明确处理或配置移除"
      priority: "低"
  
  testing:
    - item: "测试所有API端点"
      action: "使用有和没有尾随斜杠的URL测试"
      tools: "MockMvc, TestRestTemplate"
    
    - item: "测试文件路径"
      action: "测试包含点号的路径变量"
      examples: "/files/document.pdf, /images/photo.jpg"
    
    - item: "测试静态资源"
      action: "验证资源处理器配置"
      paths: "/static/**, /public/**"
    
    - item: "测试重定向"
      action: "验证目录重定向行为"
      examples: "/static 重定向到 /static/"
  
  monitoring:
    - item: "启用路径匹配日志"
      config: "logging.level.org.springframework.web.servlet.HandlerMapping=DEBUG"
    
    - item: "监控404错误"
      action: "记录并分析未知路径的404"
    
    - item: "用户代理检测"
      action: "记录用户使用的URL格式"

4.2 问题排查流程图

渲染错误: Mermaid 渲染失败: Parse error on line 16: ... L -->|是| M[使用正则表达式 {var:.+}] L -->| -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'DIAMOND_START'

4.3 配置模板

// Spring Boot 3.x Spring MVC 6.x路径匹配完整配置模板
@Configuration
@EnableWebMvc
public class SpringMvc6PathMatchingConfig implements WebMvcConfigurer {
    
    private static final Logger log = LoggerFactory.getLogger(
        SpringMvc6PathMatchingConfig.class);
    
    /**
     * 核心路径匹配配置
     * 解决Spring MVC 6.x路径匹配严格化问题
     */
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        log.info("配置Spring MVC 6.x路径匹配兼容性...");
        
        // 1. 启用尾随斜杠匹配(Spring 5.x兼容行为)
        configurer.setUseTrailingSlashMatch(true);
        
        // 2. 禁用后缀模式匹配
        configurer.setUseSuffixPatternMatch(false);
        
        // 3. 配置PathPatternParser
        PathPatternParser patternParser = new PathPatternParser();
        
        // 设置匹配可选尾随分隔符
        patternParser.setMatchOptionalTrailingSeparator(true);
        
        // 其他PathPattern选项(根据需求)
        // patternParser.setCaseSensitive(true);
        // patternParser.setPathOptions(PathPattern.Options...);
        
        configurer.setPatternParser(patternParser);
        
        // 4. 可选:设置路径前缀
        // configurer.addPathPrefix("/api/v1", 
        //     HandlerTypePredicate.forAnnotation(RestController.class));
    }
    
    /**
     * URL路径帮助器配置
     * 控制URL解析行为
     */
    @Bean
    public UrlPathHelper urlPathHelper() {
        UrlPathHelper helper = new UrlPathHelper();
        
        // 移除分号内容(影响矩阵变量)
        helper.setRemoveSemicolonContent(true);
        
        // 总是使用完整路径
        helper.setAlwaysUseFullPath(true);
        
        // URL解码配置
        helper.setUrlDecode(false);
        helper.setDefaultEncoding("UTF-8");
        
        return helper;
    }
    
    /**
     * 资源处理器配置
     * 确保静态资源路径匹配正确
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        log.info("配置资源处理器...");
        
        // 静态资源 - 明确指定多个模式
        registry.addResourceHandler(
                "/static/",
                "/static/**",
                "/static/*.css",
                "/static/*.js",
                "/static/*.png",
                "/static/*.jpg"
            )
            .addResourceLocations("classpath:/static/")
            .setCachePeriod(3600)
            .resourceChain(true)
            .addResolver(new FallbackResourceResolver());
        
        // WebJars资源
        registry.addResourceHandler("/webjars/**")
            .addResourceLocations("classpath:/META-INF/resources/webjars/")
            .setCachePeriod(3600);
        
        // 公共资源
        registry.addResourceHandler("/public/**")
            .addResourceLocations("file:./public/")
            .setCachePeriod(0); // 开发环境不缓存
    }
    
    /**
     * 拦截器配置
     * 用于路径规范化等处理
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 路径规范化拦截器
        registry.addInterceptor(new PathNormalizationInterceptor())
                .addPathPatterns("/**")
                .order(Ordered.HIGHEST_PRECEDENCE);
        
        // 请求日志拦截器
        registry.addInterceptor(new RequestLoggingInterceptor())
                .addPathPatterns("/api/**")
                .order(Ordered.HIGHEST_PRECEDENCE + 1);
    }
    
    /**
     * 自定义参数解析器
     * 处理路径变量中的特殊字符
     */
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new FileExtensionArgumentResolver());
        resolvers.add(new MatrixVariableCompatibilityResolver());
    }
    
    /**
     * 异常处理器
     * 处理路径匹配相关的异常
     */
    @Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        resolvers.add(new PathMatchingExceptionResolver());
    }
    
    /**
     * 视图控制器
     * 配置简单的路径映射
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 重定向旧路径到新路径
        registry.addRedirectViewController("/old/path", "/new/path");
        
        // 简单的视图映射
        registry.addViewController("/home").setViewName("home");
        registry.addViewController("/").setViewName("index");
        
        // 确保尾随斜杠处理
        registry.addViewController("/about/").setViewName("about");
    }
    
    // 辅助类实现
    static class FallbackResourceResolver extends PathResourceResolver {
        @Override
        protected Resource getResource(String resourcePath, Resource location) throws IOException {
            Resource resource = super.getResource(resourcePath, location);
            
            // 如果找不到,尝试其他变体
            if (resource == null) {
                // 尝试添加尾随斜杠
                if (!resourcePath.endsWith("/")) {
                    resource = super.getResource(resourcePath + "/", location);
                }
                
                // 尝试移除尾随斜杠
                if (resource == null && resourcePath.endsWith("/")) {
                    resource = super.getResource(
                        resourcePath.substring(0, resourcePath.length() - 1), 
                        location
                    );
                }
            }
            
            return resource;
        }
    }
    
    static class PathNormalizationInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, 
                                 Object handler) throws Exception {
            // 规范化请求路径
            String path = request.getRequestURI();
            
            // 移除多余的尾随斜杠(保留根路径)
            if (path.endsWith("/") && path.length() > 1) {
                String normalized = path.substring(0, path.length() - 1);
                
                // 记录重定向
                log.debug("规范化路径: {} -> {}", path, normalized);
                
                // 如果是GET请求,可以重定向
                if ("GET".equals(request.getMethod())) {
                    response.sendRedirect(normalized);
                    return false;
                }
            }
            
            return true;
        }
    }
    
    static class PathMatchingExceptionResolver implements HandlerExceptionResolver {
        @Override
        public ModelAndView resolveException(HttpServletRequest request, 
                                             HttpServletResponse response, 
                                             Object handler, Exception ex) {
            
            // 处理路径匹配相关的异常
            if (ex instanceof NoHandlerFoundException) {
                log.warn("未找到处理器: {}", request.getRequestURI());
                
                // 检查是否是尾随斜杠问题
                String path = request.getRequestURI();
                if (path.endsWith("/") && path.length() > 1) {
                    String suggestion = path.substring(0, path.length() - 1);
                    
                    // 返回友好的错误信息
                    return new ModelAndView("error/404", 
                        Map.of("path", path, "suggestion", suggestion));
                }
            }
            
            return null;
        }
    }
}

通过上述系统化的分析和解决方案,可以有效解决Spring Boot 3.x中Spring MVC 6.x路径匹配严格化的问题。关键是要理解新版本的行为变化,并采取适当的配置和代码调整策略。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐