
シェア:
シナはVonageのJavaデベロッパー・アドボケイト。アカデミックなバックグラウンドを持ち、自動車、コンピューター、プログラミング、テクノロジー、人間性など、あらゆることに好奇心旺盛。余暇には散歩をしたり、対戦型ビデオゲームをしたりしている。
Javaランタイム依存の問題を解決する簡単な1つのトリック
はじめに
JVMベースの言語 JVMベースの言語Java、Kotlin、Scala、Groovy、ClojureなどのJVMベースの言語を使っている場合、次のようなビルドツールや依存関係管理ツールに出会ったことがあるだろう。 Ant / Ivy, Maven, sbt, ライネンゲンまたは グラドル.
基本的に、これらのツールの目的は、アプリケーションのビルド(そして時にはリリース)プロセスを自動化することです。これらのツールは、コードのコンパイル、テストの実行、ドキュメント・ページ(Javadoc)の生成、JARファイルのような配布可能な成果物の生成を行います。
ビルド自動化ツールを使うことの主な責任とメリットは、依存関係を宣言的に管理してくれることである。小規模なプロジェクトでは、ビルドシステムをまったく使わないことも可能である。 javacそして jarを使ってアプリケーションをコンパイルし、パッケージ化します。
サードパーティの依存関係をプロジェクトに追加し始めると、これは面倒になり、保守性も低下する。依存関係のあるJARファイルの最新バージョンを見つけ、それをビルドプロセスの一部として使用し、コンパイル時と実行時の両方でクラスパスに含め、バージョン管理システムに不要なバイナリを残さないように注意しながら、手作業で管理するのは大変です。
Javaエコシステム内の主要なビルドツールは、依存関係を指定するための宣言的な方法を提供し、あなたのためにそのプロセスを処理しますが、それらは無謬ではありません。時には、表面的には意味をなさない依存関係が含まれていることが原因で、明らかな原因も解決策もない問題が発生することがあります。
この記事の目的は、問題のある依存関係管理のシナリオを探り、それがなぜ起こるのか、そしてそれを解決する方法を探ることである。
サンプル・プロジェクト
説明のため、この記事ではMavenを使ったJavaプロジェクトを最小限の作業例として使用する。しかし、この原則はGradleや他のJVM言語にも適用できます。さて、早速本題に入りましょう!
以下は <project>要素の初期内容である。 pom.xml:
<groupId>org.example</groupId>
<artifactId>minimal</artifactId>
<version>1.0-SNAPSHOT</version>
<name>Dependency Example</name>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.vonage</groupId>
<artifactId>client</artifactId>
<version>7.10.0</version>
</dependency>
</dependencies>
ご覧のように、依存関係を1つ宣言しています - Vonage Java SDKです。メイン・クラスは Account API を使って残高を取得し、Messages API を使って残高を送信します。
package com.example.minimal;
import com.vonage.client.VonageClient;
import com.vonage.client.messages.sms.SmsTextRequest;
public class BalancePrinter {
public static void main(String[] args) throws Throwable {
VonageClient client = VonageClient.builder()
.apiKey(System.getenv("VONAGE_API_KEY"))
.apiSecret(System.getenv("VONAGE_API_SECRET"))
.build();
var balance = client.getAccountClient().getBalance();
var balanceText = SmsTextRequest.builder()
.from("Vonage").to(System.getenv("TO_NUMBER"))
.text("Balance: €" + balance.getValue()).build();
var response = client.getMessagesClient().sendMessage(balanceText);
System.out.println(response.getMessageUuid());
}
}IDEから実行すると、これは問題なく動作する。では、スタンドアロンの実行ファイルを作成するために、すべての依存関係を含む実行可能なJARファイルを作成しましょう。これは Maven Shadeプラグイン:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.example.minimal.BalancePrinter</mainClass>
</transformer>
</transformers>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
を実行すると mvn clean installを実行すると、まさに実行可能なスタンドアロンJAR(これは target/minimal-1.0-SNAPSHOT.jar).そしてそれは動作する!コマンドを実行したときに生成された警告に気づきましたか? mvnコマンドを実行したときに生成された警告に気づきましたか?最終的にはこのように表示されます:
" [WARNING] maven-shade-pluginは、いくつかのファイルが2つ以上のJARに存在することを検出しました。これが発生すると、1つの単一バージョンのファイルのみがuber jarにコピーされます。通常、これは有害ではないので、これらの警告をスキップできますが、そうでない場合は mvn dependency:tree -Ddetail=true と上記の出力に基づいて手動で成果物を除外してみてください。"
この警告は何を伝えようとしているのだろうか?Java SDK自体には依存関係があります。 mvnrepository.comの.これらの依存関係には、それ自身の依存関係などがあります。
この依存階層を "平坦化 "する単一のJARファイルを作成するため、最終的には多くのクラス・ファイルが作成されることになります。しかし、これらの依存関係の中には、同じアーティファクトに依存するものもあります。クラスだけが残るように各JARを再帰的に解凍するこのプロセスは、同じ依存関係に複数のバージョンがある場合、クラスの1つだけが保持されることを意味します。結局のところ、同じディレクトリに同じ名前のファイルが2つ存在することはありえません。
例えば、'jackson-datatype-jsr310'は'jackson-core'に依存し、'jackson-databind'を通して'jackson-core'にも依存している。このチェーンのどこかに、多くの一般的なライブラリで使われているSLF4Jロギングフレームワークへの依存関係もある。Mavenは、依存関係の階層をフラットにするときに、重複しているクラスをすべてハイライトします。
問題の発生場所
警告にあるように、通常はこれらは問題ではない。もしそうだとしたら、この記事のような依存関係を持つ最も単純なプロジェクトでさえ、ビルドを自動化するのは不可能ではないにせよ、非常に難しくなるだろう。
どのバージョンのクラスを使用するか(つまり、どの依存関係から選ぶか)を選択する戦略は、クラスのロード順序に依存し、詳細はこの記事の範囲を超えていますが、最終的には、Javaアプリケーションの任意のランタイム・インスタンスには、完全修飾クラスのインスタンスが1つしか存在しないということです。複数のバージョンが必要な場合、Shadeプラグインには設定可能な 再配置戦略があるので、それらを共存させることができる。
それを踏まえた上で、どのような場合に問題があるのだろうか?デフォルトですべてがうまくいくのであれば、どのような場合に失敗するのだろうか?それを示すために、先ほどの例に戻ろう。
では、次の依存関係を <dependencies>セクションに pom.xml:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.0-alpha1</version>
</dependency>
で再構築し mvn clean installでリビルドし、そのJARファイルを使ってプログラムを実行しようとすると、次のようなエラーが発生する:
スレッド "main" での例外 java.lang.NoClassDefFoundError: org/apache/http/conn/HttpClientConnectionManager
これは、SDKのコアとなる依存関係の1つを、依存するクラスを含まない古いバージョンでオーバーライドしているためです。IDEから再実行しても同じです。代わりに別の依存関係を試してみてください。例えば
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.0.0</version>
</dependency>
この場合、次のようなエラーが発生する:
スレッド "main" での例外 java.lang.NoClassDefFoundError: com/fasterxml/jackson/core/util/JacksonFeature
Vonage Java SDKが'jackson-core'に直接依存しないにもかかわらず、アプリケーションに含まれるライブラリのバージョン(したがって、どのクラスファイル)をオーバーライドしているため、問題が発生します。
ランタイムとコンパイル
このような誤った依存関係があるにもかかわらず、私たちのプログラムはうまくビルドできたのに、実行時に問題が発生したのはなぜだろうと不思議に思うかもしれない。これは、依存関係がコンパイル時に解決される方法と、実行時に解決される方法に起因している。
コンパイル時には、必要な依存関係はすでにすべて揃っている。結局のところ、Vonage Java SDKはすでにビルドされ、動作することが証明されているので、クラスを再コンパイルする必要はなく、既存のJARからロードしている。しかし、実行時には、実行時に使用されるクラスを置き換えます。
クラス・ファイルはコンパイル段階ですでに生成されているが、実行時には異なるバージョンのクラスがクラスパス上に存在する可能性がある。
これは、実行時の依存関係の問題を理解する鍵だ。ほとんどの場合、クラスパスが誤って設定されていることが原因だ。MavenやGradleのようなビルドシステムには、これを助けるための賢明なデフォルトや設定オプションがあるが、上に示したような使い方を誤るとエラーにつながる。依存性宣言のさまざまなスコープを理解する価値はある。
Maven のデフォルトでは、依存関係は実行時とコンパイル時の両方でインクルードされるようにスコープされます。 compile.スコープについての詳細は 公式ドキュメント.同様に Gradle では、デフォルトのスコープは implementationです (例えば compileOnly).
用語は異なりますが、依存関係のスコープとプロパゲーションは、依存関係、ひいてはアプリケーションのクラスパスを管理するためにビルドシステムに依存する場合に理解すべき重要な概念です。
シンプルな解決策
この記事のタイトルは、依存関係の問題を素早く解決することを約束したものだ。実際のところ、この問題は最終的にはクラスパスの設定ミスと関係しているのですが、修正方法はプロジェクトとその依存関係の複雑さに大きく依存します。
この mvn dependency:treeコマンド(および他のビルド・ツールの同様のもの)は、ネストした依存関係や競合する可能性のある依存関係を特定するのに役立ちますが、最終的には、競合につながる可能性があるため、依存する他のライブラリが使用する依存関係をオーバーライドしていないことを確認するのはあなた次第です。
時には、特定の依存関係間の相互作用が問題を引き起こすことがある。そのため、最初に呼び出すポイントは、どの依存関係に共通点があり、バージョンの不一致があるかどうかを把握することである。他のすべての依存関係と互換性のあるバージョンはありますか?もしそうなら、それを明示的に宣言してください。
しかし、そもそもコンフリクトを最小限に抑える最善の方法は、自分が直接使う依存関係だけを宣言することです。通常、プロジェクトで宣言する依存関係の数は最小限にし、必要なものだけを使うのがベストプラクティスです。
プロジェクトに依存関係を追加すればするほど、最終的な成果物は肥大化し、依存関係の衝突から発生する可能性のある問題が増えていきます。言うまでもなく、依存関係はアプリケーションのセキュリティを低下させます。
例えば、あなたがあるライブラリ(ライブラリA)に依存しており、そのライブラリはセキュリティー上の欠陥がある別のライブラリ(ライブラリB)の古いバージョンに間接的に依存しているとします。もしライブラリAのメンテナがライブラリBの最新バージョンにアップデートしなければ、その欠陥はあなたのアプリケーションにも影響を与えるかもしれません。この問題に対処するには、ライブラリBの最新バージョンを直接 pom.xmlまたは build.gradle.しかし、この方法は互換性を保証しませんし、責任をライブラリAのメンテナからあなたに転嫁することになります。
たいていの場合、開発者として、推移的依存関係を通じてアプリケーションに含まれるすべてのクラスについて知ることも気にすることもないでしょう。つまり、何かが壊れるまでは。
それから、クラスパスを調べる必要がある。いくつかのツールが役に立つ。例えば、Mavenは 同名のプラグイン.これにより、ビルドでどのような設定が使われているのか、より明確に把握できるようになる(複雑なプロジェクトで役立つ)。
しかし、おそらく最も役に立つのは、依存関係グラフを理解することだろう。ここで mvn dependency:treeコマンドは、すべてのライブラリとそのスコープを明確に示してくれる。スコープを持つライブラリを探し runtimeスコープを持つものを探し、競合の可能性があるかどうかを確認する。他のビルド・ツールにも似たような仕組みがある。例えば gradle dependenciesGradleの dependencytreeIvy などです。この情報を使って、競合しそうな依存関係を確認することができます。
サインオフ
今回は以上である!この記事があなたにとって有益で役に立ったことを願っています。重要なことは 実行時の依存関係の問題は、クラスパスの設定ミスの結果である。つまり、何かが欠けているか、間違ったバージョンのものが使われているということです。依存関係のスコープと、推移的依存関係間の潜在的な競合を理解することが、真相を究明するのに役立ちます。
ご意見、ご感想がありましたら、お気軽にX(旧名 ツイッターまたは コミュニティ・スラック.この記事を楽しんでいただけたなら、私の他の Javaの記事.