この記事は、https://dev.to/autonomousapps/build-compile-run-a-crash-course-in-classpaths-f4g を簡易翻訳したものです。
私は "グラドル見習い "と呼んでるものがある。ある仕事をしていた時、Gradleで1年間働き、私たちの世代で最も偉大なビルドマインドに直接アクセスすることができました。私はその山頂から秘密を持って帰ってきました。実際、すべてのものがclasspathなんだよ。じゃあ下のは?ただのクソコードだ。
javac -classpath barks.jar:drool.jar:poop.jar Dog.java
おっと。
けどねえ、これはJavaやJavaに隣接する開発におけるclasspathの重要性を過大評価することがいかに難しいかを表してるんだ。私はclasspathを理解し始めるのに4年かかり、この記事を書くのにさらに2年かかったんだよ。朝のコーヒーを飲みながらこの記事を7分で読むことができ、Jiraボードから来るタスクを避けることができるんだ。
##誰のための記事なのか
JavaやKotlinやGroovyなどのJVM言語でコードを書いている場合、また、JVMやAndroidをターゲットにしているかどうかに関わらず、この記事はあなたの役に立つかもね。classpathを理解することは、厄介なバグを理解して回避するのに役立つし、JVM言語への理解が深まり、上級プログラマーへの近道になること間違いない。
これらは私が最もよく知っているドメインであり、(私にとって)例として説明しやすい。また、classpathを可視化するための非常に優れたツールであるGradleのビルドスキャンも見てみましょう。
##何をカバーするのか
この記事では、JavaにおけるclasspathとClassLoaderの基本に焦点を当て、今後の記事では、ビルド時、コンパイル時、ランタイムでのclasspathについて具体的に書きます。
##classpathとは?
classpathとは、SDKツール(2)(java
やjavac
など)やSDKツールを包んでいるビルドツール(Ant、Maven、Gradleなど)に、拡張機能やコアJDKの一部ではないサードパーティのクラスやユーザー定義のクラスをどこで見つけるかを伝える方法です。(公式ドキュメント)
第三者の場合、クラスは一般的にライブラリまたは依存関係と呼ばれ、通常はjarとして利用可能です。ユーザー定義クラスは、あなたとあなたのチーム、つまりあなたのアプリによって書かれたクラスです。これを、java.lang.String
などのコアJavaプラットフォームクラスと対比してみましょう。(これらのプラットフォーム・クラスは、boot classpath上にあると考えることができます)。
以下で詳しく説明しますが、classpathは、クラスファイルを含む jar ファイルとディレクトリを並べただけのリストです。上のDog.java
の例を思い出すと、コンパイル操作のためのクラスパスは、文字通り、barks.jar、drool.jar、poop.jarの順に指定された3つのjarファイルです。
もちろん、現代のJVM開発者としては、javac
(Javaコンパイラ)のようなツールと直接やりとりすることはほとんどありません。同様に、一般的には、classpathを直接指定することはありません。依存関係の管理と解決は、それ自体が 信じられないほど複雑なトピック であり、私たちはほとんど省略します。これだけは言っておきます: gradle ビルドスクリプトで依存関係を宣言すると、ツールはそれを依存関係を解決するための命令として扱います (多くの場合、インターネットからのダウンロードを含みます)。例えば、典型的な Android プロジェクトで以下のように宣言したとします。
dependencies {
implementation 'androidx.appcompat:appcompat:1.1.0'
}
その結果(3)、Androidx appcompat lib v1.1.1.0 が jar ファイル(4)で表現され、コンパイル時と実行時のclasspathに追加されます(5)。
さらに、appcompat のすべての依存関係(およびその依存関係)は、遷移依存関係とも呼ばれ、同じclasspathに追加されます。以下は appcompat のトップレベルの依存関係を示す build scanの一部です。
上述のように、gradle の依存関係解決エンジンの複雑さには触れませんので、constraint
ラベルのついた最初の 3 つの依存関係についてもっと理解したい場合は、docsを参照してください。
ビルドスキャンでは依存関係がツリー構造に整理されて表示されますが、 gradle が実際に関連するコンパイルタスクを起動すると、このツリーは単純なリストに平坦化されます。
要約すると
依存関係を宣言することは、機能的には 1 つ以上のclasspathに 1 つ以上の jar を追加することと同等です。
classpathが Java ランタイムにどのような影響を与えるかを理解するためには、クラスの読み込みについて話をしなければなりません。ここでは、クラスローディングについて説明します。
##ClassLoaderはクラスを読み込む
クラスローディングと ClassLoader
クラスの優れた入門書として、私は このチュートリアル by Baeldung をお勧めします。ここでは要点をまとめておきます。
Javaアプリケーションで使用されるビルトインClassLoaderは3つあります。
-
ブートストラップClassLoader
java.lang.String
やjava.util.ArrayList
などのJDKクラスを読み込むために使用されます。 - 拡張ClassLoader 拡張クラスをロードするために使用されます(この投稿の範囲外)。
- アプリケーションまたはシステムClassLoader これは、ユーザー定義のクラスパスで構成されているクラス・ローダです。
また、実行時にカスタムClassLoaderを定義することもできます。
以下の簡単な例 (Baeldung チュートリアルからの引用) は、1 番目と 3 番目のタイプを示しています。
public class Main {
public static void main(String... args) {
System.out.println("Main class loader = " + Main.class.getClassLoader());
System.out.println("String class loader = " + String.class.getClassLoader());
}
}
このプログラムを実行すると、以下のように吐き出されます。
Main class loader = sun.misc.Launcher$AppClassLoader@7852e922
String class loader = null
はい、ここでString
class loaderが null
と表示されていることに注目。このclass loaderはネイティブ・コードで書かれているので、Javaクラスとしての表現はありません。
このプログラムをコンパイルして実行するには、
javac Main.java && java Main
を実行します。
これらのclass loaderは階層構造になっていて、bootstrap class loaderが拡張class loaderの親として機能し、それ自体がアプリケーションclass loaderの親になっています。class loaderは、あるクラスを読み込むように要求されると、その親に委任します。その親は、さらにその上の親に委任します。親がクラスを読み込むことができない場合は、その直後の子がそれを試みるなどして、最終的にクラスが読みこまれるか ClassNotFoundException
または NoClassDefFoundError
がスルーされるまで 依存 します。
すべての Class
インスタンスは、それを読み込んだ ClassLoader
への参照を持ち、それは Class.getClassLoader()
メソッドによって取得されます。
クラスパスは、アプリケーションのclass loaderを設定するために使用され、その情報を使用して実行時にプログラムが要求したクラスを読み込みます。
##classpathは順序に敏感で、重複エントリにも耐性があります。
これで、classpathが Java または Java 隣接アプリケーションにどのように影響を与えるかがわかりました。classpathは、アプリケーションclass loaderに、サードパーティのライブラリやユーザー定義クラスを見つける方法を指示します。したがって、classpathは、Javaランタイムに非常に深いレベルで影響を与えます。
Javaの興味深いところは、負荷の高い用語を多用すると、驚くほど動的であることです。例えば、classpathにちょっとした変更を加えるだけで、2つの連続した操作の間で根本的に異なる動作をすることができます。
classpathは、class
ファイルを提供する要素の順序付きリスト
と考えることができます。
これらのクラスファイルは、ビルドのオーケストレーション、コンパイル、プロジェクトの実行などの操作に使用されます。
ほとんどの場合、classpathから要素を削除すると、指定した操作が失敗するだけですが、それ以外のケースでは、実際には異なる動作を見ることができるかもしれません。また、classpathは順序に敏感であることを理解することも非常に重要です。最初の例をもう一度考えてみましょう。
javac -classpath barks.jar:drool.jar:poop.jar Dog.java
これが
javac -classpath drool.jar:barks.jar:poop.jar Dog.java
これに?
たぶん何も変わらないよ。でももし、 drool.jar
と barks.jar
のクラスファイルが重複していたとしたら (パッケージの間違いやリファクタリングの中間段階、アーキテクチャの問題などの理由で)、最初のケースでは barks.jar
のクラスファイルを使用し、2番目のケースでは drool.jar
のクラスファイルを使用することになり、これらは異なるものになるかもしれません。このことは、classpathが重複したエントリにも耐性があることを暗黙のうちに示しています。SDKツールは、リストの前の方にあるものと重複するエントリを単純に無視します。
重要:Gradleはclasspathの順序が決定論的であることを保証します。
次の記事では、ビルドドメインの例を使ってそれを利用します。
##まとめ(今のところ)
この記事では、classpathとは何か、Javaランタイムがアプリケーションclass loaderを設定するために使用するものであること、class loaderはboot class loaderをルートとする階層に編成されていること、そしてclasspathは順序に依存しており、プログラム(ビルド・プログラムを含む)のランタイム動作に大きな意味を持つことを学びました。
これらの基本的なことを理解したところで、ビルド、コンパイル、実行の各ドメインから、より実践的な実世界の例を探っていきましょう。
それでは、またどこかでお会いしましょう!
##感謝
César Puertaに感謝します。しかし、すべての間違いは私自身のものです!
##巻末注
- 私が猫派であることがバレバレ?
- この芸術用語はOracleのドキュメントから拝借しています。
- コンプレックスって言ったでしょ?
- appcompat は実際には aar としてパッケージ化されていると理解していますが、AGP が行うことの一つは、その aar を解凍して、その中の jarをclasspathに置くことです。
- なぜ複数のclasspathにあるのかは、今後の記事で説明します。