DUICUO

エージェントで Fastjson に依存するのはやめてください。

I. 背景

最近、エージェントに例外を報告する機能を追加し、エージェントが HTTP リクエスト中にオブジェクトを JSON 形式に簡単に変換する必要があったため、Fastjson 依存関係を追加し、さまざまな問題が発生しました。

環境:

  • JDK 1.8
  • SpringBoot 2.0.0.リリース
  • スカイウォーキングエージェント 8.14.0

II. 初期の問題

2.1 予備的な位置づけ

同僚から、アプリケーションはローカルでは起動できるが、テスト環境 (エージェントで起動) では起動に失敗し、次のエラーが発生するという報告がありました。

写真

まず、依存関係の競合の問題かどうかを確認する必要があります。`GenericHttpMessageConverter` クラスは `spring-web` パッケージ内にあります。ローカルのパッケージ環境とテスト環境が異なる場合があるため、テスト環境にデプロイされたパッケージに `spring-web` パッケージが含まれているかどうかを確認する必要があります。`spring-web` パッケージが存在することを確認したため、この可能性は排除されました。

そこで、エージェントとアプリケーションの間に依存関係の競合が発生しているのではないかと疑いました。アプリケーションのエージェントを一時的にオフラインにした後、再デプロイしました。その結果、正常に起動できることが確認され、問題の原因がエージェントにあることがほぼ確定しました。

2.2 さらなる調査

トラブルシューティングを容易にするため、問題が発見されたアプリケーションデプロイメントパッケージをローカルマシンにダウンロードし、エージェントをローカルにマウントしてアプリケーションを起動しました。問題は再現され、エラーメッセージはテスト環境と一致していました。これでローカルでデバッグできるようになりました。

ちなみに、最初は IntelliJ IDEA でアプリケーションの起動 (エージェントのマウント) に問題はありませんでしたが、その理由は後で説明します。

java.net.URLClassLoader#findClass メソッドのエントリポイントに条件付きブレークポイントをローカルに設定しました(GenericHttpMessageConverter という名前のクラスのみがこのブレークポイントをトリガーします)。ブレークポイントはアプリケーションの起動直後にトリガーされます。

IntelliJ IDEAツールは本当に便利です。デバッグインターフェースから、`findClass`が3回呼び出されたことがすぐにわかり、さらにどのクラスがロードされたかまで確認できます。

写真

上の図の最後の行からわかるように、このクラスの読み込みの最初のトリガーは、内部サードパーティ ライブラリの 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のクラスローディングもトリガーされています。これは、ソースコードと上記のデバッグから得られた結論と一致しています。

分析のこの時点で、クラスのロード メカニズムとエージェントの動作をよく理解していれば、「GenericHttpMessageConverter クラスが見つかりません」というエラーが発生する理由を結論付けることができるはずです。

次に、クラスローディングのメカニズムに基づいて、GenericHttpMessageConverter が見つからない理由を詳しく分析します。

III. クラスローディングメカニズム

3.1 親の任命メカニズム

写真

一つ上のレベルのクラスローダーは、一つ下のレベルのクラスローダーの親クラスローダーです。Bootstrap ClassLoaderを除き、すべてのクラスローダーには親クラスローダーがあります。

いわゆる親委譲メカニズムとは、クラスローダーがクラスのロード要求を受け取った際に、指定されたクラスを直接ロードするのではなく、その要求を親クラスローダーに委譲する仕組みを指します。親クラスローダーがクラスをロードできない場合にのみ、現在のクラスローダーがクラスのロード処理を引き継ぎます。

冗談です。つまり、「親の割り当て」という用語は、父親はいるものの母親がいないということなので、あまり正確ではないようです。より正確な用語は「ひとり親の割り当て」でしょう...

3.1.1 このクラスが依存する他のクラスはどのようにロードされますか?

----------------以下が重要な部分です----------------

私たちが定義するクラスは通常、他のクラスに依存します。そのため、クラスローダーによってクラスがロードされる際には、親クラスへの委譲メカニズムに加えて、クラスロードメカニズムにおけるもう1つの重要なメカニズムが存在します。

クラス A がクラス B に依存している場合、クラス A を見つけた ClassLoader はクラス B もロードしようとします (もちろん、クラス B のロード プロセスも親の委任モデルに従います)。

3.2 Spring Bootのクラスローディングメカニズム

パッケージ化された Spring Boot プロジェクト 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 ファイルの実行に不可欠です。その内容を見てみましょう。

...

メインクラス: org.springframework.boot.loader.JarLauncher

開始クラス: com.xxxxxx.DemoApplication

Spring-Boot-Classes: BOOT-INF/classes/

Spring-Boot-Lib: BOOT-INF/lib/

...

まず、すべてのJARファイルには、`main`メソッドを定義するエントリポイントクラスがあります。Spring Bootプロジェクトからパッケージ化されたJARで定義されているエントリポイントクラスは、アプリケーションコード内の`XxxApplication`ではなく、Spring Bootの`JarLauncher`クラスであることがわかります。では、アプリケーションコード内の`XxxApplication`はどのように実行されるのでしょうか?

`java -jar` コマンドを実行すると、JarLauncher は `/BOOT-INF/classes` 下のクラスと `/BOOT-INF/lib` 下の JAR ファイルをロードします。最後に、`MANIFEST.MF` ファイルの `Start-Class` 属性で指定されたクラスの `main` メソッドを呼び出してアプリケーションを起動します。

問題は、`/BOOT-INF/` が標準のクラスパスではないため、システム組み込みの ClassLoader ではこれらのディレクトリからクラスをロードできないことです。では、誰がこれらのクラスをロードするのでしょうか?答えは、Spring Boot のカスタムクラスローダーである `LaunchedURLClassLoader` です。

写真

つまり、アプリケーション コード内のクラスとアプリケーションが依存する JAR はすべて、LaunchedURLClassLoader によってロードされます。

3.3 fastjson のクラスはどのようにして見つかるのでしょうか?

セクション 2.2 で説明したクラスの読み込み順序に戻りましょう。

BootMessageConverter(セカンドパーティパッケージ)

-> FastJsonHttpMessageConverter (fastjson)

-> GenericHttpMessageConverter (spring-web)

ここでは、中央の FastJsonHttpMessageConverter がどのようにロードされるかを分析することに焦点を当てます。

アプリケーションは fastjson と spring-web に依存しており、エージェントも fastjson に依存していますが、spring-web には依存していません。

Oracle の公式ドキュメントによると、Java 8 エージェントの JAR ファイル内のクラスはクラスパスに追加されるため、AppClassLoader を使用してロードされます。

写真

サードパーティパッケージのBootMessageConverterは、アプリケーションが依存するJARファイルで、/BOOT-INF/libに配置されているため、LaunchedURLClassLoaderによってロードされます。クラスロードの全体的なプロセスは次の図に示されています。

写真

上の画像は次のことを示しています。

BootMessageConverter が LaunchedURLClassLoader によってロードされると、それが FastJsonHttpMessageConverter に依存していることが分かります。そのため、LaunchedURLClassLoader は FastJsonHttpMessageConverter のロードを試行し続けます。クラスロードの親クラスへの委譲メカニズムにより、LaunchedURLClassLoader は親クラスである AppClassLoader にロードを委譲します。当然のことながら、AppClassLoader は親クラスローダーを上方向に検索し続け、Bootstrap ClassLoader まで辿り着きます。

明らかに、Bootstrap ClassLoaderもExtClassLoaderもFastJsonHttpMessageConverterを見つけることができませんが、AppClassLoaderは見つけることができます(fastjsonクラスがエージェントパッケージに存在するため)。そして、このステップは重要です。AppClassLoaderはFastJsonHttpMessageConverterを見つけた後、それがGenericHttpMessageConverterに依存していることを検出します。そのため、FastJsonHttpMessageConverterを見つけたAppClassLoaderは、GenericHttpMessageConverterのロードを試行し続けます。しかし、GenericHttpMessageConverterはアプリケーションの依存ライブラリであるspring-web.jar(/BOOT-INF/lib)にのみ存在し、LaunchedURLClassLoaderによってのみロードできます。親の委任メカニズムでは、子ローダーは親ローダーに委任することしかできず、その逆は許可されません。 GenericHttpMessageConverter は AppClassLoader またはその親ローダーによってロードできないため、AppClassLoader は GenericHttpMessageConverter が見つからないことを示すエラーをスローします。

ここで重要な点は、LaunchedURLClassLoader 自体は fastjson クラス (/BOOT-INF/lib 内) を見つけることができますが、親の委任メカニズムにより、fastjson クラスをロードするときに AppClassLoader によってインターセプトされ、依存するクラスをロードする主導権を失ってしまうことです。

これで、先ほどの疑問への答えが導き出されました。なぜIntelliJ IDEAを使って(エージェントをマウントした状態で)アプリケーションを起動しても問題ないのでしょうか?IntelliJ IDEAは、Spring BootのJarLauncherを経由せず、アプリケーションの`XxxApplication`クラスのメインメソッドを直接実行するからです。すべての依存関係は実行時にクラスパスで指定されるため、IntelliJ IDEAの実行中にAppClassLoaderを介してすべてのクラスをロードすることができ、前述の競合の問題を回避できます。

IV. 解決策1: maven-shade-plugin

問題の根本原因がわかったので、解決策はfastjsonクラスをLaunchedURLClassLoaderからアクセス可能にし、AppClassLoaderからはアクセスできないようにすることです。このアプローチでは、エージェントが依存するfastjsonパッケージの名前を変更します。

maven-shade-plugin は、Maven の公式ウェブサイトで提供されているプラ​​グインです。その機能は公式ドキュメントで次のように定義されています。

このプラグインは、依存関係を含むアーティファクトを uber-jar にパッケージ化し、一部の依存関係のパッケージをシェーディング (つまり名前変更) する機能を提供します。

簡単に言うと、パッケージフェーズで依存パッケージをJARファイルに含め、依存JARファイルの名前を変更して分離を実現するというものです。次に、このMavenプラグインをエージェントに導入します。

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>

包装後の効果:

写真

ご覧のとおり、エージェントパッケージ内の fastjson クラスのパッケージ名には「shade.」というプレフィックスが付けられています。これは、アプリケーションが通常の fastjson クラスをロードする際に、エージェントパッケージ内でそのクラスが見つからないことを意味します。これにより、クラスのロードが AppClassLoader によってインターセプトされる状況を回避できます。

再パッケージ化したエージェントを起動した後、アプリケーションは正常に起動し、問題は解決しました。

V. 問題の再現

問題は解決したと思いましたが、数日後、別のアプリケーションでクラスが見つからないというエラーが報告されました。

写真

前回の経験もあり、今回は比較的スムーズに進み、調査プロセスも前回と同様でした。

この問題は最終的に、アプリケーションが依存していたサードパーティ製ライブラリJerseyに起因することが判明しました。JerseyはSPIを介して、すべてのクラスパスの\META-INF\services\ディレクトリでjavax.ws.rs.ext.MessageBodyReaderファイルを検索します。エージェントはFastjsonに依存しており、FastjsonはこのSPI拡張機能を実装しているため、Jerseyはエージェントパッケージの\META-INF\services\ディレクトリでjavax.ws.rs.ext.MessageBodyReaderファイルを検出しました。javax.ws.rs.ext.MessageBodyReaderファイルの内容は次のとおりです。

写真

ご覧のとおり、maven-shade-plugin はここでもクラスパッケージを変更しています。Jersey はこのファイルを読み込んだ後、クラス名に基づいて `shade.com.alibaba.fastjson.support.jaxrs.FastJsonProvider` クラスをロードします。必然的にこのクラスは `agent` パッケージ内で見つかりますが、このクラスは `jsr311-api.jar` 内の `MessageBodyReader` クラスに依存しています。この JAR ファイルはアプリケーション内でのみ必要であり、エージェントはこれに依存しません。そのため、「クラスが見つかりません」というエラーが発生します。

依存関係の競合を防ぐのは本当に困難です。

VI. 決断: ファストソンを殺す

当初、maven-shade-plugin を確認したところ、エージェントをパッケージ化する際に \META-INF\services\ ディレクトリを除外することで上記の問題を解決できるようでした。しかし、この問題に2度遭遇したため、落ち着いて慎重に検討する必要がありました。

どちらの依存関係の競合も、Fastjson が重すぎることに起因していました。1つ目は Fastjson が Spring に依存していたこと、2つ目は Fastjson が JSR311 API を実装していたことが原因でした。しかし、このエージェントでは Fastjson はそれほど必要とされていませんでした。Java オブジェクトと JSON 文字列間の変換という、純粋な変換タスクのためでした。そのため、純粋に軽量な JSON 変換ライブラリを見つけることが私の最優先事項でした。そうでなければ、将来 Fastjson が他の依存関係の競合に遭遇し、再度修正が必要になる可能性がありました。

軽量かどうかはどうやって判断するのでしょうか?私は主に2つの側面からアプローチしています。

  1. このサードパーティ ライブラリの pom.xml ファイルが他のサードパーティ ライブラリに依存しているかどうかを確認します。
  2. このサードパーティ ライブラリの \META-INF\services\ ディレクトリに冗長な SPI 実装があるかどうかを確認します。

最終的に、エージェントが依存する JSON 変換ライブラリとして Google の Gson を選択しました。