一 起因
之前寫一個(gè)小 demo,慣例使用自己歸納起來的方式集成 Swagger 來做 api 調(diào)試,然后啟動(dòng)時(shí)報(bào)了個(gè)錯(cuò):
org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException
at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:181) ~[spring-context-5.3.14.jar:5.3.14]
at org.springframework.context.support.DefaultLifecycleProcessor.access$200(DefaultLifecycleProcessor.java:54) ~[spring-context-5.3.14.jar:5.3.14]
at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:356) ~[spring-context-5.3.14.jar:5.3.14]
at java.base/java.lang.Iterable.forEach(Iterable.java:75) ~[na:na]
...
集成方式如下:
簡(jiǎn)書地址:http://www.itdecent.cn/p/55cbce0ecb16
明明之前一直都是好的,為什么這次就突然報(bào)錯(cuò)了呢?
本著遇事不決找版本的思路看了看, SpringBoot 2.6.2,會(huì)不會(huì)是版本的問題?改成 2.5.8 果然一切正常,真相大白,就是我們的老朋友,版本的問題。
那么為什么升到 2.6.2 就不行了呢?版本的具體什么問題導(dǎo)致了錯(cuò)誤的出現(xiàn)?
這時(shí)我想到了之前在 b 站看到的一個(gè) gradle 大佬給 maven 修 bug 的視頻:https://www.bilibili.com/video/BV1vt411g7F5
沒錯(cuò),這個(gè)通過找不同從而快速定位問題的方法,我稱之為對(duì)比調(diào)試法 ,對(duì)版本問題應(yīng)該是特效工具了。
這就來操作一波(其實(shí)早前就操作過了,現(xiàn)在回來記錄一下而已)。
二 對(duì)比調(diào)試
- 直接打開兩個(gè) IDE 窗口分別運(yùn)行 2.5.8 和 2.6.2 版本的程序(注意端口不要沖突),啟動(dòng)觀察結(jié)果,馬上就能確定調(diào)試入口:

-
定位到
WebMvcPatternsRequestConditionWrapper.getPatterns()方法,打個(gè)斷點(diǎn)重新啟動(dòng):圖 2 -
找到報(bào) NPE 的地方了,是這個(gè) Wrapper 里的成員變量 condition,上下翻動(dòng)可知只有構(gòu)造方法能創(chuàng)建它,那么結(jié)合左邊的調(diào)用棧來看看誰創(chuàng)建了這個(gè) Wrapper :
圖 3從右邊窗口可知有兩個(gè) Handler 創(chuàng)建了這個(gè) Wrapper,具體是哪個(gè)呢?暫時(shí)不得而知,那么不妨先看看左邊的調(diào)用棧,可以得知錯(cuò)誤出現(xiàn)的大致上下文是:在對(duì) RequestHandler 進(jìn)行排序的時(shí)候根據(jù) Condition 來排,結(jié)果這個(gè) condition 是 null 的,所以就報(bào) NPE 了。
-
那么是誰調(diào)用了排序呢,跳過 java.util 的步驟,從調(diào)用棧上往下找:
圖 4 -
從以上的分析不難看出, 上圖中的
toRequestHandler()這個(gè)方法有很大的嫌疑,打個(gè)斷點(diǎn)對(duì)比一下:圖 5簡(jiǎn)單分析可以看出,這個(gè)
WebMvcRequestHandlerProvider里的私有屬性handlerMapping中的數(shù)據(jù)就已經(jīng)不同了。那依舊老樣子,追蹤數(shù)據(jù)來源:這個(gè)requestHandler()方法又由誰調(diào)用的呢?是DocumentationContextBuilder.withDefault(),如下。 -
老樣子打上斷點(diǎn)重啟。從下圖可知,這個(gè)
handlerProvider成了頭號(hào)嫌疑,而它是AbstractDocumentationPluginsBootstrapper的一個(gè)屬性。從開頭的報(bào)錯(cuò)信息到這里可能鏈路有點(diǎn)長(zhǎng)了,重新梳理一下就是:這個(gè) provider 提供的 handlerMapping 中的 mappingRegistry 里,名為 registry 的鍵值對(duì)中,key(類型是 RequestMappingInfo)里面的 patternsCondition 有問題,SpringBoot 2.6 環(huán)境中它為 null,2.5 則有值。圖 6 -
那是不是應(yīng)該看看這個(gè) Bootstrapper 的創(chuàng)建邏輯在兩個(gè)版本之間有什么不同呢?不錯(cuò),繼續(xù)翻調(diào)用棧,buildContext, 再翻,可以來到它的子類
DocumentationPluginsBootstrapper的start方法。沒錯(cuò),它實(shí)現(xiàn)了 Spring 的SmartLifecycle接口,所以它在 Spring 加載并初始化完 bean 后執(zhí)行 start 中的邏輯(當(dāng)然這是個(gè)題外話)。仔細(xì)觀察,我們要找的圖 7handlerProvider在這個(gè)類的構(gòu)造器中被注入。如你所見,這個(gè)類加了@Component注解,是個(gè)被 Spring 托管的類。所以根據(jù)注入的基本原理,可以到這個(gè) provider 的類中看看。 -
如下圖,這個(gè) Provider 是個(gè)接口,慣例找到它的實(shí)現(xiàn):
圖 8這回又回到了這里,發(fā)現(xiàn) handlerMappings 是由注入生成的(如果夠敏感,第一次到這里就應(yīng)該能發(fā)現(xiàn))。記得剛才梳理的結(jié)果嗎?下一步就是
handlerMappings里的mappingRegistry里的registry里的 key ,忘記的可以往上翻翻第 6 步。 -
handlerMappings所屬類是RequestMappingInfoHandlerMapping,mappingRegistry不在此類而在它的抽象父類AbstractHandlerMethodMapping中, 是一個(gè)內(nèi)部類的實(shí)現(xiàn)。圖 9這個(gè)方法叫
register,大概是往 mapping 中注冊(cè)訪問路徑和訪問規(guī)則的功能,那進(jìn)一步追蹤 mapping 的由來。 -
可以找到
register的兩個(gè)調(diào)用方:AbstractHandlerMethodMapping.registerMapping()與AbstractHandlerMethodMapping.registerHandlerMethod(),通過打斷點(diǎn)大法可得知走了后者的方法。然后找到這個(gè)方法的真正調(diào)用者: ?圖10那段注釋的意思大致就是要使用
getBuilderConfiguration()的值去設(shè)置RequestMappingInfo里的某個(gè)東西, 用來匹配這個(gè) info 里設(shè)置HandlerMapping的邏輯,這非常重要,例如對(duì)于使用基本匹配的 PathPattern 或 PathMatcher 來說非常重要。好像看不出和我們的目標(biāo)有什么聯(lián)系?沒關(guān)系,接著往下調(diào)試吧。 -
可以看到還有一個(gè)方法調(diào)用了這個(gè)
registerHandlerMethod,而且是通過 lambda 表達(dá)式的方式調(diào)用的:圖 11通過斷點(diǎn)的方式可以知道這個(gè) mapping 里的 patternsCondition 是空的,繼續(xù)找 mapping 的由來。
-
下面這段代碼比較復(fù)雜,不過只需要理清楚數(shù)據(jù)來源就行了,目的是什么不用管。
圖 12追蹤到 inspect 方法, 發(fā)現(xiàn)是個(gè)函數(shù)式接口,其實(shí)真正用的是上一步傳進(jìn)來的 lambda 表達(dá)式。其核心是
getMappingForMethod(method, userType)這個(gè)方法。 -
進(jìn)入方法,關(guān)鍵分支打上斷點(diǎn):
圖 13對(duì)比左右兩邊,發(fā)現(xiàn) create 出來的 info(就是我們要找的 mapping)里面的值不一樣。左邊的
pathPatternsCondition有值而patternsCondition為空, 而右邊的正好相反。 -
一路追蹤,到達(dá)
createRequestMappingInfo(requestMapping, codition)這個(gè)方法,直到這里,兩邊的入?yún)⒍际窍嗤摹?/p>圖 14但是,通過對(duì)比發(fā)現(xiàn),config 里的值不同。左邊的是 SpringBoot 2.6.2,其中
patternParser是有值的,值為PathPatternParser,而右邊 SpringBoot 2.5.8 的版本里有值的是pathMatcher,其值為AntPathMatcher。通過萬能的搜索引擎(或者經(jīng)驗(yàn)豐富的同學(xué)已經(jīng)知道了)可以得知,這是 SpringBoot 解析與匹配路徑的策略。那么到了這一步,其實(shí)我們已經(jīng)找到那個(gè)報(bào)錯(cuò)的問題的根本原因了。 -
出現(xiàn)問題的根本原因就是:SpringBoot 在 2.6 中改掉了路徑匹配策略。這點(diǎn)可以通過翻 SpringBoot 項(xiàng)目的 Release Note 得知:
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.6-Release-Notes
如下圖:
圖 15解決方案也很簡(jiǎn)單,如果項(xiàng)目求穩(wěn),不需要 2.6 的新特性,直接降到舊版本的 SpringBoot 使用;如果想嘗鮮,但是對(duì)匹配策略不感冒的,可以通過在 Spring 配置文件中設(shè)置
spring.mvc.pathmatch.matching-strategy為ant-path-matcher即可,這點(diǎn)在官方文件中也提到了。
三 調(diào)試心得
- 對(duì)比調(diào)試法對(duì)版本出現(xiàn)差異的問題調(diào)試起來有很強(qiáng)的針對(duì)性,可以作為特效工具使用;
- 哪怕是對(duì)于不清楚運(yùn)作邏輯的代碼,只要咬緊線索,深入挖掘,還是能夠找到問題所在的;
- 調(diào)試要有“不擇手段”的精神;
- 平時(shí)多了解熱門工具的版本及其新特性,多關(guān)注官方信息,可以簡(jiǎn)化很多不必要的開銷。(比如事先知道 SpringBoot 2.6 的改動(dòng),那么出現(xiàn)問題的時(shí)候就會(huì)多一個(gè)心眼,明白大概是哪個(gè)更新導(dǎo)致了錯(cuò)誤的出現(xiàn))













