拜託,別在 agent 中依賴 fastjson 了
一、背景
最近因爲增加了一個在 agent 中上報異常的功能,agent 爲了在 http 請求時方便把對象轉換爲 json 格式,增加了一個 fastjson 的依賴,結果搞出來各種問題。
環境:
-
JDK 1.8
-
SpringBoot 2.0.0.RELEASE
-
skywalking agent 8.14.0
二、初現問題
2.1 初步定位
有同事反饋應用在本地能啓動,但是到了測試環境(帶 agent 啓動)就起不來,報錯如下:
首先還是要確認下是不是應用的依賴衝突問題,GenericHttpMessageConverter
這個類是在 spring-web 這個包下面的, 因爲本地打包環境和測試環境有可能不一致,需要確認最終部署到測試環境的包裏是否包含了 spring-web 包。經確認包裏有 spring-web,排除這個可能。
然後懷疑是 agent 和應用的依賴衝突,臨時讓這個應用的 agent 下線後重新部署,發現能正常啓動,基本確認是 agent 帶來的問題。
2.2 進一步排查
爲了方便定位問題,我把發現問題時應用部署的包下載到本地,並在本地掛載 agent 啓動,問題重現,報錯和測試環境一致。至此我就可以在本地 debug 了。
順便說一下,我一開始用 idea 啓動應用(掛載 agent)是沒問題的,至於爲什麼沒問題下面會說到。
本地我在java.net.URLClassLoader#findClass
方法的入口處打了一個條件斷點 (類名爲GenericHttpMessageConverter
的纔會進來),啓動應用後一會兒進入斷點。
idea 這個工具就是好用,從 debug 界面一下子就能看出來,這個 findClass 是調用了 3 次,並且能看到每一次 findClass 是加載的哪個類:
從上面的圖的最後一行也能看出來,這個類加載最開始的觸發是在內部的一個二方庫的類WebAutoConfig
中觸發的。
這 3 次 findClass 的順序可以看出, 類的加載順序爲:
BootMessageConverter (二方包)
-> FastJsonHttpMessageConverter (fastjson)
-> GenericHttpMessageConverter (spring-web)
再來看看WebAutoConfig
觸發類加載的那段代碼:
@Configuration
public class WebAutoConfig implements WebMvcConfigurer {
@Bean
@ConditionalOnMissingBean
public HttpMessageConverters httpMessageConverter() {
BootMessageConverter converter = new BootMessageConverter(); //這一行觸發了類加載
...
}
}
public class BootMessageConverter extends FastJsonHttpMessageConverter {
...
}
public class FastJsonHttpMessageConverter extends AbstractHttpMessageConverter<Object>
implements GenericHttpMessageConverter<Object> {
...
}
從上面的代碼能看出最開始是因爲BootMessageConverter
的實例化進行了類加載, 而BootMessageConverter
因爲繼承了FastJsonHttpMessageConverter
, 又接着觸發了FastJsonHttpMessageConverter
的類加載, 然後FastJsonHttpMessageConverter
因爲實現了GenericHttpMessageConverter
接口, 又進一步觸發了GenericHttpMessageConverter
的類加載, 這樣來看源碼和上面 debug 得出的結論是一致的。
分析到這一步,如果你對類加載機制以及 agent 的運行方式非常熟悉的話,基本已經能得出 “爲什麼會報GenericHttpMessageConverter
類找不到的錯誤” 結論了。
那麼接下來,我會基於類加載的機制來詳細分析一下,爲什麼會找不到GenericHttpMessageConverter
三、類加載機制
3.1 雙親委派機制
上一層類加載器是下一層類加載器的父加載器,除了 Bootstrap ClassLoader 之外,所有的加載器都是有父加載器的。
所謂的雙親委派機制,指的就是:當一個類加載器收到了類加載的請求的時候,他不會直接去加載指定的類,而是把這個請求委託給自己的父加載器去加載。只有父加載器無法加載這個類的時候,纔會由當前這個加載器來負責類的加載。
開個玩笑:這樣說來,雙親委派這種說法似乎並不準確,因爲有父無母,準確來說應該是 “單親委派”...
3.1.1 類中依賴的其他類是怎麼加載的
---------------- 接下來是重點 ----------------
我們定義的類一般還會依賴其他類,因此在被類加載器加載時,類加載機制中除了雙親委派機制之外,還有一個重要的機制是:
假設類 A 依賴類 B,那麼哪個 ClassLoader 找到了類 A,這個 ClassLoader 也會嘗試去加載類 B(當然類 B 的加載過程也遵循雙親委派)。
3.2 springboot 的類加載機制
springboot 項目打包之後的 jar 目錄結構如下:
├─BOOT-INF
│ ├─classes
│ │ ├─應用代碼
│ └─lib
│ ├─應用依賴的jar包
├─META-INF
│ ├─MANIFEST.MF
└─org
└─springframework
└─boot
└─loader
│ JarLauncher.class
│ LaunchedURLClassLoader.class
│ Launcher.class
│ ...
其中 / META-INF/MANIFEST.MF 是 jar 包運行的關鍵, 來看一下里面的內容:
...
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.xxxxxx.DemoApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
...
首先 jar 包運行都有一個入口類定義了 main 方法,可以看到 springboot 項目打包出來的 jar 定義的入口運行類並不是應用代碼中的XxxApplication
,而是 springboot 中的一個類JarLauncher
,那麼應用代碼中的XxxApplication
是怎麼運行的呢?
當你運行 java -jar 命令的時候,JarLauncher
會加載 /BOOT-INF/classes 下的類和 /BOOT-INF/lib 下的 jar 包。最後調用 MANIFEST.MF 文件的 Start-Class 屬性指定的類的 main 方法來完成應用程序的啓動。
問題是 /BOOT-INF/ 並不是標準的 classpath 路徑,系統內置的 ClassLoader 是加載不到這些目錄的類的,那麼這些類是誰來加載的呢?答案就是 springboot 自定義的類加載器:LaunchedURLClassLoader
也就是說應用代碼中的類以及應用依賴的 jar 都是LaunchedURLClassLoader
負責加載的。
3.3 fastjson 的類到底是怎麼找到的
再說回來在第 2.2 節中說到的類加載順序:
BootMessageConverter (二方包)
-> FastJsonHttpMessageConverter (fastjson)
-> GenericHttpMessageConverter (spring-web)
這裏我們重點來分析一下中間那個FastJsonHttpMessageConverter
到底是怎麼被加載的。
已知應用依賴了 fastjson 和 spring-web,agent 也依賴了 fastjson 但不依賴 spring-web。
從 Oracle 官方的文檔看到,Java 8 的 agent 的 jar 包裏的類會添加到 classpath 中,因此會用AppClassLoader
來加載。
而二方包的BootMessageConverter
是應用依賴的 jar, 放在 / BOOT-INF/lib 下, 因此是被LaunchedURLClassLoader
加載的。整體類加載流程如下圖:
上圖說明:
當BootMessageConverter
被LaunchedURLClassLoader
加載時, 發現依賴了FastJsonHttpMessageConverter
, 因此LaunchedURLClassLoader
會繼續嘗試去加載FastJsonHttpMessageConverter
。由於類加載的雙親委派機制,LaunchedURLClassLoader
會委派它的父加載器AppClassLoader
來嘗試加載,當然AppClassLoader
會繼續往上找父加載器,一直到Bootstrap ClassLoader
。
很顯然,Bootstrap ClassLoader
和ExtClassLoader
都無法找到FastJsonHttpMessageConverter
,但是AppClassLoader
可以找到(因爲 agent 包中存在 fastjson 的類)。然後,這一步是關鍵,AppClassLoader
找到了FastJsonHttpMessageConverter
之後發現它依賴了GenericHttpMessageConverter
,因此由找到了FastJsonHttpMessageConverter
的AppClassLoader
繼續嘗試加載GenericHttpMessageConverter
,但是GenericHttpMessageConverter
只有應用依賴的 spring-web.jar 中才有,而這個 jar 在 / BOOT-INF/lib 下,只能被LaunchedURLClassLoader
加載。雙親委派機制只能由子加載器往父加載器委託而反過來是不行的,而GenericHttpMessageConverter
沒辦法被AppClassLoader
以及它的父加載器加載到,因此AppClassLoader
拋出了找不到GenericHttpMessageConverter
的錯誤。
---------------- 劃重點 ----------------
這裏的關鍵就在於LaunchedURLClassLoader
本身是能找到 fastjson 類的 (在 / BOOT-INF/lib), 但是因爲雙親委派機制, 在加載 fastjson 類的時候, 被AppClassLoader
截胡了,以至於喪失了後面依賴的類加載主動權。
說到這裏,就可以回答之前的那個問題了:爲什麼用 idea 啓動應用(掛載 agent)是沒問題的?因爲 idea 是直接運行應用的 XxxApplication 類的 main 方法,不是通過 springboot 的
JarLauncher
啓動的,而在運行時所有的依賴都是通過指定 classpath 來做的,因此 idea 運行過程中,所有的類都能通過AppClassLoader
加載到,也就不存在上面這種衝突問題了。
四、解決方案一:maven-shade-plugin
知道問題的根因了,那麼思路就是怎麼樣可以讓 fastjson 類被LaunchedURLClassLoader
找到而不要被AppClassLoader
找到。這裏的思路是把 agent 中依賴的 fastjson 的 package 給重命名一下。
maven-shade-plugin 在 maven 官方網站中提供的一個插件,官方文檔中定義其功能如下:
This plugin provides the capability to package the artifact in an uber-jar, including its dependencies and to shade - i.e. rename - the packages of some of the dependencies.
簡單來說就是將依賴的包在 package 階段一起打入 jar 包中,以及對依賴的 jar 包進行重命名從而達到隔離的作用。接下來就把這個 maven 插件引入 agent 中。
maven 配置:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<shadedArtifactAttached>false</shadedArtifactAttached>
<createDependencyReducedPom>true</createDependencyReducedPom>
<createSourcesJar>true</createSourcesJar>
<shadeSourcesContent>true</shadeSourcesContent>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Premain-Class>xxxxxx.AgentStarter</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</transformer>
</transformers>
<!-- 這段是package重命名的關鍵配置 -->
<relocations>
<relocation>
<pattern>com.alibaba.fastjson</pattern>
<shadedPattern>shade.com.alibaba.fastjson</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
package 之後的效果:
可以看到在 agent 包中,fastjson 類的 package 都已經加上了一個前綴shade.
,這樣的話,應用中加載正常的 fastjson 類的時候,肯定不會找到 agent 裏面來了,以此避免了類加載被AppClassLoader
截胡的情況。
用重新 package 的 agent 包啓動之前應用, 應用正常啓動, 至此問題解決。
五、再現問題
本以爲問題已經解決,沒想到幾天後另一個應用又報了類找不到的錯誤:
有了上次的經驗, 這次還算順利, 排查過程跟上次的差不多。
最後發現是應用依賴的 jersey 這個三方庫,而 jersey 通過 SPI 的方式會去找所有 classpath 中 \ META-INF\services \ 目錄下的javax.ws.rs.ext.MessageBodyReader
這個文件,由於 agent 依賴了 fastjson,而 fastjson 也實現了這個 SPI 的擴展,結果 jersey 就找到了 agent 包的 \ META-INF\services \ 目錄下的javax.ws.rs.ext.MessageBodyReader
文件,而javax.ws.rs.ext.MessageBodyReader
文件中的內容如下:
可以看到 maven-shade-plugin 把這裏的類 package 也改掉了。然後 jersey 讀取到這個文件後,根據類名去加載了shade.com.alibaba.fastjson.support.jaxrs.FastJsonProvider
這個類,結果肯定是找到了 agent 包裏的這個類,而這個類依賴的MessageBodyReader
類是在 jsr311-api.jar 裏的, 這個 jar 包只在應用中依賴, agent 並不依賴這個 jar 包, 因此就拋出了找不到類的錯誤。
依賴衝突真是讓人防不勝防~
六、決定:幹掉 fastjson
本來我查了下 maven-shade-plugin 似乎是可以在 agent 打包時把 \ META-INF\services \ 這個目錄排除掉的,這樣的話上面的問題也能解決掉,但是連續兩次踩了這個坑還是讓我靜下來好好思考了一下。
這兩次的依賴衝突從根本上來看,都是因爲 fastjson 做的太重,第一次是因爲 fastjson 依賴了 spring,第二次是因爲 fastjson 實現了 jsr311-api,而在 agent 中去依賴 fastjson 並沒有那麼多的需求,只是爲了做一個純粹的轉換工作:Java 對象和 Json 串之間的互相轉換。所以找一個純粹的輕量級的 Json 轉換庫是我的本質需求。否則 fastjson 下次可能又遇到其他的依賴衝突問題,我還得改。
如何考量是否輕量級呢?我主要從兩方面着手:
-
看這個三方庫的 pom.xml 中有沒有依賴其他三方庫
-
看這個三方庫的 \ META-INF\services \ 目錄有沒有多餘的 SPI 實現
最終我選擇了 Google 的 gson 作爲 agent 依賴的 Json 轉換庫。
可以看到 fastjson 的 “罪行” 可謂罄竹難書,而 gson 除了 junit 之外沒有任何依賴,且 gson 不存在 \ META-INF\services \ 目錄,完全滿足我的需求。
順便給 fastjson 也提個建議:目前的包耦合這麼嚴重,是不是可以考慮拆成多個,比如 fastjson-core,fastjson-spring 等,讓使用者按需依賴是不是更好呢。
七、總結
-
在 agent 研發中儘量用 JDK 內置的類去做功能,減少第三方庫的依賴。
-
如果依賴了第三方庫,可以用 maven-shade-plugin 來進行 package 重命名,以此達到和應用依賴類的隔離效果。
-
小心 SPI 機制,agent 依賴第三方庫後,需要確認 \ META-INF\services \ 目錄下的內容,如有必要可以進行排除或換成其他乾淨的依賴。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/ZYSiPGBQZLljZE0ESMM2tg