一次Spring無法啟動(dòng)的問題排查實(shí)戰(zhàn)之字節(jié)碼篇
問題背景
有同學(xué)反饋,有一個(gè)項(xiàng)目從 kotlin 1.2 升級(jí)到 kotlin 1.3 以后 Spring 項(xiàng)目無法啟動(dòng),報(bào) java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'xxx' method 錯(cuò)誤

沒有引入任何其它變量,只更改了 kotlin 的版本,猜測(cè)可能是編譯出來的字節(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 值的方法是生成一個(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
通過閱讀字節(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ì)出問題嗎??jī)蓚€(gè)方法都標(biāo)注了 @GetMapping("/page") 要處理,理論上不論是 Koltin1.2 還是 1.3 在處理的時(shí)候都會(huì)出問題才對(duì)。
遇事不決,上字節(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ì)對(duì)比,發(fā)現(xiàn)只有在方法的 flags 上有一些區(qū)別,1.3 的字節(jié)碼少了 ACC_BRIDGE。
眾所周不知,ACC_BRIDGE 是一種為了實(shí)現(xiàn)某些語(yǔ)言特性而由編譯器自動(dòng)生成的方法。除了 Kotlin,Java 自己本身在實(shí)現(xiàn)類型擦除等場(chǎng)景下也會(huì)用到 ACC_BRIDGE,具體我這里就不展開了,大家可以去試一下。
是不是就是這個(gè)導(dǎo)致的問題呢?
我們來看我們當(dāng)前用的 Spring 版本是如何處理方法掃描的,通過調(diào)試我們進(jìn)入到了這個(gè)方法

可以看到 Spring 4.3.10 版本判斷是否是用戶自己寫的方法時(shí)的邏輯是方法不是 bridge 且方法不處于 Object 類中,因此現(xiàn)在情況就很明朗了。
在 kotlin1.2 中,因?yàn)榫幾g出的 getActivityGameByPage$default() 包含了 bridge,在 Spring 掃描的過程中就會(huì)被忽略掉,而 kotlin1.3 中,因?yàn)榉椒ê灻话?bridge,所以被當(dāng)做了用戶自己書寫的方法,參與到掃描中,這樣 controller 就沖突了,所以報(bào)了 Ambiguous mapping 錯(cuò)誤。
如何解決
那這么嚴(yán)重的問題,難道 kotlin 不解決嗎?是的,kotlin 不解決,那就只能上層框架兼容了,Spring 在后續(xù)的版本中做了修復(fù),增加了對(duì) ACC_SYNTHETIC 的判斷,修改的地方如下:

這樣,在新版本的 Spring 中,就不存在這個(gè)問題了,升級(jí)以后果然發(fā)現(xiàn)解決了問題。
Kotlin 編譯器源碼探秘
有了實(shí)驗(yàn)的結(jié)果,反過來尋找原因就很簡(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)問題也消失了,這又引起了我的興趣,看了一下字節(jié)碼,發(fā)現(xiàn)新版本的 getActivityGameByPage$default() 中,已經(jīng)沒有了注解,這下從源頭解決了問題。
這下真相大白了,準(zhǔn)備落班。
小結(jié)
學(xué)一點(diǎn)字節(jié)碼對(duì)于我們解決 JVM、中間件的一些問題是很有幫助的,這也是我探究字節(jié)碼的動(dòng)力來源,這個(gè),又解決了一個(gè)問題吧。
到此這篇關(guān)于一次Spring無法啟動(dòng)的問題排查實(shí)戰(zhàn)之字節(jié)碼篇的文章就介紹到這了,更多相關(guān)Spring無法啟動(dòng)問題排查內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
spring?retry實(shí)現(xiàn)方法請(qǐng)求重試的使用步驟
這篇文章主要介紹了spring?retry實(shí)現(xiàn)方法請(qǐng)求重試及使用步驟,本文分步驟通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07
SpringBoot @ComponentScan掃描的局限性方式
文章總結(jié):SpringBoot的@ComponentScan注解在掃描組件時(shí)存在局限性,只能掃描指定的包及其子包,無法掃描@SpringBootApplication注解自動(dòng)配置的組件,使用@SpringBootApplication注解可以解決這一問題,它集成了@Configuration、@EnableAutoConfiguration2025-01-01
Java GUI進(jìn)階之流式布局管理器FlowLayout專項(xiàng)精講
FlowLayout-流式布局管理器,按水平方向依次排列放置組件,排滿一行,換下一行繼續(xù)排列。排列方向(左到右 或 右到左)取決于容器的componentOrientation屬性2022-04-04
Java中CyclicBarrier的理解與應(yīng)用詳解
這篇文章主要介紹了Java中CyclicBarrier的理解與應(yīng)用詳解,CyclicBarrier類是JUC框架中的工具類,也是一個(gè)同步輔助裝置:允許多個(gè)線程去等待直到全部線程抵達(dá)了公共的柵欄點(diǎn),需要的朋友可以參考下2023-12-12
SpringBoot結(jié)合JSR303對(duì)前端數(shù)據(jù)進(jìn)行校驗(yàn)的示例代碼
這篇文章主要介紹了SpringBoot結(jié)合JSR303對(duì)前端數(shù)據(jù)進(jìn)行校驗(yàn)的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09
springboot在idea下debug調(diào)試熱部署問題
這篇文章主要介紹了springboot在idea下debug調(diào)試熱部署問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02
java必學(xué)必會(huì)之方法的重載(overload)
java必學(xué)必會(huì)之方法的重載,介紹了方法的重載、構(gòu)造方法的重載,想要學(xué)好java方法的重載的朋友一定要好好閱讀這篇文章2015-12-12
解決Java導(dǎo)入excel大量數(shù)據(jù)出現(xiàn)內(nèi)存溢出的問題
今天小編就為大家分享一篇解決Java導(dǎo)入excel大量數(shù)據(jù)出現(xiàn)內(nèi)存溢出的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-06-06

