Spring5新宠:PathPattern

真正的程序员认为自己比用户更明白用户需要什么 。
本文已被收录;程序员专用网盘;公号后台回复“专栏列表”获取全部小而美的原创技术专栏
前言
你好 , 我是 。
依稀记得3年前的在“玩” 的时候 , 看到在ng中起到了重要作用:用于URL的匹配 。当时就很好奇:这一直不都是的活吗?
于是乎我就拿出了自己更为熟悉的 对于类进行功能比对,发现扮演的角色和一毛一样,所以当时也就没去深入研究啦 。
正所谓念念不忘必有回响 。时隔3年最近又回到搞了,欠下的债总归要还呀,有必要把深入解读 , 毕竟它是在路径解析器方面的新宠,贯穿上下 。重点是号称比拥有更好的使用体验以及更快的匹配效率,咦,勾起了兴趣了解一下~
正值周末,说干就干 。
所属专栏本文提纲
版本约定正文
是新增的API,所在包:org..web.util..,所属模块为-web 。可见它专为Web设计的“工具” 。
不同于是一个“上帝类”把所有活都干了,新的路径匹配器围绕着拥有一套体系 , 在设计上更具模块化、更加面向对象,从而拥有了更好的可读性和可扩展性 。
下面深入了解下该技术体系下的核心元素 。主要有:
:路径元素
顾名思义,它表示路径节点 。一个path会被解析成N多个节点 。
核心属性:
// Since: 5.0abstract class PathElement {protected final int pos;protected final char separator;@Nullableprotected PathElement next;@Nullableprotected PathElement prev;}
所有的之间形成链状结构,构成一个完整的URL模板 。
Tips:我个人意见,并不需要太深入去了解内部的具体实现,在宏观角度了解它的定义,然后认识下它的子类实现不同的节点类型即可
它有如下子类实现:
分离器元素 。代表用于分离的元素(默认是/ , 也可以是.)
@Testpublic void test1() {PathPatternParser parser = new PathPatternParser();PathPattern pathPattern = parser.parse("/api/v1");System.out.println(pathPattern);}
断点调试查看解析后的变量拥有的元素情况:
可以看到这是标准的链式结构嘛,这种关系用图画出来就是这样子:
其中绿色的/都是类型,蓝色都是字面量类型 。将一个拆解成为了一个个的对象,后面就可以方便的面向对象编程,大大增加了可读性、降低出错的概率 。
说明:由于这是第一个元素,所以才举了个实际的代码示例辅助理解 。下面的就只需描述概念啦,举一反三即可~
通配符元素 。如:/api/*/
说明:在路径中间它至少匹配1个字符(//不行,/ /可行),但在路径末尾可以匹配0个字符
单字符通配符元素 。如:/api/your??tman
说明:一个?代表一个单字通配符 , 若需要适配多个用多个?即可
通配剩余路径元素 。如:/api//**
说明:**只能放在path的末尾,这才是rest剩余的含义嘛
将一段路径作为变量捕获的路径元素 。如:/api//{age}
说明:{age}就代表此元素类型被封装进来
ement
捕获路径其余部分的路径元素 。如:/api//{*}
说明:若待匹配的路径是/api//a/b/c , 那么=a/b/c
字面量元素 。不解释~
正则表达式元素 。如:api/*_*/*_{age}
说明:*_*和*_{age}都会被解析为该元素类型,这种写法是从里派生来过的(但不会依赖于)
总之:任何一个字符串的最终都会被解析为若干段的,这些以链式结构连接起来用以表示该,形成一个对象数据 。不同于的纯字符串操作,这里把每一段都使用对象来描述 , 结构化的表示使得可读性更强、更具灵活性,甚至可以获得更好的性能表现 。
:URL的结构化表示
和类似,待匹配的path的每一段都会表示为一个元素并保存其元数据信息 。也就是说:每一个待匹配的URL路径都会被解析为一个实例 。
虽然是个接口,但我们无需关心其实现,类同于Java 8的java.util..接口使用者无需关心其实现一样 。因为提供了静态工具方法用于直接生成对应实例 。体验一把:
@Testpublic void test2() {PathContainer pathContainer = PathContainer.parsePath("/api/v1/address", PathContainer.Options.HTTP_PATH);System.out.println(pathContainer);}
debug模式运行,查看对象详情:
这和解析为的结构何其相似(不过这里元素们是通过有序的集合组织起来的) 。对比看来,拍脑袋应该能够猜到何新版的匹配效率会更高了吧 。
补充说明:
:路径解析的模式
表示解析路径的模式 。包括用于快速匹配的路径元素链,并累积用于快速比较模式的计算状态 。它是直接面向使用者进行匹配逻辑的最重要API,完成match操作 。
所在包是org..web.util..,位于-web模块,专为web(含和)设计的全新一套路径匹配API,具有更高的匹配效率 。
认识下它的成员属性:
// Since: 5.0public class PathPattern implements Comparable {// pattern的字符串形式private final String patternString;// 用于构建本实例的解析器private final PathPatternParser parser;// 分隔符使用/还是.,默认是/private final PathContainer.Options pathOptions;// 如果pattern里结尾没/而待匹配的有,仍然让其匹配成功(true) , 默认是trueprivate final boolean matchOptionalTrailingSeparator;// 是否对大小写敏感,默认是trueprivate final boolean caseSensitive;// 链式结构:表示URL的每一部分元素@Nullableprivate final PathElement head;private int capturedVariableCount;private int normalizedLength;private boolean endsWithSeparatorWildcard = false;private int score;private boolean catchAll = false;}
以上属性是直接读?。旅嬲庑└鍪羌扑愠隼吹?nbsp;, 比较特殊就特别照顾下:
score、等标记用于加速匹配的速度 , 具体体现在.OR这个比较器上,这是速度比快的根因之一
值得注意的是:所有属性均不提供的set方法,也就是说实例一旦创建就是只读(不可变)实例了 。
快速创建缺省的实例
上面了解到,的构造器不是的,所以有且仅能通过创建其实例 。然而,为快速满足绝大多数场景,还提供了一种快速创建缺省的实例的方式:
提供一个全局共享的、只读的实例用于快速创建缺省的实例 , 类似于实例工厂的作用 。毕竟绝大部分场景下用的缺省属性即可,因此有了它着实方便不少 。
注意:虽然该实例是全局共享只有1个,但是,创建出来的可是不同实例哦(基本属性都一样而已)
代码示例
的匹配方式和基本保持一致:使用的基于Ant风格模式匹配 。
但是发现没,这里不再强调Ant字样 , 也许觉得Ant的概念确实已廉波老矣?不符合它紧跟潮流的身份?
相比于,主要有两处地方不一样:
说明:只支持两种分隔符(/和.),而可以随意指定 。虽然这也是不同点,但这一般无伤大雅所以就不单独列出了
1. 新增{*}语法支持
这是新增的“语法”,表示匹配余下的path路径部分并将其赋值给变量 。
@Testpublic void test1() {System.out.println("======={*pathVariable}语法======");PathPattern pattern = PathPatternParser.defaultInstance.parse("/api/yourbatman/{*pathVariable}");// 提取匹配到的的变量值System.out.println("是否匹配:" + pattern.matches(PathContainer.parsePath("/api/yourbatman/a/b/c")));PathPattern.PathMatchInfo pathMatchInfo = pattern.matchAndExtract(PathContainer.parsePath("/api/yourbatman/a/b/c"));System.out.println("匹配到的值情况:" + pathMatchInfo.getUriVariables());}======={*pathVariable}语法======是否匹配:true匹配到的值情况:{pathVariable=/a/b/c}
在没有之前,虽然也可以通过/**来匹配成功,但却无法得到匹配到的值,现在可以了!
和**的区别
我们知道/**和/{*}都有匹配剩余所有path的“能力”,那它俩到底有什么区别呢?
/**能匹配成功 , 但无法获取到动态成功匹配元素的值/{*}可认为是/**的加强版:可以获取到这部分动态匹配成功的值
正所谓一代更比一代强嘛 , 如是而已 。
和**的优先级关系
既然/**和/{*}都有匹配剩余path的能力 , 那么它俩若放在一起,优先级关系是怎样的呢?
妄自猜测没有意义,跑个案例一看便知:由于实现了比较器接口,因此本例利用自动排序即可 , 排第一的证明优先级越高
@Testpublic void test2() {System.out.println("======={*pathVariable}和/**优先级======");PathPattern pattern1 = PathPatternParser.defaultInstance.parse("/api/yourbatman/{*pathVariable}");PathPattern pattern2 = PathPatternParser.defaultInstance.parse("/api/yourbatman/**");SortedSet sortedSet = new TreeSet<>();sortedSet.add(pattern1);sortedSet.add(pattern2);System.out.println(sortedSet);}======={*pathVariable}和/**优先级======[/api/yourbatman/**, /api/yourbatman/{*pathVariable}]
测试代码的细节:故意将/{*}先放进set里面而后放/**,但最后还是/**在前 。
结论:当二者同时出现(出现冲突)时,/**优先匹配 。
2. 禁用中间**语法支持
在上篇文章对的详细分析文章中,我们知道是可以把/**放在整个URL中间用来匹配的,如:
@Testpublic void test4() {System.out.println("=======**:匹配任意层级的路径/目录=======");String pattern = "/api/**/yourbatman";match(1, MATCHER, pattern, "/api/yourbatman");match(2, MATCHER, pattern, "/api//yourbatman");match(3, MATCHER, pattern, "/api/a/b/c/yourbatman");}=======**:匹配任意层级的路径/目录=======1 match结果:/api/**/yourbatman 【成功】 /api/yourbatman2 match结果:/api/**/yourbatman 【成功】 /api//yourbatman3 match结果:/api/**/yourbatman 【成功】 /api/a/b/c/yourbatman
与不同 , **仅在模式末尾受支持 。中间不被允许了 , 否则实例创建阶段就会报错:
@Testpublic void test3() {System.out.println("=======/**放在中间语法======");PathPattern pattern = PathPatternParser.defaultInstance.parse("/api/**/yourbatman");pattern.matches(PathContainer.parsePath("/api/a/b/c/yourbatman"));}=======/**放在中间语法======org.springframework.web.util.pattern.PatternParseException: No more pattern data allowed after {*...} or ** pattern elementat org.springframework.web.util.pattern.InternalPathPatternParser.peekDoubleWildcard(InternalPathPatternParser.java:250)...
从报错中还能看出端倪:不仅**,{*xxx}也是不能放在中间而只能是末尾的
这么做的目的是:消除歧义 。
那么问题来了 , 如果就是想匹配中间的任意层级路径怎么做呢?
答:首先这在web环境里有这样需求的概率极?。ㄖ皇视糜趙eb环境) , 若这依旧是刚需,那就只能蜕化到借助来完成喽 。
对比
二者目前都存在于技术栈内,做着“相同”的事 。虽说现在还鲜有同学了解到,我认为淘汰掉只是时间问题(特指web环境哈) , 毕竟后浪总归有上岸的一天 。
但不可否认 , 二者将在较长时间内共处,那么它俩到底有何区别呢?了解一下
出现时间
是一个早在2003年(的第一个版本)就已存在的路径匹配器 , 而是 5新增的,旨在用于替换掉较为“古老”的 。
功能差异
去掉了Ant字样,但保持了很好的向下兼容性:除了不支持将**写在path中间之外,其它的匹配规则从行为上均保持和一致,并且还新增了强大的{*}的支持 。
因此在功能上姑且可认为二者是一致的 , 极特殊情况下的不兼容除外 。
性能差异
官方说的性能优于,我抱着怀疑的态度做了测试,示例代码和结果如下:
// 匹配的模板:使用一个稍微有点复杂的模板进行测试private static final String pattern = "/api/your?atman/{age}/**";
// AntPathMatcher匹配代码:使用单例的PathMatcher,符合实际使用情况private static final PathMatcher MATCHER = new AntPathMatcher();public static void antPathMatcher(String reqPath) {MATCHER.match(reqPath);}
// PathPattern代码示例:这里的pattern由下面来定义private static final PathPattern PATTERN = PathPatternParser.defaultInstance.parse(pattern);public static void pathPattern(String reqPath) {PATTERN.matches(PathContainer.parsePath(reqPath));}
匹配的测试代码:
@Testpublic void test1() {Instant start = Instant.now();for (int i = 0; i < 100000; i++) {String reqPath = "/api/yourBatman/" + i + "/" + i;antPathMatcher(reqPath);// pathPattern(reqPath);}System.out.println("耗时(ms):" + Duration.between(start, Instant.now()).toMillis());}
不断调整循环次数,且各执行三次,将结果绘制成如下表格:
测试机配置为:
循环次:
路径匹配器第1次耗时第2次耗时第3次耗时
171
199
188
118
134
128
循环次:
路径匹配器第1次耗时第2次耗时第3次耗时
944
852
882
633
637
626
循环次:
路径匹配器第1次耗时第2次耗时第3次耗时
5561
5469
5461
4495
4440
4571
结论:性能比优秀 。理论上越复杂 , 的优势越明显 。
最佳实践
既然路径匹配器有两种方案 , 那必然有最佳实践 。官方对此也是持有态度的:
Web环境
如果是应用() , 官方推荐(只是推荐,但默认的依旧是哈) , 相关代码体现在里:
【Spring5新宠:PathPattern】// Since: 07.04.2003public abstract class AbstractHandlerMapping ... {private UrlPathHelper urlPathHelper = new UrlPathHelper();private PathMatcher pathMatcher = new AntPathMatcher();...@Nullableprivate PathPatternParser patternParser;// Since: 5.3public void setPatternParser(PathPatternParser patternParser) {this.patternParser = patternParser;}}
注意:()从5.3版本开始才被加入,也就说虽然从 5就有了,但直到5.3版本才被加入到里 , 且作为可?。弦谰墒牵?。换句话讲:在 5.3版本之前,仍旧只能用 。
在里启用
默认情况下,MVC依旧是使用的进行路径匹配的,那如何启用效率更高的呢?
通过上面源码知道,就是要调用ng的方法嘛 , 其实为此是预留了扩展点的,只需这么做即可:
/*** 在此处添加备注信息** @author YourBatman. Send email to me* @site https://yourbatman.cn* @date 2021/6/20 18:33* @since 0.0.1*/@Configuration(proxyBeanMethods = false)public class WebMvcConfiguration implements WebMvcConfigurer {@Overridepublic void configurePathMatch(PathMatchConfigurer configurer) {configurer.setPatternParser(PathPatternParser.defaultInstance);}}
如果是应用(),那就是唯一解决方案 。这体现在org..web...ng:
// Since: 5.0public abstract class AbstractHandlerMapping... {private final PathPatternParser patternParser;...public AbstractHandlerMapping() {this.patternParser = new PathPatternParser();}}
里早已不见的踪影,因为是从 5.0开始的,因此没有向下兼容的负担,直接全面拥抱了 。
结论:语法更适合于web应用程序,其使用更方便且执行更高效 。
非Web环境
嗯,如果认真“听课”了的同学就知道:非Web环境依旧有且仅有一种选择,那便是,因为是专为Web环境设计,不能用于非Web环境 。所以像上面资源加载、包名扫描之类的,底层依旧是交给去完成 。
说明:由于这类URL的解析绝大多数情况下匹配一次(执行一次)就行,所以微小的性能差异是无所谓的(对API来讲收益较大)
可能有小伙伴会说:在层,甚至Dao层我也可以正常使用对象呀 , 何解?