一次Spring無法啟動的問題排查實戰(zhàn)之字節(jié)碼篇
問題背景
有同學(xué)反饋,有一個項目從 kotlin 1.2 升級到 kotlin 1.3 以后 Spring 項目無法啟動,報 java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'xxx' method 錯誤
沒有引入任何其它變量,只更改了 kotlin 的版本,猜測可能是編譯出來的字節(jié)碼不一樣,出問題的函數(shù)如下。
@OptionalAuthAPI @GetMapping("/page") fun?getActivityGameModulePage( ????????@OptionalAuthRes?authRes:?OptionalAuthResDTO, ????????@RequestParam(name?=?"type",?defaultValue?=?"0")?type:?Int?=?0, ????????@RequestParam(name?=?"page",?defaultValue?=?"0")?page:?Int?=?0, ????????@RequestParam(name?=?"pageSize",?defaultValue?=?"30")?pageSize:?Int?=?0 ):?APIResult<Page<ActivityGameModuleRespDTO>>?{ ????return; }
kotlin 處理函數(shù)中 default 值的方法是生成一個靜態(tài)的函數(shù),比如下面的函數(shù)。
class?MyTest1?{ ????private?var?m?=?101 ????fun?foo(x:?Int?=?100,?y:?String?=?"foo",?z:?Double?=?1.0)?{ ????????println(""?+?x?+?y?+?m?+?z) ????} ????fun?bar()?{ ????????foo(101,?"bar") ????????foo(101); ????????foo(); ????} }
生成的部分字節(jié)碼如下,主要看函數(shù)簽名
??public?final?void?foo(int,?java.lang.String,?double); ????descriptor:?(ILjava/lang/String;D)V ????flags:?ACC_PUBLIC,?ACC_FINAL ??public?static?void?foo$default(MyTest1,?int,?java.lang.String,?double,?int,?java.lang.Object); ????descriptor:?(LMyTest1;ILjava/lang/String;DILjava/lang/Object;)V ????flags:?ACC_PUBLIC,?ACC_STATIC,?ACC_SYNTHETIC
通過閱讀字節(jié)碼,人肉翻譯為 java 就是:
public?class?MyTest3?{ ????private?int?m; ????public?void?foo(int?x,?String?y,?double?z)?{ ????????String?str?=?""?+?x?+?y?+?this.m?+?z; ????????System.out.println(str); ????} ????public?void?bar()?{ ????????foo$default(this,?101,?"bar",?0.0D,?4,?null);?//?4?=?b0100 ????????foo$default(this,?101,?null,?0.0D,?6,?null);?//?6?=?b0110 ????????foo$default(this,?0,?null,?0.0D,?7,?null);?//?7?=?b0111 ????} ????public?static?void?foo$default(MyTest3?thisObj,?int?x,?String?y,?double?z,?int?mask,?Object?obj)?{ ????????if?((mask?&?0x01)?!=?0)?{ ????????????x?=?100; ????????} ????????if?((mask?&?0x02)?!=?0)?{ ????????????y?=?"foo"; ????????} ????????if?((mask?&?0x04)?!=?0)?{ ????????????z?=?1.0; ????????} ????????thisObj.foo(x,?y,?z); ????} }
由此可以看到 kotlin 對于默認(rèn)參數(shù)的處理辦法就是用一個 mask,告訴后面的邏輯,特定位置的參數(shù)是否需要使用默認(rèn)值。
回到原 getActivityGameModulePage 方法,這個方法上有兩個注解,kotlin 在編譯以后會新增一個 static 的方法
//?默認(rèn)方法 @OptionalAuthAPI @GetMapping("/page") public?static? APIResult<Page<ActivityGameModuleRespDTO>>? getActivityGameByPage(...)?{ } //?新增方法 @OptionalAuthAPI @GetMapping("/page") public?static? APIResult<Page<ActivityGameModuleRespDTO>>? getActivityGameByPage$default(...)?{ }
咦,這樣 Spring 在掃描的時候,不會出問題嗎?兩個方法都標(biāo)注了 @GetMapping("/page")
要處理,理論上不論是 Koltin1.2 還是 1.3 在處理的時候都會出問題才對。
遇事不決,上字節(jié)碼
kotlin 1.2 編譯出來的字節(jié)碼
public?static?APIResult?getActivityGameByPage$default(); ???flags:?ACC_PUBLIC,?ACC_STATIC,?ACC_BRIDGE,?ACC_SYNTHETIC
kotlin 1.3 編譯出來的字節(jié)碼
public?static?APIResult?getActivityGameByPage$default(); ????flags:?ACC_PUBLIC,?ACC_STATIC,?ACC_SYNTHETIC
經(jīng)過仔細(xì)對比,發(fā)現(xiàn)只有在方法的 flags 上有一些區(qū)別,1.3 的字節(jié)碼少了 ACC_BRIDGE。
眾所周不知,ACC_BRIDGE 是一種為了實現(xiàn)某些語言特性而由編譯器自動生成的方法。除了 Kotlin,Java 自己本身在實現(xiàn)類型擦除等場景下也會用到 ACC_BRIDGE,具體我這里就不展開了,大家可以去試一下。
是不是就是這個導(dǎo)致的問題呢?
我們來看我們當(dāng)前用的 Spring 版本是如何處理方法掃描的,通過調(diào)試我們進(jìn)入到了這個方法
可以看到 Spring 4.3.10 版本判斷是否是用戶自己寫的方法時的邏輯是方法不是 bridge 且方法不處于 Object 類中,因此現(xiàn)在情況就很明朗了。
在 kotlin1.2 中,因為編譯出的 getActivityGameByPage$default()
包含了 bridge,在 Spring 掃描的過程中就會被忽略掉,而 kotlin1.3 中,因為方法簽名不包含 bridge,所以被當(dāng)做了用戶自己書寫的方法,參與到掃描中,這樣 controller 就沖突了,所以報了 Ambiguous mapping
錯誤。
如何解決
那這么嚴(yán)重的問題,難道 kotlin 不解決嗎?是的,kotlin 不解決,那就只能上層框架兼容了,Spring 在后續(xù)的版本中做了修復(fù),增加了對 ACC_SYNTHETIC
的判斷,修改的地方如下:
這樣,在新版本的 Spring 中,就不存在這個問題了,升級以后果然發(fā)現(xiàn)解決了問題。
Kotlin 編譯器源碼探秘
有了實驗的結(jié)果,反過來尋找原因就很簡單了,找到 kotlin 1.2 的源碼,然后翻一翻源碼,馬上找到了對應(yīng)的邏輯。在 4 年前的一個 commit 中,有一個伙計干掉了 ACC_BRIDGE
標(biāo)記。
對應(yīng)的源碼修改如下
Kotlin 新版邏輯
有小伙伴又試了 kotlin 1.4+,發(fā)現(xiàn)問題也消失了,這又引起了我的興趣,看了一下字節(jié)碼,發(fā)現(xiàn)新版本的 getActivityGameByPage$default()
中,已經(jīng)沒有了注解,這下從源頭解決了問題。
這下真相大白了,準(zhǔn)備落班。
小結(jié)
學(xué)一點字節(jié)碼對于我們解決 JVM、中間件的一些問題是很有幫助的,這也是我探究字節(jié)碼的動力來源,這個,又解決了一個問題吧。
到此這篇關(guān)于一次Spring無法啟動的問題排查實戰(zhàn)之字節(jié)碼篇的文章就介紹到這了,更多相關(guān)Spring無法啟動問題排查內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于jmeter實現(xiàn)跨線程組傳遞token過程圖解
這篇文章主要介紹了基于jmeter實現(xiàn)跨線程組傳遞token,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-04-04將Java程序與數(shù)據(jù)庫進(jìn)行連接的操作方法
這篇文章主要介紹了將Java程序與數(shù)據(jù)庫進(jìn)行連接的操作方法,是Java入門學(xué)習(xí)中的基礎(chǔ)知識,需要的朋友可以參考下2015-10-10解決Idea運行junit測試時報Error:[3,17]?程序包org.junit不存在的問題
這篇文章主要介紹了Idea運行junit測試時報Error:[3,17]?程序包org.junit不存在解決方法,本文給大家分享兩種解決辦法,需要的朋友可以參考下2023-03-03Java不借助第三變量實現(xiàn)兩數(shù)交換的示例
本文主要介紹了Java不借助第三變量實現(xiàn)兩數(shù)交換的示例,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-02-02