首页 >> 大全

Spring5新宠:PathPattern

2024-01-11 大全 261 作者:考证青年

真正的程序员认为自己比用户更明白用户需要什么。

本文已被收录;程序员专用网盘;公号后台回复“专栏列表”获取全部小而美的原创技术专栏

前言

你好,我是。

依稀记得3年前的在“玩” 的时候,看到在ng中起到了重要作用:用于URL的匹配。当时就很好奇:这一直不都是的活吗?

于是乎我就拿出了自己更为熟悉的 对于类进行功能比对,发现扮演的角色和一毛一样,所以当时也就没去深入研究啦。

正所谓念念不忘必有回响。时隔3年最近又回到搞了,欠下的债总归要还呀,有必要把深入解读,毕竟它是在路径解析器方面的新宠,贯穿上下。重点是号称比拥有更好的使用体验以及更快的匹配效率,咦,勾起了兴趣了解一下~

正值周末,说干就干。

所属专栏 本文提纲

版本约定 正文

是新增的API,所在包:org..web.util..,所属模块为-web。可见它专为Web设计的“工具”。

不同于是一个“上帝类”把所有活都干了,新的路径匹配器围绕着拥有一套体系,在设计上更具模块化、更加面向对象,从而拥有了更好的可读性和可扩展性。

下面深入了解下该技术体系下的核心元素。 主要有:

:路径元素

顾名思义,它表示路径节点。一个path会被解析成N多个节点。

核心属性:

// Since: 5.0
abstract class PathElement {protected final int pos;protected final char separator;@Nullableprotected PathElement next;@Nullableprotected PathElement prev;
}

所有的之间形成链状结构,构成一个完整的URL模板。

Tips:我个人意见,并不需要太深入去了解内部的具体实现,在宏观角度了解它的定义,然后认识下它的子类实现不同的节点类型即可

它有如下子类实现:

分离器元素。代表用于分离的元素(默认是/,也可以是.)

@Test
public 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..接口使用者无需关心其实现一样。因为提供了静态工具方法用于直接生成对应实例。体验一把:

@Test
public 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.0
public class PathPattern implements Comparable<PathPattern> {// 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;}

以上属性是直接读取,下面这些个是计算出来的,比较特殊就特别照顾下:

score、等标记用于加速匹配的速度,具体体现在.OR这个比较器上,这是速度比快的根因之一

值得注意的是:所有属性均不提供的set方法,也就是说实例一旦创建就是只读(不可变)实例了。

快速创建缺省的实例

上面了解到,的构造器不是的,所以有且仅能通过创建其实例。然而,为快速满足绝大多数场景,还提供了一种快速创建缺省的实例的方式:

提供一个全局共享的、只读的实例用于快速创建缺省的实例,类似于实例工厂的作用。毕竟绝大部分场景下用的缺省属性即可,因此有了它着实方便不少。

注意:虽然该实例是全局共享只有1个,但是,创建出来的可是不同实例哦(基本属性都一样而已)

代码示例

的匹配方式和基本保持一致:使用的基于Ant风格模式匹配。

但是发现没,这里不再强调Ant字样,也许觉得Ant的概念确实已廉波老矣?不符合它紧跟潮流的身份?

相比于,主要有两处地方不一样:

说明:只支持两种分隔符(/和.),而可以随意指定。虽然这也是不同点,但这一般无伤大雅所以就不单独列出了

1. 新增{*}语法支持

这是新增的“语法”,表示匹配余下的path路径部分并将其赋值给变量。

@Test
public 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的能力,那么它俩若放在一起,优先级关系是怎样的呢?

妄自猜测没有意义,跑个案例一看便知:由于实现了比较器接口,因此本例利用自动排序即可,排第一的证明优先级越高

@Test
public void test2() {System.out.println("======={*pathVariable}和/**优先级======");PathPattern pattern1 = PathPatternParser.defaultInstance.parse("/api/yourbatman/{*pathVariable}");PathPattern pattern2 = PathPatternParser.defaultInstance.parse("/api/yourbatman/**");SortedSet<PathPattern> sortedSet = new TreeSet<>();sortedSet.add(pattern1);sortedSet.add(pattern2);System.out.println(sortedSet);
}======={*pathVariable}/**优先级======
[/api/yourbatman/**, /api/yourbatman/{*pathVariable}]

测试代码的细节:故意将/{*}先放进set里面而后放/**,但最后还是/**在前。

结论:当二者同时出现(出现冲突)时,/**优先匹配。

2. 禁用中间**语法支持

在上篇文章对的详细分析文章中,我们知道是可以把/**放在整个URL中间用来匹配的,如:

@Test
public 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/yourbatman
2	match结果:/api/**/yourbatman	【成功】	/api//yourbatman
3	match结果:/api/**/yourbatman	【成功】	/api/a/b/c/yourbatman

与不同,**仅在模式末尾受支持。中间不被允许了,否则实例创建阶段就会报错:

@Test
public 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环境里有这样需求的概率极小(只适用于web环境),若这依旧是刚需,那就只能蜕化到借助来完成喽。

对比

二者目前都存在于技术栈内,做着“相同”的事。虽说现在还鲜有同学了解到,我认为淘汰掉只是时间问题(特指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));
}

匹配的测试代码:

@Test
public 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环境

如果是应用(),官方推荐(只是推荐,但默认的依旧是哈),相关代码体现在里:

// Since: 07.04.2003
public 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.0
public abstract class AbstractHandlerMapping... {private final PathPatternParser patternParser;...public AbstractHandlerMapping() {this.patternParser = new PathPatternParser();}
}

里早已不见的踪影,因为是从 5.0开始的,因此没有向下兼容的负担,直接全面拥抱了。

结论:语法更适合于web应用程序,其使用更方便且执行更高效。

非Web环境

嗯,如果认真“听课”了的同学就知道:非Web环境依旧有且仅有一种选择,那便是,因为是专为Web环境设计,不能用于非Web环境。所以像上面资源加载、包名扫描之类的,底层依旧是交给去完成。

说明:由于这类URL的解析绝大多数情况下匹配一次(执行一次)就行,所以微小的性能差异是无所谓的(对API来讲收益较大)

可能有小伙伴会说:在层,甚至Dao层我也可以正常使用对象呀,何解?

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了