【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…...
用QT实现 端口扫描工具1
安装在线QT,尽量是完整地自己进行安装,不然会少包 参考【保姆级图文教程】QT下载、安装、入门、配置VS Qt环境-CSDN博客 临时存储空间不够。 Windows系统通常会使用C盘来存储临时文件。 修改临时文件存储位置 打开系统属性: 右键点击“此电…...
基于单片机的肺功能MVV简单测算
肺功能MVV一般是指肺部每分钟的最大通气量。 MVV本身是最大值的英文缩写,在临床上,肺功能MVV表示肺部每分钟最大通气量,用以衡量气道的通畅度,以及肺部和胸廓的弹性、呼吸肌的力量。 肺部每分钟的最大通气量的参考值男性与女性之…...
入手STM32单片机学习指南
目录 引言 一、基础概念 1.1 STM32单片机简介 1.2 ARM Cortex-M系列处理器 1.3 微控制器的基本组成 二、开发环境搭建 2.1 选择开发板 2.2 安装开发软件 2.3 配置开发环境 三、编程入门 3.1 GPIO编程 3.2 UART编程 3.3 ADC编程 引言 STM32单片机是基于ARM Cortex…...
无法定位软件包cuda
无法定位软件包cuda 如果你在使用 sudo apt install cuda 命令安装 CUDA 时遇到“无法定位软件包cuda”的问题,这可能是由于你的系统没有正确配置 CUDA 的安装源。以下是一些可能的解决方案: 更新 Ubuntu 软件源并升级到最新版本的软件包。你可以选择使…...
GWAS数据和软件下载
这部分主要是数据获取,以及软件配置方法。 一、配套数据和代码 数据和代码目前在不断的更新,最新的教程可以私信,我通过后手动发送最新版的pdf和数据代码。发送的压缩包,有电子版的pdf和数据下载链接,里面是最新的百度网盘的地址,下载到本地即可。然后根据pdf教程,结合配套的…...
SpringBoot3-深入理解自动配置类的原理(尚硅谷SpringBoot3-雷神)
文章目录 目录了解自动配置 一、导入对应场景的Mean依赖:1、引入依赖**找到自动配置类的所有配置都存放在哪里** 二、编写主程序:SpringBootApplication观察源码时所需要知道的几个核心注解:1、观察SpringBootApplication源码都做了什么 三、…...
MOE怎样划分不同专家
MOE怎样划分不同专家 目录 MOE怎样划分不同专家MOE划分不同专家的方法LLM模型拆分的方法**子模块拆分法**:**多头拆分法**:**层间拆分法****基于功能的拆分法**Python代码实现MOE划分不同专家以及LLM模型拆分的方法及举例如下: MOE划分不同专家的方法 ffn前馈神经网络 独立…...
NLP CH3复习
CH3 3.1 几种损失函数 3.2 激活函数性质 3.3 哪几种激活函数会发生梯度消失 3.4 为什么会梯度消失 3.5 如何解决梯度消失和过拟合 3.6 梯度下降的区别 3.6.1 梯度下降(GD) 全批量:在每次迭代中使用全部数据来计算损失函数的梯度。计算成本…...
Pyecharts SVG 标记使用笔记
Pyecharts SVG 标记使用笔记 在数据可视化中,图表的标记点常常用于突出显示重要数据点。Pyecharts 提供了丰富的标记点配置选项,其中使用 SVG 路径来自定义标记点的样式是一个非常强大的功能。本文将详细介绍如何在 Pyecharts 中使用 SVG 路径来定制标记…...
C++ 文件操作
文件操作 // 文件操作 // 程序运行时产生的数据都属于临时数据,程序结束后临时数据会被操作系统释放 // 通过文件操作可以将数据持久化 // c 中文件操作需要包含头文件 <fstream>// 文件类型分为两种: // 文本文件: 文件以文本的ASCII…...
享元模式详解
享元模式详解 一、定义 享元模式(Flyweight Pattern)是一种结构型设计模式,旨在通过共享对象来尽量减少内存的使用。它通过将重复使用的对象分离成共享和非共享部分,达到复用的目的,从而有效节省内存。具体来说&#x…...
数据库中的并发控制
并发操作带来的数据不一致性 1、并发控制:为了保证事务的隔离性和一致性,数据库管理系统需要对并发操作进行正确调度 并发控制的主要技术有:封锁、时间戳、乐观控制法、多版本并发控制等 并发操作带来的数据不一致性: ① 丟失修改:两个事务 T1 和 T2 读入同一数据…...
【软考网工笔记】计算机基础理论与安全——计算机硬件知识
计算机分级存储体系 计算机分级存储体系目前最常用的是三级存储体系。 CPU——CaChe(高速缓存)——主存——辅存 其中 Cache 是用于解决存取速度不够快,辅存是用于解决存储容量不够大,二者结合可在容量和速度实现提升的情况下尽可…...
SpringBoot 多种生产打包方式详解
Springboot 多种生产打包方式简介 生产上发布 Spring Boot 项目时,流程颇为繁琐且低效。但凡代码有一丁点改动,就得把整个项目重新打包部署,耗时费力不说,生成的 JAR 包还特别臃肿,体积庞大。每次更新项目,…...
WebSocket 安全实践:从认证到加密
在前三篇文章中,我们深入探讨了 WebSocket 的基础原理、服务端开发和客户端实现。今天,让我们把重点放在安全性上,看看如何构建一个安全可靠的 WebSocket 应用。我曾在一个金融项目中,通过实施多层安全机制,成功防御了多次恶意攻击尝试。 安全挑战 WebSocket 应用面临的主要安…...
实现单例模式的五种方式
如何实现一个单例 1、构造器需要私有化 2、提供一个私有的静态变量 3、暴露一个公共的获取单例对象的接口 需要考虑的两个问题 1、是否支持懒加载 2、是否线程安全 1、饿汉式 public class EagerSingleton {private static final EagerSingleton INSTANCE new EagerSi…...
【Go学习】-01-6-数据库泛型新特性
【Go学习】-01-6-数据库泛型新特性 1 数据库操作1.1 操作mysql1.1.1 Insert1.1.2 Select1.1.3 Update1.1.4 Delete1.1.5 sql事务 1.2 go操作Redis 2 泛型2.1 非泛型函数2.2 泛型函数2.3 泛型类型2.3.1 泛型结构体2.3.2 泛型接口 2.4 泛型约束2.5 泛型切片和映射2.5.1 泛型切片2…...
算法学习(22)—— BFS解决最短路问题
关于最短路问题 最短路问题是“图论”里非常重要的一类问题,涉及的内容非常多,在这个专题里,我们主要讲“边权为1的最短路问题”,因为这个比较基础比较简单而关于啥是“边权为1的最短路问题”,我们通过下面的例子来讲…...
【双层模型】考虑供需双侧的综合能源双层优化模型
目录 主要内容 内容研究 1.模型简介 2 程序释义 部分代码 运行结果 下载链接 主要内容 该程序实现一个综合能源系统的优化调度双层模型,上下层分别采用差分进化算法和规划算法进行求解。模型考虑了多种能源设备,包括燃气轮机、燃气锅炉、风电…...
【读书笔记/源码】How Tomcat Works 笔记- c11~c13
chapter11: standardwrapperchapter12: 无程序 第十章 安全性 servlet容器是通过一个名为验证器的阀来支持安全限制的。当servlet容器启动时,验证器阀会被添加到Context容器的管道中。 验证器阀会调用Context容器的领域对象的authenticate()方法,传入…...
Electron快速入门——跨平台桌面端应用开发框架
个人简介 👀个人主页: 前端杂货铺 🙋♂️学习方向: 主攻前端方向,正逐渐往全干发展 📃个人状态: 研发工程师,现效力于中国工业软件事业 🚀人生格言: 积跬步…...
Vision Transformer模型详解(附pytorch实现)
写在前面 最近,我在学习Transformer模型在图像领域的应用。图像处理任务一直以来都是深度学习领域的重要研究方向,而传统的卷积神经网络已在许多任务中取得了显著的成绩。然而,近年来,Transformer模型由于其在自然语言处理中的成…...
中国区域创新创业指数IRIEC数据(省级、地市级)1990-2020年-社科数据
中国区域创新创业指数IRIEC数据(省级、地市级)1990-2020年-社科数据https://download.csdn.net/download/paofuluolijiang/90028728 https://download.csdn.net/download/paofuluolijiang/90028728 中国区域创新创业指数(IRIEC)…...
Elasticsearch:减少 Elastic 容器镜像中的 CVE(常见的漏洞和暴露)
作者:来自 Elastic Maxime Greau 在这篇博文中,我们将讨论如何通过在 Elastic 产品中切换到最小基础镜像并优化可扩展漏洞管理程序的工作流程来显著减少 Elastic 容器镜像中的常见漏洞和暴露 (Common Vulnerabilities and Exposures - CVEs)。 基于 Chai…...
webpack02
webpack中常用loader postcss-loader 在css-loader之前,对css进行一些操作,,,比如统一加前缀,,或者是重置样式,,, 这个postcss-loader会自己去找 postcss工具࿰…...
腾讯云更改用户为root
最近买了台99元一年的2核的云服务器,方便学习一些java开发中间件,以及部署一些项目。 1.设置root用户密码 sudo passwd root 2.修改配置文件 ll /etc | grep ssh cd /etc/ssh/ ls vim sshd_config 输入/PasswordAuthentication 寻找 输入:set nu 再按下…...
Excel导入导出-若依版本
最终效果 1、导出 1、在实体类上加注解 Excel(name “客户类型名称”) ToString AllArgsConstructor NoArgsConstructor public class UserType extends BaseEntity2 implements Serializable {Excel(name "客户类型ID", cellType Excel.ColumnType.NUMERIC…...
【Qt】快速添加对应类所需的头文件包含
快速添加对应类所需的头文件包含 一,简介二,操作步骤 一,简介 本文介绍一下,如何快速添加对应类所需要包含的头文件,可以提高开发效率,供参考。 二,操作步骤 以QTime类为例: 选中…...
基于服务器部署的综合视频安防系统的智慧快消开源了。
智慧快消视频监控平台是一款功能强大且简单易用的实时算法视频监控系统。它的愿景是最底层打通各大芯片厂商相互间的壁垒,省去繁琐重复的适配流程,实现芯片、算法、应用的全流程组合,从而大大减少企业级应用约95%的开发成本。国产化人工智能“…...
浅谈棋牌游戏开发流程七:反外挂与安全体系——守护游戏公平与玩家体验
一、前言:为什么反外挂与安全这么重要? 对于任何一款线上棋牌游戏而言,公平性和玩家安全都是最重要的核心要素之一。如果游戏环境充斥着各式各样的外挂、作弊方式,不仅会毁坏玩家体验,更会导致游戏生态崩塌、口碑下滑…...
Laravel操作ElasticSearch
在Laravel项目中操作ElasticSearch可以通过以下步骤来实现,通常会借助相应的ElasticSearch客户端扩展包。 ### 安装ElasticSearch客户端包 在Laravel项目中,常用的是 elasticsearch/elasticsearch 这个PHP客户端库来与ElasticSearch进行交互,…...
缓存-文章目录
关于缓存系列文章: 缓存学习总结1(缓存分类) 缓存学习总结2(服务器本地缓存) 缓存学习总结3(服务器内存缓存)推荐使用 缓存学习总结4(分布式缓存) 关于redis系列文章…...
安装教程:慧集通集成平台(DataLinkX)智能体客户端安装操作(Linux/windows/mac)
1.下载客户端 使用提供的账号登录集成平台后台(https://www.datalinkx.cn/),点击左侧菜单栏【智能体】→【智能体】进入到智能体列表界面,在该界面我们找到功能栏中的下载按钮点击则会弹出下载界面,在该界面我们可以选择不同的系统操作系统来下载对应版…...
解决vmware虚拟机和宿主机之间不能复制粘贴
在虚拟机内执行一下命令 /usr/bin/vmware-user 更多解决方案 https://www.cnblogs.com/wutou/p/17629408.html...
由源程序到运行
由源程序到运行 第一步:编写源程序 assume cs:codesg codesg segmentmov ax,0123Hmov bx,0456Hadd ax,bxadd ax,axmov ax,4c00hint 21h codesg ends end第二步:进行编译 进入到编译目录 编译 .asm文件生成目标文件(.obj) m…...
Java-JDBC的使用
目录 一、JDBC(java数据库连接):java database connector 二、使用JDBC的步骤 三、加条件查询 四、预处理(防止SQL注入) 五、Statement和PreparedStatement的优略 六、将数据中的数据查询出来后需要保存在一个集合中,方便前端…...
如何优化亚马逊广告以提高ROI?
在竞争激烈的亚马逊市场中,优化广告以提高投资回报率(ROI)是卖家的关键任务。以下是一些实用的策略: 一、精准的关键词研究与选择 深入了解产品特性和目标受众 详细分析产品的功能、用途、优势和适用人群。例如,如果你…...
身是菩提树,心如明镜台;时时勤拂拭,莫使惹尘埃。
神秀: 身是菩提树,心如明镜台;时时勤拂拭,莫使惹尘埃。 第一个毛病1: 在神秀看来,修行就是要保持我们本来干净的心, 跟外部世界的灰尘之间的隔绝状态,始终保持这种隔绝, 尘世是什么? 尘就是烦恼,人世间无处不是烦恼&a…...
如何修复富士相机卡错误并恢复卡数据
富士相机以其卓越的图像质量而闻名,但不幸的是,其 SD 卡错误可能会意外发生,导致数据丢失和摄影会话中断。 在本指南中,我们将引导您了解常见的富士相机 SD 卡错误、如何修复这些错误,以及如何有效地从损坏的卡中恢复…...
呼叫中心中间件实现IVR进入排队,判断排队超时播放提示音
文章目录 [TOC](文章目录) 前言需求排队结束原因 联系我们实现步骤1. 调用http接口返回动作2. 启用拨号方案 前言 需求 呼叫中心需要实现调用IVR接口进入排队,如果是因为等待超时导致退出排队的,那就播放一段提示音再挂断通话;其他的情况就…...
数据分析思维(八):分析方法——RFM分析方法
数据分析并非只是简单的数据分析工具三板斧——Excel、SQL、Python,更重要的是数据分析思维。没有数据分析思维和业务知识,就算拿到一堆数据,也不知道如何下手。 推荐书本《数据分析思维——分析方法和业务知识》,本文内容就是提取…...
SpringBoot3动态切换数据源
背景 随着公司业务战略的发展,相关的软件服务也逐步的向多元化转变,之前是单纯的拿项目,赚人工钱,现在开始向产品化\服务化转变。最近雷袭又接到一项新的挑战:了解SAAS模型,考虑怎么将公司的产品转换成多租…...
Java虚拟机面试题:内存管理(上)
🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/?__c1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,精通Java编…...
WPF通过反射机制动态加载控件
Activator.CreateInstance 是 .NET 提供的一个静态方法,它属于 System 命名空间。此方法通过反射机制根据提供的类型信息。 写一个小demo演示一下 要求:在用户反馈界面点击建议或者评分按钮 弹出相应界面 编写MainWindow.xmal 主窗体 <Window x:C…...
前端学习-操作元素属性(二十三)
前言 假期快乐,大家加油 操作元素属性 操作元素常用属性 还可以通过 JS 设置/修改标签元素属性,比如通过 src更换 图片最常见的属性 比如:href、title、src等语法:对象.属性 值 const pic document.querySelector(img);pic.src ./images/b0.jpgp…...
Javascript 编写的一个红、黄、绿灯交替变亮
为了创建一个简单但功能完整的交通灯程序,我们将使用 HTML、CSS 和 JavaScript 来实现红、黄、绿三种颜色按照规定的顺序循环显示。这个例子将确保灯光按照红 -> 绿 -> 黄的顺序循环,并且可以调整每个灯光的持续时间以模拟真实的交通灯行为。 效果…...
基于64QAM的载波同步和定时同步性能仿真,包括Costas环和gardner环
目录 1.算法仿真效果 2.算法涉及理论知识概要 3.MATLAB核心程序 4.完整算法代码文件获得 1.算法仿真效果 matlab2022a仿真结果如下(完整代码运行后无水印): 仿真操作步骤可参考程序配套的操作视频。 2.算法涉及理论知识概要 载波同步是…...
小于n的最大数 - 贪心算法 - C++
字节经典面试题 给定一个整数n,并从1~9中给定若干个可以使用的数字,根据上述两个条件,得到每一位都为给定可使用数字的、最大的小于整数n的数,例如,给定可以使用的数字为 {2,3,8} 三个数:给定 n3589&#x…...
leetcode(hot100)4
解题思路:双指针思想 利用两个for循环,第一个for循环把所有非0的全部移到前面,第二个for循环将指针放在非0的末尾全部加上0。 还有一种解法就是利用while循环双指针条件,当不为0就两个指针一起移动 ,为0就只移动右指针…...