一次Spring無(wú)法啟動(dòng)的問(wèn)題排查實(shí)戰(zhàn)之字節(jié)碼篇
問(wèn)題背景
有同學(xué)反饋,有一個(gè)項(xiàng)目從 kotlin 1.2 升級(jí)到 kotlin 1.3 以后 Spring 項(xiàng)目無(wú)法啟動(dòng),報(bào) java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'xxx' method 錯(cuò)誤
沒(méi)有引入任何其它變量,只更改了 kotlin 的版本,猜測(cè)可能是編譯出來(lái)的字節(jié)碼不一樣,出問(wèn)題的函數(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 值的方法是生成一個(gè)靜態(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
通過(guò)閱讀字節(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 對(duì)于默認(rèn)參數(shù)的處理辦法就是用一個(gè) mask,告訴后面的邏輯,特定位置的參數(shù)是否需要使用默認(rèn)值。
回到原 getActivityGameModulePage 方法,這個(gè)方法上有兩個(gè)注解,kotlin 在編譯以后會(huì)新增一個(gè) static 的方法
//?默認(rèn)方法 @OptionalAuthAPI @GetMapping("/page") public?static? APIResult<Page<ActivityGameModuleRespDTO>>? getActivityGameByPage(...)?{ } //?新增方法 @OptionalAuthAPI @GetMapping("/page") public?static? APIResult<Page<ActivityGameModuleRespDTO>>? getActivityGameByPage$default(...)?{ }
咦,這樣 Spring 在掃描的時(shí)候,不會(huì)出問(wèn)題嗎??jī)蓚€(gè)方法都標(biāo)注了 @GetMapping("/page")
要處理,理論上不論是 Koltin1.2 還是 1.3 在處理的時(shí)候都會(huì)出問(wèn)題才對(duì)。
遇事不決,上字節(jié)碼
kotlin 1.2 編譯出來(lái)的字節(jié)碼
public?static?APIResult?getActivityGameByPage$default(); ???flags:?ACC_PUBLIC,?ACC_STATIC,?ACC_BRIDGE,?ACC_SYNTHETIC
kotlin 1.3 編譯出來(lái)的字節(jié)碼
public?static?APIResult?getActivityGameByPage$default(); ????flags:?ACC_PUBLIC,?ACC_STATIC,?ACC_SYNTHETIC
經(jīng)過(guò)仔細(xì)對(duì)比,發(fā)現(xiàn)只有在方法的 flags 上有一些區(qū)別,1.3 的字節(jié)碼少了 ACC_BRIDGE。
眾所周不知,ACC_BRIDGE 是一種為了實(shí)現(xiàn)某些語(yǔ)言特性而由編譯器自動(dòng)生成的方法。除了 Kotlin,Java 自己本身在實(shí)現(xiàn)類(lèi)型擦除等場(chǎng)景下也會(huì)用到 ACC_BRIDGE,具體我這里就不展開(kāi)了,大家可以去試一下。
是不是就是這個(gè)導(dǎo)致的問(wèn)題呢?
我們來(lái)看我們當(dāng)前用的 Spring 版本是如何處理方法掃描的,通過(guò)調(diào)試我們進(jìn)入到了這個(gè)方法
可以看到 Spring 4.3.10 版本判斷是否是用戶自己寫(xiě)的方法時(shí)的邏輯是方法不是 bridge 且方法不處于 Object 類(lèi)中,因此現(xiàn)在情況就很明朗了。
在 kotlin1.2 中,因?yàn)榫幾g出的 getActivityGameByPage$default()
包含了 bridge,在 Spring 掃描的過(guò)程中就會(huì)被忽略掉,而 kotlin1.3 中,因?yàn)榉椒ê灻话?bridge,所以被當(dāng)做了用戶自己書(shū)寫(xiě)的方法,參與到掃描中,這樣 controller 就沖突了,所以報(bào)了 Ambiguous mapping
錯(cuò)誤。
如何解決
那這么嚴(yán)重的問(wèn)題,難道 kotlin 不解決嗎?是的,kotlin 不解決,那就只能上層框架兼容了,Spring 在后續(xù)的版本中做了修復(fù),增加了對(duì) ACC_SYNTHETIC
的判斷,修改的地方如下:
這樣,在新版本的 Spring 中,就不存在這個(gè)問(wèn)題了,升級(jí)以后果然發(fā)現(xiàn)解決了問(wèn)題。
Kotlin 編譯器源碼探秘
有了實(shí)驗(yàn)的結(jié)果,反過(guò)來(lái)尋找原因就很簡(jiǎn)單了,找到 kotlin 1.2 的源碼,然后翻一翻源碼,馬上找到了對(duì)應(yīng)的邏輯。在 4 年前的一個(gè) commit 中,有一個(gè)伙計(jì)干掉了 ACC_BRIDGE
標(biāo)記。
對(duì)應(yīng)的源碼修改如下
Kotlin 新版邏輯
有小伙伴又試了 kotlin 1.4+,發(fā)現(xiàn)問(wèn)題也消失了,這又引起了我的興趣,看了一下字節(jié)碼,發(fā)現(xiàn)新版本的 getActivityGameByPage$default()
中,已經(jīng)沒(méi)有了注解,這下從源頭解決了問(wèn)題。
這下真相大白了,準(zhǔn)備落班。
小結(jié)
學(xué)一點(diǎn)字節(jié)碼對(duì)于我們解決 JVM、中間件的一些問(wèn)題是很有幫助的,這也是我探究字節(jié)碼的動(dòng)力來(lái)源,這個(gè),又解決了一個(gè)問(wèn)題吧。
到此這篇關(guān)于一次Spring無(wú)法啟動(dòng)的問(wèn)題排查實(shí)戰(zhàn)之字節(jié)碼篇的文章就介紹到這了,更多相關(guān)Spring無(wú)法啟動(dòng)問(wèn)題排查內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
jfinal添加jcaptcha驗(yàn)證碼實(shí)現(xiàn)方法
這篇文章主要介紹了jfinal的jcaptcha驗(yàn)證碼實(shí)現(xiàn)方法,大家參考使用吧2014-01-01基于jmeter實(shí)現(xiàn)跨線程組傳遞token過(guò)程圖解
這篇文章主要介紹了基于jmeter實(shí)現(xiàn)跨線程組傳遞token,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-04-04java如何寫(xiě)接口給別人調(diào)用的示例代碼
這篇文章主要介紹了java如何寫(xiě)接口給別人調(diào)用的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09將Java程序與數(shù)據(jù)庫(kù)進(jìn)行連接的操作方法
這篇文章主要介紹了將Java程序與數(shù)據(jù)庫(kù)進(jìn)行連接的操作方法,是Java入門(mén)學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-10-10解決Idea運(yùn)行junit測(cè)試時(shí)報(bào)Error:[3,17]?程序包org.junit不存在的問(wèn)題
這篇文章主要介紹了Idea運(yùn)行junit測(cè)試時(shí)報(bào)Error:[3,17]?程序包org.junit不存在解決方法,本文給大家分享兩種解決辦法,需要的朋友可以參考下2023-03-03Java不借助第三變量實(shí)現(xiàn)兩數(shù)交換的示例
本文主要介紹了Java不借助第三變量實(shí)現(xiàn)兩數(shù)交換的示例,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02java中的類(lèi)型擦除type?erasure示例詳解
泛型是java從JDK?5開(kāi)始引入的新特性,泛型的引入可以讓我們?cè)诖a編譯的時(shí)候就強(qiáng)制檢查傳入的類(lèi)型,從而提升了程序的健壯度,泛型可以用在類(lèi)和接口上,在集合類(lèi)中非常常見(jiàn),本文將會(huì)講解泛型導(dǎo)致的類(lèi)型擦除2023-09-09JDBC連接Mysql的5種方式實(shí)例總結(jié)
JDBC是Java DataBase Connectivity技術(shù)的簡(jiǎn)稱,是一種可用于執(zhí)行 SQL語(yǔ)句的Java API,下面這篇文章主要給大家介紹了關(guān)于JDBC連接Mysql的5種方式,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-04-04