【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 的一部分。你完全可以根据具体情况来选择合适的方案。
相关文章:
【SpringBoot】当 @PathVariable 遇到 /,如何处理
1. 问题复现 在解析一个 URL 时,我们经常会使用 PathVariable 这个注解。例如我们会经常见到如下风格的代码: RestController Slf4j public class HelloWorldController {RequestMapping(path "/hi1/{name}", method RequestMethod.GET)publ…...
【FlutterDart】页面切换 PageView PageController(9 /100)
上效果: 有些不能理解官方例子里的动画为什么没有效果,有可能是我写法不对 后续如果有动画效果修复了,再更新这篇,没有动画效果,总觉得感受的丝滑效果差了很多 上代码: import package:flutter/material.…...
Backend - C# 的日志 NLog日志
目录 一、注入依赖和使用 logger 二、配置记录文件 1.安装插件 NLog 2.创建 nlog.config 配置文件 3. Programs配置日志信息 4. 设置 appsettings.json 的 LogLevel 5. 日志设定文件和日志级别的优先级 (1)常见的日志级别优先级 (2&…...
Flask是什么?深入解析 Flask 的设计与应用实践
文章目录 一、引言:从微框架到生态系统二、Flask 的核心设计理念三、Flask 的关键组件解析3.1 路由系统3.2 请求与响应对象3.3 模板引擎 Jinja23.4 扩展系统 四、Flask 的并发与性能优化4.1 默认的单线程模型4.2 提升并发性能的方法4.3 性能优化技巧 五、在企业级场…...
malloc函数和calloc函数的区别是什么?
malloc函数和calloc函数在动态内存管理中都起着分配内存空间的作用,但它们存在以下区别: 参数方面 - malloc函数:它只有一个参数,该参数表示要分配的字节数。例如, int *ptr (int *)malloc(10 * sizeof(int)); &#…...
Ansys Maxwell:3PH 变压器电感计算
各位变形金刚粉丝们,大家好: 在本博客中,我讨论了如何使用 Ansys Maxwell 计算三相变压器中的自感、互感和漏感。有多种方法和表达式可用于计算这些电感。 基本电感定义 电感的单位是亨利(H),其基本单位…...
【Go】Go文件操作详解
1. 前言 相信如果看过之前文章的朋友们一定知道我想讲什么了?灵魂三问:文件是什么?为什么需要文件?文件怎么操作?前面章节我们已经能够编写各种各样的功能代码了,但是一个很现实的问题就是我们没有任何 持…...
[react+ts] useRef获取自定义组件dom或方法声明
想用useRef获取自定义组件? 如果获取dom,直接写 const sonRef useRef<HTMLDivElement>(null); 然后子组件用forwardRef包一层,注意是HTMLDivElement,别写错, 写HTMLElement不行 const Son forwardRef<HTMLDivElement, IProps>((props, ref) > {}) 切记这…...
AI 将在今年获得“永久记忆”,2028美国会耗尽能源储备
AI的“永久记忆”时代即将来临 谷歌前CEO施密特揭示了AI技术的前景,他相信即将在2025年迎来一场伟大的变化。AI将实现“永久记忆”,改变我们与科技的互动过程。施密特将现有的AI上下文窗口比作人类的短期记忆,难以持久保存信息。他的设想是…...
【视频笔记】基于PyTorch从零构建多模态(视觉)大模型 by Umar Jamil【持续更新】
视频链接: 基于PyTorch从零构建多模态(视觉)大模型 by Umar Jamil 从头编写一个视觉语言模型:PloyGamma,是谷歌的一个模型 1:原始图像 2:视觉编码器(本文是viT),通过对比学习进行训练。这个对比学习最开始是CLIP,后来被谷歌改成了SigLIP 3:线性投影层 4:如何将图…...
解决 C++ 中头文件相互引用和解耦问题
在 C 中,当多个 .h 文件相互引用时,可能会导致 循环依赖 或 头文件冗余 问题,进而引发编译时间延迟、代码复杂度增加等问题。为了有效地解耦和组织代码,可以采用以下几种策略和思想: 1. 前向声明(Forward …...
河马剧场(短剧)APP的邀请码怎么填写
上篇给大家说到河马剧场免费看短剧还能领5.2元3天vip会员,本文就说一下河马剧场河马短剧APP的邀请码怎么填写。 河马短剧APP填写邀请码分三步: 1、安装登陆河马短剧APP 2、点击底部导航栏中间的“福利” 3、往下划会看到“填写邀请码领3天vip” 4、…...
01:C语言的本质
C语言的本质 1、ARM架构与汇编2、局部变量初始化与空间分配2.1、局部变量的初始化2.1、局部变量数组初始化 3、全局变量/静态变量初始化化与空间分配4、堆空间 1、ARM架构与汇编 ARM简要架构如下:CPU,ARM(能读能写),Flash(能读&a…...
第1章:数据库基础
第1章:数据库基础 1.1 数据库概述 1.1.1 什么是数据库 数据库的定义数据库的发展历程数据库的重要性 1.1.2 关系型数据库简介 关系型数据库模型常见的关系型数据库关系型数据库的特点 1.1.3 MySQL在企业中的应用 Web应用电商平台金融系统大数据存储 1.2 数据…...
C++教程 | string类的定义和初始化方法
在C中,string是标准库中用于处理字符串的类,定义在 头文件中,它提供了方便、灵活的字符串操作功能。以下是一些常见的定义和初始化string对象的方法: 1. 默认初始化 可以直接定义一个空的string对象,语法如下&#x…...
React中的合成事件
合成事件与原生事件 区别: 1. 命名不一样,原生用纯小写方式,react用小驼峰的方式 原生:onclick React的:onClick 2. 事件处理函数的写法不一样 原生的是传入一个字符串,react写法传入一个回调函数 3.…...
[SMARTFORMS] 创建FORM
输入事务码SMARTFORMS进入表单开发界面,选中表单,自定义表单名称ZFS_DEMO_2025 点击"创建"按钮,跳转至"SAP表格设计器"页面 在"表格属性"填写表单描述、指定页格式和样式 在"表格接口"可以填写SMART…...
成都和力九垠科技有限公司九垠赢系统Common存在任意文件上传漏洞
免责声明: 本文旨在提供有关特定漏洞的深入信息,帮助用户充分了解潜在的安全风险。发布此信息的目的在于提升网络安全意识和推动技术进步,未经授权访问系统、网络或应用程序,可能会导致法律责任或严重后果。因此,作者不对读者基于本文内容所采取的任何行为承担责任。读者在…...
基于Python的考研学习系统
作者:计算机学姐 开发技术:SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等,“文末源码”。 专栏推荐:前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏:…...
『SQLite』几种向表中插入数据的方法
向表中插入数据 INSERT INTO 语句用来给数据库中的某个表中新增数据行。 案例 直接根据基本语法插入数据插入时不用全部指定列名方式根据查询结果将数据插入另一张表中 注意 上述内容详讲见文章:SQLite的INSERT操作(内含案例)...
Python爬虫实战:研究feedparser库相关技术
1. 引言 1.1 研究背景与意义 在当今信息爆炸的时代,互联网上存在着海量的信息资源。RSS(Really Simple Syndication)作为一种标准化的信息聚合技术,被广泛用于网站内容的发布和订阅。通过 RSS,用户可以方便地获取网站更新的内容,而无需频繁访问各个网站。 然而,互联网…...
江苏艾立泰跨国资源接力:废料变黄金的绿色供应链革命
在华东塑料包装行业面临限塑令深度调整的背景下,江苏艾立泰以一场跨国资源接力的创新实践,重新定义了绿色供应链的边界。 跨国回收网络:废料变黄金的全球棋局 艾立泰在欧洲、东南亚建立再生塑料回收点,将海外废弃包装箱通过标准…...
Cinnamon修改面板小工具图标
Cinnamon开始菜单-CSDN博客 设置模块都是做好的,比GNOME简单得多! 在 applet.js 里增加 const Settings imports.ui.settings;this.settings new Settings.AppletSettings(this, HTYMenusonichy, instance_id); this.settings.bind(menu-icon, menu…...
leetcodeSQL解题:3564. 季节性销售分析
leetcodeSQL解题:3564. 季节性销售分析 题目: 表:sales ---------------------- | Column Name | Type | ---------------------- | sale_id | int | | product_id | int | | sale_date | date | | quantity | int | | price | decimal | -…...
pikachu靶场通关笔记22-1 SQL注入05-1-insert注入(报错法)
目录 一、SQL注入 二、insert注入 三、报错型注入 四、updatexml函数 五、源码审计 六、insert渗透实战 1、渗透准备 2、获取数据库名database 3、获取表名table 4、获取列名column 5、获取字段 本系列为通过《pikachu靶场通关笔记》的SQL注入关卡(共10关࿰…...
【HarmonyOS 5 开发速记】如何获取用户信息(头像/昵称/手机号)
1.获取 authorizationCode: 2.利用 authorizationCode 获取 accessToken:文档中心 3.获取手机:文档中心 4.获取昵称头像:文档中心 首先创建 request 若要获取手机号,scope必填 phone,permissions 必填 …...
dify打造数据可视化图表
一、概述 在日常工作和学习中,我们经常需要和数据打交道。无论是分析报告、项目展示,还是简单的数据洞察,一个清晰直观的图表,往往能胜过千言万语。 一款能让数据可视化变得超级简单的 MCP Server,由蚂蚁集团 AntV 团队…...
视频行为标注工具BehaviLabel(源码+使用介绍+Windows.Exe版本)
前言: 最近在做行为检测相关的模型,用的是时空图卷积网络(STGCN),但原有kinetic-400数据集数据质量较低,需要进行细粒度的标注,同时粗略搜了下已有开源工具基本都集中于图像分割这块,…...
c++第七天 继承与派生2
这一篇文章主要内容是 派生类构造函数与析构函数 在派生类中重写基类成员 以及多继承 第一部分:派生类构造函数与析构函数 当创建一个派生类对象时,基类成员是如何初始化的? 1.当派生类对象创建的时候,基类成员的初始化顺序 …...
0x-3-Oracle 23 ai-sqlcl 25.1 集成安装-配置和优化
是不是受够了安装了oracle database之后sqlplus的简陋,无法删除无法上下翻页的苦恼。 可以安装readline和rlwrap插件的话,配置.bahs_profile后也能解决上下翻页这些,但是很多生产环境无法安装rpm包。 oracle提供了sqlcl免费许可,…...
