<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          拜托,別在 agent 中依賴 fastjson 了

          共 13488字,需瀏覽 27分鐘

           ·

          2023-08-10 08:52

          依舊是夏老師的文章,原文發(fā)表在 infoQ:https://xie.infoq.cn/article/99092a1da96f7d17989db7217,點擊文章底部閱讀原文直達。

          一、背景

          最近因為增加了一個在 agent 中上報異常的功能,agent 為了在 http 請求時方便把對象轉(zhuǎn)換為 json 格式,增加了一個 fastjson 的依賴,結果搞出來各種問題。

          環(huán)境:

          • JDK 1.8
          • SpringBoot 2.0.0.RELEASE
          • skywalking agent 8.14.0

          二、初現(xiàn)問題

          2.1 初步定位

          有同事反饋應用在本地能啟動,但是到了測試環(huán)境(帶 agent 啟動)就起不來,報錯如下:

          首先還是要確認下是不是應用的依賴沖突問題,GenericHttpMessageConverter這個類是在 spring-web 這個包下面的, 因為本地打包環(huán)境和測試環(huán)境有可能不一致,需要確認最終部署到測試環(huán)境的包里是否包含了 spring-web 包。經(jīng)確認包里有 spring-web,排除這個可能。

          然后懷疑是 agent 和應用的依賴沖突,臨時讓這個應用的 agent 下線后重新部署,發(fā)現(xiàn)能正常啟動,基本確認是 agent 帶來的問題。

          2.2 進一步排查

          為了方便定位問題,我把發(fā)現(xiàn)問題時應用部署的包下載到本地,并在本地掛載 agent 啟動,問題重現(xiàn),報錯和測試環(huán)境一致。至此我就可以在本地 debug 了。

          順便說一下,我一開始用 idea 啟動應用(掛載 agent)是沒問題的,至于為什么沒問題下面會說到。

          本地我在java.net.URLClassLoader#findClass方法的入口處打了一個條件斷點(類名為GenericHttpMessageConverter的才會進來),啟動應用后一會兒進入斷點。

          idea 這個工具就是好用,從 debug 界面一下子就能看出來,這個 findClass 是調(diào)用了 3 次,并且能看到每一次 findClass 是加載的哪個類:

          從上面的圖的最后一行也能看出來,這個類加載最開始的觸發(fā)是在內(nèi)部的一個二方庫的類WebAutoConfig中觸發(fā)的。

          這 3 次 findClass 的順序可以看出, 類的加載順序為:

          BootMessageConverter (二方包)

          -> FastJsonHttpMessageConverter (fastjson)

          -> GenericHttpMessageConverter (spring-web)

          再來看看WebAutoConfig觸發(fā)類加載的那段代碼:

          @Configuration
          public class WebAutoConfig implements WebMvcConfigurer {
            
            @Bean
              @ConditionalOnMissingBean
              public HttpMessageConverters httpMessageConverter() {
                  BootMessageConverter converter = new BootMessageConverter(); //這一行觸發(fā)了類加載
              ...
              }
          }

          public class BootMessageConverter extends FastJsonHttpMessageConverter {
           ...
          }

          public class FastJsonHttpMessageConverter extends AbstractHttpMessageConverter<Object>
                  implements GenericHttpMessageConverter<Object
          {
           ...        
          }

          從上面的代碼能看出最開始是因為BootMessageConverter的實例化進行了類加載, 而BootMessageConverter因為繼承了FastJsonHttpMessageConverter, 又接著觸發(fā)了FastJsonHttpMessageConverter的類加載, 然后FastJsonHttpMessageConverter因為實現(xiàn)了GenericHttpMessageConverter接口, 又進一步觸發(fā)了GenericHttpMessageConverter的類加載, 這樣來看源碼和上面 debug 得出的結論是一致的。

          分析到這一步,如果你對類加載機制以及 agent 的運行方式非常熟悉的話,基本已經(jīng)能得出“為什么會報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 包運行的關鍵, 來看一下里面的內(nèi)容:

          ...

          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 包。最后調(diào)用 MANIFEST.MF 文件的 Start-Class 屬性指定的類的 main 方法來完成應用程序的啟動。

          問題是 /BOOT-INF/ 并不是標準的 classpath 路徑,系統(tǒng)內(nèi)置的 ClassLoader 是加載不到這些目錄的類的,那么這些類是誰來加載的呢?答案就是 springboot 自定義的類加載器:LaunchedURLClassLoader

          也就是說應用代碼中的類以及應用依賴的 jar 都是LaunchedURLClassLoader負責加載的。

          3.3 fastjson 的類到底是怎么找到的

          再說回來在第 2.2 節(jié)中說到的類加載順序:

          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加載的。整體類加載流程如下圖:

          上圖說明:

          BootMessageConverterLaunchedURLClassLoader加載時, 發(fā)現(xiàn)依賴了FastJsonHttpMessageConverter, 因此LaunchedURLClassLoader會繼續(xù)嘗試去加載FastJsonHttpMessageConverter。由于類加載的雙親委派機制,LaunchedURLClassLoader會委派它的父加載器AppClassLoader來嘗試加載,當然AppClassLoader會繼續(xù)往上找父加載器,一直到Bootstrap ClassLoader

          很顯然,Bootstrap ClassLoaderExtClassLoader都無法找到FastJsonHttpMessageConverter,但是AppClassLoader可以找到(因為 agent 包中存在 fastjson 的類)。然后,這一步是關鍵,AppClassLoader找到了FastJsonHttpMessageConverter之后發(fā)現(xiàn)它依賴了GenericHttpMessageConverter,因此由找到了FastJsonHttpMessageConverterAppClassLoader繼續(xù)嘗試加載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 官方網(wǎng)站中提供的一個插件,官方文檔中定義其功能如下:

          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 都已經(jīng)加上了一個前綴shade.,這樣的話,應用中加載正常的 fastjson 類的時候,肯定不會找到 agent 里面來了,以此避免了類加載被AppClassLoader截胡的情況。

          用重新 package 的 agent 包啟動之前應用, 應用正常啟動, 至此問題解決。

          五、再現(xiàn)問題

          本以為問題已經(jīng)解決,沒想到幾天后另一個應用又報了類找不到的錯誤:

          有了上次的經(jīng)驗, 這次還算順利, 排查過程跟上次的差不多。

          最后發(fā)現(xiàn)是應用依賴的 jersey 這個三方庫,而 jersey 通過 SPI 的方式會去找所有 classpath 中\(zhòng)META-INF\services\目錄下的javax.ws.rs.ext.MessageBodyReader這個文件,由于 agent 依賴了 fastjson,而 fastjson 也實現(xiàn)了這個 SPI 的擴展,結果 jersey 就找到了 agent 包的\META-INF\services\目錄下的javax.ws.rs.ext.MessageBodyReader文件,而javax.ws.rs.ext.MessageBodyReader文件中的內(nèi)容如下:

          可以看到 maven-shade-plugin 把這里的類 package 也改掉了。然后 jersey 讀取到這個文件后,根據(jù)類名去加載了shade.com.alibaba.fastjson.support.jaxrs.FastJsonProvider這個類,結果肯定是找到了 agent 包里的這個類,而這個類依賴的MessageBodyReader類是在 jsr311-api.jar 里的, 這個 jar 包只在應用中依賴, agent 并不依賴這個 jar 包, 因此就拋出了找不到類的錯誤。

          依賴沖突真是讓人防不勝防~

          六、決定:干掉 fastjson

          本來我查了下 maven-shade-plugin 似乎是可以在 agent 打包時把\META-INF\services\這個目錄排除掉的,這樣的話上面的問題也能解決掉,但是連續(xù)兩次踩了這個坑還是讓我靜下來好好思考了一下。

          這兩次的依賴沖突從根本上來看,都是因為 fastjson 做的太重,第一次是因為 fastjson 依賴了 spring,第二次是因為 fastjson 實現(xiàn)了 jsr311-api,而在 agent 中去依賴 fastjson 并沒有那么多的需求,只是為了做一個純粹的轉(zhuǎn)換工作:Java 對象和 Json 串之間的互相轉(zhuǎn)換。所以找一個純粹的輕量級的 Json 轉(zhuǎn)換庫是我的本質(zhì)需求。否則 fastjson 下次可能又遇到其他的依賴沖突問題,我還得改。

          如何考量是否輕量級呢?我主要從兩方面著手:

          1. 看這個三方庫的 pom.xml 中有沒有依賴其他三方庫
          2. 看這個三方庫的\META-INF\services\目錄有沒有多余的 SPI 實現(xiàn)

          最終我選擇了 Google 的 gson 作為 agent 依賴的 Json 轉(zhuǎn)換庫。

          可以看到 fastjson 的“罪行”可謂罄竹難書,而 gson 除了 junit 之外沒有任何依賴,且 gson 不存在\META-INF\services\目錄,完全滿足我的需求。

          順便給 fastjson 也提個建議:目前的包耦合這么嚴重,是不是可以考慮拆成多個,比如 fastjson-core,fastjson-spring 等,讓使用者按需依賴是不是更好呢。

          七、總結

          1. 在 agent 研發(fā)中盡量用 JDK 內(nèi)置的類去做功能,減少第三方庫的依賴。
          2. 如果依賴了第三方庫,可以用 maven-shade-plugin 來進行 package 重命名,以此達到和應用依賴類的隔離效果。
          3. 小心 SPI 機制,agent 依賴第三方庫后,需要確認\META-INF\services\目錄下的內(nèi)容,如有必要可以進行排除或換成其他干凈的依賴。

          最后感謝能抽空看到這里,如果你能點贊在看、分享,我會更加感激不盡~


          • 搜索關注微信公眾號"捉蟲大師",后端技術分享,架構設計、性能優(yōu)化、源碼閱讀、問題排查、踩坑實踐
          • 進技術交流群加微信 MrRoshi

          瀏覽 3005
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  69er小视频 | 天天插天天干天天日 | 天天无码高清 | 婷婷情色五月天 | 欧美视频一区二区 |