對(duì)比調(diào)試法解決SpringBoot版本問題

一 起因

之前寫一個(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]
...

集成方式如下:

個(gè)人博客: https://lookoutldz.top/archives/springboot%E9%9B%86%E6%88%90%E6%96%B0%E7%89%88swagger2starter%E6%96%B9%E5%BC%8F

簡(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)試

  1. 直接打開兩個(gè) IDE 窗口分別運(yùn)行 2.5.8 和 2.6.2 版本的程序(注意端口不要沖突),啟動(dòng)觀察結(jié)果,馬上就能確定調(diào)試入口:
圖 1
  1. 定位到 WebMvcPatternsRequestConditionWrapper.getPatterns() 方法,打個(gè)斷點(diǎn)重新啟動(dòng):

    圖 2
  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 了。

  3. 那么是誰調(diào)用了排序呢,跳過 java.util 的步驟,從調(diào)用棧上往下找:

    圖 4
  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() ,如下。

  5. 老樣子打上斷點(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
  6. 那是不是應(yīng)該看看這個(gè) Bootstrapper 的創(chuàng)建邏輯在兩個(gè)版本之間有什么不同呢?不錯(cuò),繼續(xù)翻調(diào)用棧,buildContext, 再翻,可以來到它的子類 DocumentationPluginsBootstrapperstart 方法。沒錯(cuò),它實(shí)現(xiàn)了 Spring 的 SmartLifecycle 接口,所以它在 Spring 加載并初始化完 bean 后執(zhí)行 start 中的邏輯(當(dāng)然這是個(gè)題外話)。

    圖 7
    仔細(xì)觀察,我們要找的 handlerProvider 在這個(gè)類的構(gòu)造器中被注入。如你所見,這個(gè)類加了 @Component 注解,是個(gè)被 Spring 托管的類。所以根據(jù)注入的基本原理,可以到這個(gè) provider 的類中看看。

  7. 如下圖,這個(gè) Provider 是個(gè)接口,慣例找到它的實(shí)現(xiàn):

    圖 8

    這回又回到了這里,發(fā)現(xiàn) handlerMappings 是由注入生成的(如果夠敏感,第一次到這里就應(yīng)該能發(fā)現(xiàn))。記得剛才梳理的結(jié)果嗎?下一步就是 handlerMappings 里的 mappingRegistry 里的 registry 里的 key ,忘記的可以往上翻翻第 6 步。

  8. handlerMappings 所屬類是 RequestMappingInfoHandlerMappingmappingRegistry 不在此類而在它的抽象父類 AbstractHandlerMethodMapping中, 是一個(gè)內(nèi)部類的實(shí)現(xiàn)。

    圖 9

    這個(gè)方法叫 register ,大概是往 mapping 中注冊(cè)訪問路徑和訪問規(guī)則的功能,那進(jìn)一步追蹤 mapping 的由來。

  9. 可以找到 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)試吧。

  10. 可以看到還有一個(gè)方法調(diào)用了這個(gè) registerHandlerMethod ,而且是通過 lambda 表達(dá)式的方式調(diào)用的:

    圖 11

    通過斷點(diǎn)的方式可以知道這個(gè) mapping 里的 patternsCondition 是空的,繼續(xù)找 mapping 的由來。

  11. 下面這段代碼比較復(fù)雜,不過只需要理清楚數(shù)據(jù)來源就行了,目的是什么不用管。

    圖 12

    追蹤到 inspect 方法, 發(fā)現(xiàn)是個(gè)函數(shù)式接口,其實(shí)真正用的是上一步傳進(jìn)來的 lambda 表達(dá)式。其核心是 getMappingForMethod(method, userType) 這個(gè)方法。

  12. 進(jìn)入方法,關(guān)鍵分支打上斷點(diǎn):

    圖 13

    對(duì)比左右兩邊,發(fā)現(xiàn) create 出來的 info(就是我們要找的 mapping)里面的值不一樣。左邊的 pathPatternsCondition 有值而 patternsCondition 為空, 而右邊的正好相反。

  13. 一路追蹤,到達(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ò)的問題的根本原因了。

  14. 出現(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-strategyant-path-matcher 即可,這點(diǎn)在官方文件中也提到了。

三 調(diào)試心得

  1. 對(duì)比調(diào)試法對(duì)版本出現(xiàn)差異的問題調(diào)試起來有很強(qiáng)的針對(duì)性,可以作為特效工具使用;
  2. 哪怕是對(duì)于不清楚運(yùn)作邏輯的代碼,只要咬緊線索,深入挖掘,還是能夠找到問題所在的;
  3. 調(diào)試要有“不擇手段”的精神;
  4. 平時(shí)多了解熱門工具的版本及其新特性,多關(guān)注官方信息,可以簡(jiǎn)化很多不必要的開銷。(比如事先知道 SpringBoot 2.6 的改動(dòng),那么出現(xiàn)問題的時(shí)候就會(huì)多一個(gè)心眼,明白大概是哪個(gè)更新導(dǎo)致了錯(cuò)誤的出現(xiàn))
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容