当前位置: 首页 > news >正文

【SpringBoot】当 @PathVariable 遇到 /,如何处理

1. 问题复现

  • 在解析一个 URL 时,我们经常会使用 @PathVariable 这个注解。例如我们会经常见到如下风格的代码:

    @RestController
    @Slf4j
    public class HelloWorldController {@RequestMapping(path = "/hi1/{name}", method = RequestMethod.GET)public String hello1(@PathVariable("name") String name){return name;};  
    }
    
  • 当我们使用 http://localhost:8080/hi1/xiaoming 访问这个服务时,会返回"xiaoming",即 Spring 会把 name 设置为 URL 中对应的值。

  • 看起来顺风顺水,但是假设这个 name 中含有特殊字符 / 时(例如http://localhost:8080/hi1/xiao/ming ),会如何?如果我们不假思索,或许答案是"xiao/ming"?然而稍微敏锐点的程序员都会判定这个访问是会报错的,具体错误参考:
    在这里插入图片描述

  • 如图所示,当 name 中含有 /,这个接口不会为 name 获取任何值,而是直接报 Not Found 错误。当然这里的“找不到”并不是指 name 找不到,而是指服务于这个特殊请求的接口。

  • 实际上,这里还存在另外一种错误,即当 name 的字符串以 / 结尾时,/ 会被自动去掉。例如我们访问 http://localhost:8080/hi1/xiaoming/,Spring 并不会报错,而是返回 xiaoming。

  • 针对这两种类型的错误,应该如何理解并修正呢?

2. 案例解析

  • 实际上,这两种错误都是 URL 匹配执行方法的相关问题,所以我们有必要先了解下 URL 匹配执行方法的大致过程。参考 AbstractHandlerMethodMapping#lookupHandlerMethod:
    
    @Nullable
    protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {List<Match> matches = new ArrayList<>();//尝试按照 URL 进行精准匹配List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);if (directPathMatches != null) {//精确匹配上,存储匹配结果addMatchingMappings(directPathMatches, matches, request);}if (matches.isEmpty()) {//没有精确匹配上,尝试根据请求来匹配addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);}if (!matches.isEmpty()) {Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));matches.sort(comparator);Match bestMatch = matches.get(0);if (matches.size() > 1) {//处理多个匹配的情况}//省略其他非关键代码return bestMatch.handlerMethod;}else {//匹配不上,直接报错return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);}
    
  • 大体分为这样几个基本步骤。

1. 根据 path 进行精确匹配

  • 这个步骤执行的代码语句是this.mappingRegistry.getMappingsByUrl(lookupPath),实际上,它是查询 MappingRegistry#urlLookup,它的值可以用调试视图查看,如下图所示:
    在这里插入图片描述
  • 查询 urlLookup 是一个精确匹配 Path 的过程。很明显,http://localhost:8080/hi1/xiao/ming 的 lookupPath 是"/hi1/xiao/ming",并不能得到任何精确匹配。这里需要补充的是,"/hi1/{name}"这种定义本身也没有出现在 urlLookup 中。

2. 假设 path 没有精确匹配上,则执行模糊匹配

  • 在步骤 1 匹配失败时,会根据请求来尝试模糊匹配,待匹配的匹配方法可参考下图: 在这里插入图片描述
  • 显然,"/hi1/{name}"这个匹配方法已经出现在待匹配候选中了。具体匹配过程可以参考方法 RequestMappingInfo#getMatchingCondition
    public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);if (methods == null) {return null;}ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);if (params == null) {return null;}//省略其他匹配条件PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request);if (patterns == null) {return null;}//省略其他匹配条件return new RequestMappingInfo(this.name, patterns,methods, params, headers, consumes, produces, custom.getCondition());
    }
    
  • 现在我们知道匹配会查询所有的信息,例如 Header、Body 类型以及 URL 等。如果有一项不符合条件,则不匹配。
  • 在我们的案例中,当使用 http://localhost:8080/hi1/xiaoming 访问时,其中 patternsCondition 是可以匹配上的。实际的匹配方法执行是通过 AntPathMatcher#match 来执行,判断的相关参数可参考以下调试视图:
    在这里插入图片描述
  • 但是当我们使用 http://localhost:8080/hi1/xiao/ming 来访问时,AntPathMatcher 执行的结果是"/hi1/xiao/ming"匹配不上"/hi1/{name}"。

3. 根据匹配情况返回结果

  • 如果找到匹配的方法,则返回方法;如果没有,则返回 null。

  • 在本案例中,http://localhost:8080/hi1/xiao/ming 因为找不到匹配方法最终报 404 错误。追根溯源就是 AntPathMatcher 匹配不了"/hi1/xiao/ming"和"/hi1/{name}"。

  • 另外,我们再回头思考 http://localhost:8080/hi1/xiaoming/ 为什么没有报错而是直接去掉了 /。这里我直接贴出了负责执行 AntPathMatcher 匹配的 PatternsRequestCondition#getMatchingPattern 方法的部分关键代码:

    private String getMatchingPattern(String pattern, String lookupPath) {//省略其他非关键代码if (this.pathMatcher.match(pattern, lookupPath)) {return pattern;}//尝试加一个/来匹配if (this.useTrailingSlashMatch) {if (!pattern.endsWith("/") && this.pathMatcher.match(pattern + "/", lookupPath)) {return pattern + "/";}}return null;
    }
    
  • 在这段代码中,AntPathMatcher 匹配不了"/hi1/xiaoming/“和”/hi1/{name}",所以不会直接返回。进而,在 useTrailingSlashMatch 这个参数启用时(默认启用),会把 Pattern 结尾加上 / 再尝试匹配一次。如果能匹配上,在最终返回 Pattern 时就隐式自动加 /。

  • 很明显,我们的案例符合这种情况,等于说我们最终是用了"/hi1/{name}/“这个 Pattern,而不再是”/hi1/{name}"。所以自然 URL 解析 name 结果是去掉 / 的。

3. 问题修正

  • 针对这个案例,有了源码的剖析,我们可能会想到可以先用"**"匹配上路径,等进入方法后再尝试去解析,这样就可以万无一失吧。具体修改代码如下:

    @RequestMapping(path = "/hi1/**", method = RequestMethod.GET)
    public String hi1(HttpServletRequest request){String requestURI = request.getRequestURI();return requestURI.split("/hi1/")[1];
    };
    
  • 但是这种修改方法还是存在漏洞,假设我们路径的 name 中刚好又含有"/hi1/",则 split 后返回的值就并不是我们想要的。实际上,更合适的修订代码示例如下:

    private AntPathMatcher antPathMatcher = new AntPathMatcher();@RequestMapping(path = "/hi1/**", method = RequestMethod.GET)
    public String hi1(HttpServletRequest request){String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);//matchPattern 即为"/hi1/**"String matchPattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); return antPathMatcher.extractPathWithinPattern(matchPattern, path); 
    };
    
  • 经过修改,两个错误都得以解决了。当然也存在一些其他的方案,例如对传递的参数进行 URL 编码以避免出现 /,或者干脆直接把这个变量作为请求参数、Header 等,而不是作为 URL 的一部分。你完全可以根据具体情况来选择合适的方案。

http://www.lryc.cn/news/515926.html

相关文章:

  • 【FlutterDart】页面切换 PageView PageController(9 /100)
  • Backend - C# 的日志 NLog日志
  • Flask是什么?深入解析 Flask 的设计与应用实践
  • malloc函数和calloc函数的区别是什么?
  • Ansys Maxwell:3PH 变压器电感计算
  • 【Go】Go文件操作详解
  • [react+ts] useRef获取自定义组件dom或方法声明
  • AI 将在今年获得“永久记忆”,2028美国会耗尽能源储备
  • 【视频笔记】基于PyTorch从零构建多模态(视觉)大模型 by Umar Jamil【持续更新】
  • 解决 C++ 中头文件相互引用和解耦问题
  • 河马剧场(短剧)APP的邀请码怎么填写
  • 01:C语言的本质
  • 第1章:数据库基础
  • C++教程 | string类的定义和初始化方法
  • React中的合成事件
  • [SMARTFORMS] 创建FORM
  • 成都和力九垠科技有限公司九垠赢系统Common存在任意文件上传漏洞
  • 基于Python的考研学习系统
  • 『SQLite』几种向表中插入数据的方法
  • 什么是Kafka的重平衡机制?
  • pdf预览 报:Failed to load module script
  • AI 角色扮演法的深度剖析与实践
  • weblogic问题
  • Qt仿音乐播放器:客户端唯一化
  • ceph文件系统
  • 【数据结构-堆】力扣2530. 执行 K 次操作后的最大分数
  • Java jdk8新特性:Stream 流
  • 房产销售系统(源码+数据库+文档)
  • Vue 项目自动化部署:Coding + Jenkins + Nginx 实践分享
  • 从零开始开发纯血鸿蒙应用之实现起始页