【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操作(内含案例)...
AGI实现路径全透视:3大主流技术路线(神经符号融合/具身智能/认知架构)的性能拐点与商业化时间表
第一章:AGI技术路线图:从当前AI到通用智能 2026奇点智能技术大会(https://ml-summit.org) 当前人工智能系统在特定任务上已展现出超越人类的表现,但其本质仍是窄域智能(Narrow AI)——依赖大量标注数据、固定分布假设…...
Godot逆向工程工具GDSDecomp:游戏资源解构与重构的深度解析
Godot逆向工程工具GDSDecomp:游戏资源解构与重构的深度解析 【免费下载链接】gdsdecomp Godot reverse engineering tools 项目地址: https://gitcode.com/GitHub_Trending/gd/gdsdecomp 在游戏开发与维护的生命周期中,资源包的管理与逆向分析一直…...
ROS2实战:构建模块化启动文件(launch file)以驱动复杂机器人系统
1. 为什么需要模块化启动文件 第一次接触ROS2的开发者往往会被一个简单问题困扰:为什么不能直接用ros2 run命令启动所有节点?想象你正在开发一辆自动驾驶小车,需要同时运行激光雷达驱动、SLAM算法、路径规划、底盘控制等十几个节点。如果每个…...
【行业首份智能编码故障白皮书】:基于178万行AI生成代码的故障热力图与根因诊断模型
第一章:智能代码生成代码故障诊断 2026奇点智能技术大会(https://ml-summit.org) 现代智能代码生成系统(如Copilot、CodeWhisperer、Tabnine)在提升开发效率的同时,也引入了新型故障模式:语义正确但逻辑错误、上下文…...
从电影特效到游戏UI:深入浅出聊聊Alpha通道和Premultiplied Alpha的那些‘坑’
从电影特效到游戏UI:深入浅出聊聊Alpha通道和Premultiplied Alpha的那些‘坑’ 在影视后期合成与游戏开发中,透明通道的处理就像空气般无处不在却又容易被忽视——直到出现诡异的黑边、白边或色彩失真。当你在Unity中导入精心制作的粒子特效PNG序列时&am…...
ZYNQ - 嵌入式Linux开发 - 从零到一:Petalinux工程构建与启动全解析
1. 从零搭建Petalinux开发环境 第一次接触ZYNQ嵌入式Linux开发的朋友,可能会被一堆专业术语吓到。其实没那么复杂,我刚开始也踩过不少坑,现在回头看整个流程其实挺清晰的。咱们先从最基础的环境搭建说起。 Petalinux是Xilinx官方提供的嵌入式…...
避坑指南:爬取深交所、上交所、中金所期权数据时,你可能遇到的编码、反爬与数据清洗问题
三大交易所期权数据爬取实战:编码陷阱、反爬策略与数据清洗全解析 当我们需要获取深交所、上交所和中金所的期权数据时,往往会遇到各种预料之外的挑战。这些挑战不仅来自网站的反爬机制,还包括数据编码、格式解析等看似简单却暗藏玄机的问题。…...
从ASTM标准到工程实践:雨流计数法的核心算法与选型指南
1. 雨流计数法:从标准到实战的桥梁 第一次接触雨流计数法是在处理风电塔筒的振动数据时,当时面对长达三个月的采样数据完全无从下手。直到发现ASTM E1049-85标准中这个神奇的方法,才明白原来疲劳分析可以如此优雅。简单来说,雨流计…...
思科紧急修复高危 ISE 漏洞
聚焦源代码安全,网罗国内外最新资讯!编译:代码卫士思科发布紧急安全公告,提醒用户称其 ISE 和 ISE-IPC 产品中存在多个漏洞,可导致经过身份认证的远程攻击者在受影响设备上执行任意命令。这些漏洞还可能导致路径遍历攻…...
数字IC面试核心:从MUX基础到Glitch-Free时钟切换电路深度剖析
1. 二选一MUX的基础原理 多路选择器(MUX)是数字电路中最基础的组合逻辑单元之一,它的核心功能就像铁路道岔的扳道工——根据控制信号决定哪条输入通道的数据能够到达输出端。我们先从最简单的二选一MUX入手,这不仅是面试必考题&am…...
