Spring5新寵PathPattern,AntPathMatcher:那我走?
前言
你好,我是YourBatman。
依稀記得3年前的在“玩”Spring WebFlux的時候,看到PathPattern在AbstractHandlerMapping中起到了重要作用:用于URL的匹配。當時就很好奇:這一直不都是AntPathMatcher的活嗎?
于是乎我就拿出了自己更為熟悉的Spring WebMvc對于類進行功能比對,發現PathPattern扮演的角色和AntPathMatcher一毛一樣,所以當時也就沒去深入研究啦。
正所謂念念不忘必有回響。時隔3年最近又回到搞WebFlux了,欠下的債總歸要還呀,有必要把PathPattern深入解讀,畢竟它是Spring5在路徑解析器方面的新寵,貫穿WebFlux上下。重點是號稱比AntPathMatcher擁有更好的使用體驗以及更快的匹配效率,咦,勾起了興趣了解一下~
正值周末,說干就干。
所屬專欄
- 點撥-Spring技術棧
本文提綱
版本約定
- JDK:8
- Spring Framework:5.3.x
正文
PathPattern是Spring5新增的API,所在包:org.springframework.web.util.pattern.PathPattern,所屬模塊為spring-web。可見它專為Web設計的“工具”。
不同于AntPathMatcher是一個“上帝類”把所有活都干了,新的路徑匹配器圍繞著PathPattern擁有一套體系,在設計上更具模塊化、更加面向對象,從而擁有了更好的可讀性和可擴展性。
下面深入了解下該技術體系下的核心元素。主要有:
- PathElement:路徑元素。一個URL模板根據/可以拆分成N多個路徑元素對象
- PathContainer:URL的結構化表示。一個URL對應一個PathContainer對象實例
- PathPattern:路徑解析的模式。路徑模式匹配器的最核心API
- PathPatternParser:將一個String類型的模式解析為PathPattern實例,這是創建PathPattern實例的唯一方式
PathElement:路徑元素
顧名思義,它表示路徑節點。一個path會被解析成N多個PathElement節點。
核心屬性:
- // Since: 5.0
- abstract class PathElement {
- protected final int pos;
- protected final char separator;
- @Nullable
- protected PathElement next;
- @Nullable
- protected PathElement prev;
- }
- pos:該節點在path里的起點位置
- separator:該path使用的分隔符
- next:后節點,可以為null(如最后一個節點)
- prev:前節點,可以為null(如第一個節點)
所有的PathElement之間形成鏈狀結構,構成一個完整的URL模板。
Tips:我個人意見,并不需要太深入去了解PathElement內部的具體實現,在宏觀角度了解它的定義,然后認識下它的子類實現不同的節點類型即可
它有如下子類實現:
SeparatorPathElement
分離器元素。代表用于分離的元素(默認是/,也可以是.)
- @Test
- public void test1() {
- PathPatternParser parser = new PathPatternParser();
- PathPattern pathPattern = parser.parse("/api/v1");
- System.out.println(pathPattern);
- }
斷點調試查看解析后的pathPattern變量擁有的元素情況:
可以看到這是標準的鏈式結構嘛,這種關系用圖畫出來就是這樣子:
其中綠色的/都是SeparatorPathElement類型,藍色都是LiteralPathElement字面量類型。將一個Pattern拆解成為了一個個的Element對象,后面就可以方便的面向對象編程,大大增加了可讀性、降低出錯的概率。
說明:由于這是第一個元素,所以才舉了個實際的代碼示例輔助理解。下面的就只需描述概念啦,舉一反三即可~
WildcardPathElement
通配符元素。如:/api/*/yourbatman
說明:在路徑中間它至少匹配1個字符(//不行,/ /可行),但在路徑末尾可以匹配0個字符
SingleCharWildcardedPathElement
單字符通配符元素。如:/api/your??tman
說明:一個?代表一個單字通配符,若需要適配多個用多個?即可
WildcardTheRestPathElement
通配剩余路徑元素。如:/api/yourbatman/**
說明:**只能放在path的末尾,這才是rest剩余的含義嘛
CaptureVariablePathElement
將一段路徑作為變量捕獲的路徑元素。如:/api/yourbatman/{age}
說明:{age}就代表此元素類型被封裝進來
CaptureTheRestPathElement
捕獲路徑其余部分的路徑元素。如:/api/yourbatman/{*restPath}
說明:若待匹配的路徑是/api/yourbatman/a/b/c,那么restPath=a/b/c
LiteralPathElement
字面量元素。不解釋~
RegexPathElement
正則表達式元素。如:api/*_*/*_{age}說明:*_*和*_{age}都會被解析為該元素類型,這種寫法是從AntPathMatcher里派生來過的(但不會依賴于AntPathMatcher)
總之:任何一個字符串的pattern最終都會被解析為若干段的PathElement,這些PathElement以鏈式結構連接起來用以表示該pattern,形成一個對象數據。不同于AntPathMatcher的純字符串操作,這里把每一段都使用對象來描述,結構化的表示使得可讀性更強、更具靈活性,甚至可以獲得更好的性能表現。
PathContainer:URL的結構化表示
和PathPattern類似,待匹配的path的每一段都會表示為一個元素并保存其元數據信息。也就是說:每一個待匹配的URL路徑都會被解析為一個PathContainer實例。
PathContainer雖然是個接口,但我們無需關心其實現,類同于Java 8的java.util.stream.Collector接口使用者無需關心其實現一樣。因為提供了靜態工具方法用于直接生成對應實例。體驗一把:
- @Test
- public void test2() {
- PathContainer pathContainer = PathContainer.parsePath("/api/v1/address", PathContainer.Options.HTTP_PATH);
- System.out.println(pathContainer);
- }
debug模式運行,查看pathContainer對象詳情:
這和解析為PathPattern的結構何其相似(不過這里元素們是通過有序的集合組織起來的)。對比看來,拍腦袋應該能夠猜到何新版的匹配效率會更高了吧。
補充說明:
- value和valueToMatch的區別:value是原值,valueToMatch是(處理過的,比如已解碼的)最終參與匹配的值
- parameters代表路徑參數。若希望它有值只需使用;號分隔填值即可。如:/api;abc/v1,此參數一般都用不著
因為Http中是允許這樣攜帶參數的,但是目錄(.形式)就不能這么寫啦
PathPattern:路徑解析的模式
表示解析路徑的模式。包括用于快速匹配的路徑元素鏈,并累積用于快速比較模式的計算狀態。它是直接面向使用者進行匹配邏輯的最重要API,完成match操作。
PathPattern所在包是org.springframework.web.util.pattern.PathPattern,位于spring-web模塊,專為web(含webmvc和webflux)設計的全新一套路徑匹配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),默認是true
- private final boolean matchOptionalTrailingSeparator;
- // 是否對大小寫敏感,默認是true
- private final boolean caseSensitive;
- // 鏈式結構:表示URL的每一部分元素
- @Nullable
- private final PathElement head;
- private int capturedVariableCount;
- private int normalizedLength;
- private boolean endsWithSeparatorWildcard = false;
- private int score;
- private boolean catchAll = false;
- }
以上屬性是直接讀取,下面這些個是計算出來的,比較特殊就特別照顧下:
- capturedVariableCount:在這個模式中捕獲的變量總數。也就是{xxx}或者正則捕獲的總數嘍
- normalizedLength:通配符批到的變量長度的總和(關于長度的計算有個約定:如?是1,字面量就是字符串長度),這個變量對提升匹配速度有幫助
- endsWithSeparatorWildcard:標記該模式是否以隔離符或者通配符*結尾
- score:分數用于快速比較該模式。不同的模式組件被賦予不同的權重。分數越低越具體,如:捕獲到的變量分數值為1,通配符值是100
- catchAll:該pattern是否以**或者{*xxx}結尾
score、catchAll等標記用于加速匹配的速度,具體體現在PathPattern.SPECIFICITY_COMPARATOR這個比較器上,這是PathPattern速度比AntPathMatcher快的根因之一
值得注意的是:所有屬性均不提供public的set方法,也就是說PathPattern實例一旦創建就是只讀(不可變)實例了。
快速創建缺省的實例
上面了解到,PathPattern的構造器不是public的,所以有且僅能通過PathPatternParser創建其實例。然而,為快速滿足絕大多數場景,Spring還提供了一種快速創建缺省的PathPattern實例的方式:
PathPatternParser提供一個全局共享的、只讀的實例用于快速創建缺省的PathPattern實例,類似于實例工廠的作用。畢竟絕大部分場景下用PathPattern的缺省屬性即可,因此有了它著實方便不少。
注意:雖然該PathPatternParser實例是全局共享只有1個,但是,創建出來的PathPattern可是不同實例哦(基本屬性都一樣而已)
代碼示例
PathPattern的匹配方式和AntPathMatcher基本保持一致:使用的基于Ant風格模式匹配。
但是發現沒,這里不再強調Ant字樣,也許Spring覺得Ant的概念確實已廉頗老矣?不符合它緊跟潮流的身份?
相比于AntPathMatcher,PathPattern主要有兩處地方不一樣:
說明:PathPattern只支持兩種分隔符(/和.),而AntPathMatcher可以隨意指定。雖然這也是不同點,但這一般無傷大雅所以就不單獨列出了
1. 新增{*pathVariable}語法支持
這是PathPattern新增的“語法”,表示匹配余下的path路徑部分并將其賦值給pathVariable變量。
- @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}
在沒有PathPattern之前,雖然也可以通過/**來匹配成功,但卻無法得到匹配到的值,現在可以了!
和**的區別
我們知道/**和/{*pathVariable}都有匹配剩余所有path的“能力”,那它倆到底有什么區別呢?
- /**能匹配成功,但無法獲取到動態成功匹配元素的值
- /{*pathVariable}可認為是/**的加強版:可以獲取到這部分動態匹配成功的值
正所謂一代更比一代強嘛,如是而已。
和**的優先級關系
既然/**和/{*pathVariable}都有匹配剩余path的能力,那么它倆若放在一起,優先級關系是怎樣的呢?
妄自猜測沒有意義,跑個案例一看便知:由于PathPattern實現了比較器接口,因此本例利用SortedSet自動排序即可,排第一的證明優先級越高
- @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}]
測試代碼的細節:故意將/{*pathVariable}先放進set里面而后放/**,但最后還是/**在前。
結論:當二者同時出現(出現沖突)時,/**優先匹配。
2. 禁用中間**語法支持
在上篇文章對AntPathMatcher的詳細分析文章中,我們知道是可以把/**放在整個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
與AntPathMatcher不同,**僅在模式末尾受支持。中間不被允許了,否則實例創建階段就會報錯:
- @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 element
- at org.springframework.web.util.pattern.InternalPathPatternParser.peekDoubleWildcard(InternalPathPatternParser.java:250)
- ...
從報錯中還能看出端倪:不僅**,{*xxx}也是不能放在中間而只能是末尾的
PathPattern這么做的目的是:消除歧義。
那么問題來了,如果就是想匹配中間的任意層級路徑怎么做呢?答:首先這在web環境里有這樣需求的概率極小(PathPattern只適用于web環境),若這依舊是剛需,那就只能蛻化到借助AntPathMatcher來完成嘍。
PathPattern對比AntPathMatcher
二者目前都存在于Spring技術棧內,做著“相同”的事。雖說現在還鮮有同學了解到PathPattern,我認為淘汰掉AntPathMatcher只是時間問題(特指web環境哈),畢竟后浪總歸有上岸的一天。
但不可否認,二者將在較長時間內共處,那么它倆到底有何區別呢?了解一下
出現時間
AntPathMatcher是一個早在2003年(Spring的第一個版本)就已存在的路徑匹配器,而PathPattern是Spring 5新增的,旨在用于替換掉較為“古老”的AntPathMatcher。
功能差異
PathPattern去掉了Ant字樣,但保持了很好的向下兼容性:除了不支持將**寫在path中間之外,其它的匹配規則從行為上均保持和AntPathMatcher一致,并且還新增了強大的{*pathVariable}的支持。
因此在功能上姑且可認為二者是一致的,極特殊情況下的不兼容除外。
性能差異
Spring官方說PathPattern的性能優于AntPathMatcher,我抱著懷疑的態度做了測試,示例代碼和結果如下:
// 匹配的模板:使用一個稍微有點復雜的模板進行測試private static final String pattern = "/api/your?atman/{age}/**";
- // 匹配的模板:使用一個稍微有點復雜的模板進行測試
- 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());
- }
不斷調整循環次數,且各執行三次,將結果繪制成如下表格:
測試機配置為:
循環100000次:
循環1000000次:
循環10000000次:
結論:PathPattern性能比AntPathMatcher優秀。理論上pattern越復雜,PathPattern的優勢越明顯。
最佳實踐
既然路徑匹配器有兩種方案,那必然有最佳實踐。Spring官方對此也是持有態度的:
Web環境
如果是Servlet應用(webmvc),官方推薦PathPattern(只是推薦,但默認的依舊是AntPathMatcher哈),相關代碼體現在PathPattern里:
- // Since: 07.04.2003
- public abstract class AbstractHandlerMapping ... {
- private UrlPathHelper urlPathHelper = new UrlPathHelper();
- private PathMatcher pathMatcher = new AntPathMatcher();
- ...
- @Nullable
- private PathPatternParser patternParser;
- // Since: 5.3
- public void setPatternParser(PathPatternParser patternParser) {
- this.patternParser = patternParser;
- }
- }
注意:setPatternParser()從5.3版本開始才被加入,也就說雖然PathPattern從Spring 5就有了,但直到5.3版本才被加入到webmvc里,且作為可選(默認依舊是AntPathMatcher)。換句話講:在Spring 5.3版本之前,仍舊只能用AntPathMatcher。
在WebMvc里啟用PathPattern
默認情況下,Spring MVC依舊是使用的AntPathMatcher進行路徑匹配的,那如何啟用效率更高的PathPattern呢?
通過上面源碼知道,就是要調用AbstractHandlerMapping的setPatternParser方法嘛,其實Spring為此是預留了擴展點的,只需這么做即可:
- /**
- * 在此處添加備注信息
- *
- * @author YourBatman. <a href=mailto:yourbatman@aliyun.com>Send email to me</a>
- * @site https://yourbatman.cn
- * @date 2021/6/20 18:33
- * @since 0.0.1
- */
- @Configuration(proxyBeanMethods = false)
- public class WebMvcConfiguration implements WebMvcConfigurer {
- @Override
- public void configurePathMatch(PathMatchConfigurer configurer) {
- configurer.setPatternParser(PathPatternParser.defaultInstance);
- }
- }
如果是Reactor應用(webflux),那PathPattern就是唯一解決方案。這體現在org.springframework.web.reactive.handler.AbstractHandlerMapping:
- // Since: 5.0
- public abstract class AbstractHandlerMapping... {
- private final PathPatternParser patternParser;
- ...
- public AbstractHandlerMapping() {
- this.patternParser = new PathPatternParser();
- }
- }
webflux里早已不見AntPathMatcher的蹤影,因為webflux是從Spring 5.0開始的,因此沒有向下兼容的負擔,直接全面擁抱PathPattern了。
結論:PathPattern語法更適合于web應用程序,其使用更方便且執行更高效。
非Web環境
嗯,如果認真“聽課”了的同學就知道:非Web環境依舊有且僅有一種選擇,那便是AntPathMatcher,因為PathPattern是專為Web環境設計,不能用于非Web環境。所以像上面資源加載、包名掃描之類的,底層依舊是交給AntPathMatcher去完成。
說明:由于這類URL的解析絕大多數情況下匹配一次(執行一次)就行,所以微小的性能差異是無所謂的(對API來講收益較大)
可能有小伙伴會說:在Service層,甚至Dao層我也可以正常使用PathPattern對象呀,何解?這個問題就相當于:HttpServletRequest屬于web層專用組件,但你依舊可以將其傳到Service層,甚至Dao層供以使用,在編譯、運行時不會報錯。但你可深入思考下,這么做合適嗎?
舉個生活上的例子:馬桶可以裝在衛生間,也可以安裝在臥室的床旁邊,都能完成大小便功能,但你覺得這么做合適嗎?
Java這門語言對訪問權限的控制設計得還是很優秀的,很多隔離性的問題在編譯器就能搞定。但有很多規范性做法是無法做到強約束的,只能依靠工程師自身水平。這就是經驗,也是區別初級工程師和高級工程師的重要因素。
總結
技術的日新月異,體現在一個個像PathPattern這個更好的API上。
Spring 5早在2017-09就已發布,可能是由于它“設計得過于優秀”,即使大版本的發布也幾乎保持100%向下兼容,使得一般開發者感受不到它的升級。但是,這對框架二次開發者并不可能完全透明,因為二次開發經常會用到其Low-Level的API,比如今天的主角PathPattern就算其中之一,所以說我們要與時俱進呀o(╥﹏╥)o!
Spring 5雖然新增了(更好的)PathPattern,但它不能完全替代掉AntPathMatcher,因為前者專為web設計,所以在web領域是可完全替代掉AntPathMatcher的。但在非web領域內,AntPathMatcher依舊不可替代。
本文轉載自微信公眾號「 BAT的烏托邦」,可以通過以下二維碼關注。轉載本文請聯系 BAT的烏托邦公眾號。