JEP 238: Multi-Release JAR Files


概要

JARファイル・フォーマットを拡張し、複数バージョンのクラス・ファイルを単一のアーカイブに共存できるようにする。


ゴール

  • Java Archive Tool(jar コマンド) を拡張し、マルチリリース Jar ファイルを作成できるようにする
  • 標準クラスローダーと JarFile API でのサポートを含め、JRE にマルチリリース Jar ファイルを実装する
  • 他の重要なツール (javacjavapjdeps など) を強化して、マルチリリース Jar ファイルを解釈できるようにする
  • マルチリリース・モジュラー Jar ファイルをサポートする
  • マルチリリース Jar ファイルを使用するツールやコンポーネントのパフォーマンスに、大きな影響を及ぼさない


動機

サードパーティのライブラリやフレームワークは、通常、Javaプラットフォームの様々なバージョンをサポートしている。 そのため、新しいリリースで利用可能な言語やAPIの機能の利用に消極的になることがある(リフレクションを伴う条件付きプラットフォーム依存性や、バージョンに対して異なるライブラリの成果物を配布したりすることが難しいため)。

このような状況は、ライブラリやフレームワークが新しい機能を使用する意欲をなくし、その結果、ユーザーが新しいJDKバージョンにアップグレードする意欲もなくすという悪循環を生みだす。

さらに、一部のライブラリやフレームワークはJDKの内部APIを使用しており、モジュールの境界が厳格に適用されるJava 9ではアクセスできなくなるため、内部APIのパブリックでサポートされたAPIの代替がある場合、新しいプラットフォームのバージョンをサポートする阻害要因になる。


ディスクリプション

Jar ファイルには、クラスとリソースを含むコンテンツルートと、Jar に関するメタデータを含むMETA-INFディレクトリがある。

特定のファイル群にバージョン管理メタデータを追加することで、Jar フォーマットは、互換性のある方法で、ターゲットとするJavaプラットフォームのリリースごとに複数バージョンのライブラリをエンコードすることができる。

マルチリリースJAR(MRJAR)の MANIFEST.MF には、以下の main 属性を含む(属性名は定数 java.util.jar.Attributes.MULTI_RELEASE としても宣言されている)。

Multi-Release: true

マルチリリースJar(MRJAR)には、特定のJavaプラットフォーム・リリースに固有のクラスやリソースのための追加ディレクトリが含まれる。

jar root
  ├ A.class
  ├ B.class
  ├ C.class
  └ META-INF
     ├ MANIFEST.MF
     └ versions
        ├ 9
        │  ├ A.class
        │  └ B.class
        └ 10
           └ A.class

MRJARをサポートしていないJDKでは、ルート・ディレクトリのクラスとリソースのみが表示され、従来通りに扱われる。

MRJARをサポートしているJDKでは、プラットフォームのバージョン以降ディレクトリは無視される。 クラスとリソースの検索は、最初に現在実行されているJavaプラットフォーム・リリース・バージョンに対応するディレクトリを検索する。 次にそれ以下のバージョンを検索していき、最後にJARルートを検索する。 これにより、後のJavaプラットフォーム・リリース用に設計されたクラスのバージョンは、以前のJavaプラットフォーム・リリース用に設計された同じクラスのバージョンをオーバーライドすることとなる。

上の例では、MRJAR 対応の Java 9 JDK で実行すると、9固有のバージョンの A.classB.class、一般的なバージョンの C.class が表示される。 Java 10 JDKでは、10固有のバージョンの A.class と9固有のバージョンの B.class が表示されます。

MANIFEST.MFファイルやMETA-INF/servicesディレクトリにあるようなJARのメタデータはバージョン管理する必要はない。 MRJAR は基本的に1つのリリース単位なので、内部的には異なるJavaプラットフォームのリリースで使用するための複数のバージョンのライブラリ実装が含まれていても、リリースバージョンは1つだけとなり、Maven Central 経由で配布される通常の Jar と変わりない。

ライブラリの各バージョンは、同じAPIを提供する必要がある。

このメカニズムによって、ライブラリとフレームワークの開発者は、特定のJavaプラットフォーム・リリース・バージョンのAPIの使用を、すべてのユーザーがそのバージョンに移行するという要件から切り離すことができる。ライブラリとフレームワークのメンテナは、古い機能のサポートを継続しながら、新しい機能に徐々に移行し、サポートすることができる。


詳細

マルチリリースJARファイルをサポートするために、JDKの以下のコンポーネントが変更される。

  • Jar ベースの URLClassLoader は、実行中の Java プラットフォームのバージョンによって選択されたバージョンのクラスファイルを読み込む必要があり、JPMSのモジュールベースのクラスローダーも同様である
  • jar URL スキームと java.util.jar.JarFile クラスのプロトコルハンドラは、マルチリリース JAR から適切なバージョンのクラスを選択しなければならない
  • Java コンパイラ (javac) は、基礎となる JavacFileManagerZipFileSystem API を介して、 -target-release コマンドラインオプションで指定されたバージョンのクラスファイルを読み込む必要がある(ツール javahschemagenwsgenJavacFileManagerZipFileSystem の根本的な変更を利用する)
  • Java Archive ツール (jar) は拡張され、マルチリリース JAR ファイルを作成できるようになる
  • Jar パッキングツール (pack200/unpack200) を更新する必要がある(JDK-8066272)
  • javap ツールを更新して、バージョン管理されたクラスファイルを選択できるようにする
  • jdeps ツールは、バージョン情報を表示し、バージョン固有のクラスファイルの依存関係に従うように修正する
  • Jar仕様は、マルチリリースJARファイルフォーマットと関連する変更の記述のために改訂する

互換性

デフォルトでは、java.util.jar.JarFilejar スキームプロトコルハンドラの動作は変わらない。 エントリのバージョン選択のためにMRJARを指す JarFile を作成するには、オプトインする必要がある。同様に、jar URL のオプトインも必要である。

クラスロードのためにランタイムが作成する JarFile インスタンスは、実行中の Java プラットフォームのバージョンに応じてエントリを選択するように設定されたインスタンスを作成する。このような JarFile インスタンスは、ランタイム・バージョン管理されていると呼ばれる。

Class loader resources

クラスローダが生成するリソースURLは、MRJAR内のリソースを識別するために、バージョン管理されたエントリを直接参照する。 例えば、バージョン管理されたリソースが foo/baz/resource.txt であった場合

URL r = loader.getResource("foo/baz/resource.txt");

URL は以下のようになる(jar:file:/mrjar.jar!/foo/baz/resource.txt ではない)。

jar:file:/mrjar.jar!/META-INF/versions/9/foo/baz/resource.txt

このような変更は、最も破壊的でないオプションと考えられる(レガシーコードにおいて、URL の文字を直接処理していた場合、これは破壊的ケースとなる)。

モジュラー・マルチリリース Jar ファイル

モジュラー・マルチリリース Jar ファイルは、モジュラー JAR ファイルのように、先頭のルートに module-info.class というモジュール記述子を持つマルチリリース JAR ファイルである。 モジュール記述子はバージョン管理された領域に存在してもよい。 このようなバージョン記述子は、2つの例外を除いて、ルート・モジュール記述子と同一でなければならない。

  • バージョン管理されたディスクリプタは、java.* モジュールと jdk.* モジュールの requires 節が異なる非 transitive を持つことができる
  • バージョン管理された記述子は、java.*jdk.* モジュールの外部で定義されたサービスタイプであっても、異なる uses 節を持つことができる

この理由は、これらはモジュールのAPIサーフェスの一部というよりもむしろ実装の詳細であり、JDK自体が進化するにつれて変更したくなるかもしれないためである。 JDK 以外のモジュールの非公開の requires に対する変更は許可されない。 もしそれが必要であれば、新しいバージョンのモジュールが必要になり、これは互換性の問題とは異なる種類のものであるため、MRJAR の範囲外である。

マルチリリースモジュールはルートにモジュール記述子を持たなくても良い(この点で、モジュール記述子は他のクラスやリソースファイルと同じように扱われる)。 これにより、例えば、Java 8 バージョンのクラスだけがルート領域に存在し、Java 9 バージョンのクラス(モジュール記述子を含む)が 9 バージョンの領域に存在することもできる。

jar root
  ├ A.class
  └ META-INF
     ├ MANIFEST.MF
     └ versions
        └ 9
           └ module-info.class

Classpath と modulepath

モジュール式JARは、Java 8ランタイムのクラスパス、Java 9ランタイムのクラスパス、Java 9ランタイムのモジュールパスで正しく動作するように構築することができる。モジュール化されたマルチリリースJARファイル(module-info.classに加えて、他のクラスがJava 9プラットフォーム用にコンパイルされているかもしれない)の場合も状況は同じである。

モジュール記述子がいくつかのパッケージのエクスポートを宣言しておらず、そのためそれらのパッケージのパブリッククラスがモジュールのプライベートになっている場合、対応するJarファイルがモジュールパスに置かれると、そのクラスにはアクセスできなくなる。 しかし、Jarファイルがクラスパスに置かれると、それらのクラスにアクセスできるようになる。 これは、クラスパスとモジュールパスをサポートすることの残念な結果である。

結果として、マルチリリースJARファイルのパブリックAPIは、クラスパス上に置かれた場合とモジュールパス上に置かれた場合とで異なる可能性がある。 通常、マルチリリースJARファイルを構築する際の jar ツールは、パブリックAPIの違いが検出された場合、ベストエフォートで失敗する。 しかし、モジュール式のマルチリリースJARファイルを構築する場合、Jar ファイルがクラスパスに配置されたときに、パブリックAPIの違いがモジュールのプライベートクラスにアクセス可能な結果である場合、jarツールは警告を出力することを提案する。

マルチリリース Jar とブートローダ

マルチリリースJARはブートローダーによってサポートされていない(例えば、マルチリリースJARファイルが-Xbootclasspath/aオプションで宣言されている場合)。 このようなケースは稀なユースケースであり、ブートローダーの実装を複雑にしてしまう。