Java ランタイムイメージ用の新しいURLスキーム jrt について


はじめに

Java でリソースにアクセスする場合、filejar といった URLスキームで参照することができます。

file:/path/to/resource.txt              // ファイルシステム上のリソース
jar:file:/path/to/foo.jar!/resource.txt // jarに固められたリソース

モジュールシステムが導入されたモジュールシステムが導入された Java9 からは、上記以外に jrt というURLスキームを考慮する必要があります。


jrt というURLスキームは、jlink ツールで作成したランタイムイメージ(いわゆる JIMAGE)に含まれるリソースにアクセスする際に使用します。

URLスキーム jrt を考慮しない場合、開発時は動くものの、jlink でランタイムイメージとして固めたとたんに動かなくなってしまう、ということが発生します。開発時は動いていたけど JAR に固めたら動かなくなった というのと同じですね。


例えば Playwright Java では、node の実行ファイルを JAR に固めて提供しており、実行時に JAR から実行ファイルを一時ディレクトリに抽出して利用しています。

public class DriverJar extends Driver {
  // ...
  void extractDriverToTempDir() throws URISyntaxException, IOException {
    URI originalUri = getDriverResourceURI();
    URI uri = maybeExtractNestedJar(originalUri);

    // Create zip filesystem if loading from jar.
    try (FileSystem fileSystem = "jar".equals(uri.getScheme()) ? initFileSystem(uri) : null) {
      Path srcRoot = Paths.get(uri);
      // ...
    }
  }

  public static URI getDriverResourceURI() throws URISyntaxException {
    ClassLoader classloader = Thread.currentThread().getContextClassLoader();
    return classloader.getResource("driver/" + platformDir()).toURI();
  }

  private FileSystem initFileSystem(URI uri) throws IOException {
    try {
      return FileSystems.newFileSystem(uri, Collections.emptyMap());
    } catch (FileSystemAlreadyExistsException e) {
      return null;
    }
  }

  private URI maybeExtractNestedJar(final URI uri) throws URISyntaxException {
    if (!"jar".equals(uri.getScheme())) {
      return uri;
    }
    // ...
  }
}

このコードでは、jrt スキームの考慮がないため、Playwright Java を jlink でランタイムイメージとして固めた場合に動作しなくなります。

このように、実行時にリソースを扱うライブラリを作成する場合には、jrt スキームを考慮したコードを書く必要があります。


JIMAGE って何?

Java9 では、モジュールシステムの導入に伴い、lib/rt.jar, lib/tools.jar, lib/dt.jar といった内部JAR は提供されなくなりました。

これらの内部 JAR に代わり、より効率的な形式で格納された、いわゆる JIMAGE が提供されるようになりました(ファイル形式は公開されておらず、予告なく変更されることがあります)。

具体的には、JDK ディレクトリの lib/modules というファイルが JIMAGE に該当します。

jimage コマンドで対象の modules を指定することで中身を確認することができます。

$ jimage list --verbose lib/modules | head -n 15

Module: java.base
Offset       Size       Compressed Entry
      119101         41          0 META-INF/services/java.nio.file.spi.FileSystemProvider
      119142       1357          0 apple/security/AppleProvider$1.class
      120499       2003          0 apple/security/AppleProvider$ProviderService.class

JIMAGE は、クラスファイルやその他リソースが、モジュール単位で格納されたランタイムイメージとなっています。


JIMAGE 内リソースへのアクセス

Java9 以前では、以下の様にリソースURLを取得した場合、

ClassLoader.getSystemResource("java/lang/Class.class")

lib/rt.jar に含まれる Class.class のURLが以下のように得られました。

jar:file:/usr/local/jdk8/jre/lib/rt.jar!/java/lang/Class.class


Java9 からは、lib/rt.jar は提供されず、lib/modules の中にモジュールとして格納されているため、Class.class は以下のような新しい jrt スキームとして取得されます。

jrt:/java.base/java/lang/Class.class


JMOD って何

すこし脱線しますが、混乱されがちな JIMAGE と JMOD の関係について補足しておきます。

JMOD は(JIMAGE とは異なり)、JARと同じZIP形式のファイルで、JAR には含めないような、より広範囲のリソースを集約します。

JDKでは jdk\jmods の配下に java.base.jmod といったモジュール別の JMODが提供され、java.base モジュールのクラスファイルやメタデータなどのリソースが提供されます(例えばjava.exe なども含まれています)。

JMODファイルは JARとは異なり、実行時に利用するものではなく、コンパイル時またはリンク時に使用されます。 具体的には、JMODファイル を元にして、jlink コマンドでランタイムイメージの JIMAGE ファイルを生成するといった流れになります。

実際、JDK の lib/modules は、jdk\jmods の配下JMOD ファイルを元にして生成されたものになります。

ちなみに、JDK の JMOD ファイルの内容は lib/modules に含まれるため、この重複分を削減してランタイムイメージサイズの削減を行うJEP 493: Linking Run-Time Images without JMODsが Java24 で導入されます。 この JEP は JDK ベンダー 向けのものであり、JDK ベンダー がオプションを有効にして JDK を生成した場合に、jdk\jmods 配下 JMOD を含まない JDK を生成できるといったものになります。ですので、当分の間は JMOD を含むJDK が提供されることになるでしょう。


jrt URIスキーム

jrt URL は RFC 3986 に従った階層的なURIで、次の構文を持ちます。

jrt:/[$MODULE[/$PATH]]

$MODULE はオプションのモジュール名です。$PATH は、そのモジュール内の特定のクラスまたはリソースファイルへのパスです。

  • jrt:/$MODULE/$PATH は、指定された $MODULE 内の $PATH という名前の特定のクラスまたはリソースファイルを参照する
  • jrt:/$MODULE は、モジュール $MODULE 内のすべてのクラスファイルとリソースファイルを参照する
  • jrt:/ は、現在のランタイムイメージに格納されているクラスファイルとリソースファイルのコレクション全体を参照する

前述の通り、ClassLoader::getSystemResource の呼び出しにより以下のような jrt URL が取得できます。

jrt:/java.base/java/lang/Class.class

このような URL オブジェクトの getContent メソッドは、 jrt スキーム用の組み込みプロトコルハンドラによって、指定されたクラスまたはリソースファイルのコンテンツを取得します。

セキュリティポリシーファイルやその他の CodeSource API で jrt URL を使用することができます。例えば、楕円曲線暗号プロバイダは以下のような jrt URL で識別できます。

jrt:/jdk.crypto.ec

jrt:/ URLで指定されたFileSystemをロードすることで、ランタイムイメージ内のクラスファイルとリソースファイルを列挙して読み込むことができます。

FileSystem fs = FileSystems.getFileSystem(URI.create("jrt:/"));
byte[] jlo = Files.readAllBytes(fs.getPath("modules", "java.base",
                                           "java/lang/Object.class"));

このファイルシステムのトップレベルの modules ディレクトリには、イメージ内の各モジュールごとに1つのサブディレクトリがあります。

トップレベルの packages ディレクトリには、イメージ内の各パッケージごとに 1 つのサブディレクトリがあり、そのサブディレクトリにはそのパッケージを定義するモジュールのサブディレクトリへのシンボリックリンクが含まれています。


Playwright Java の DriverJar

ということで、Playwright Java の DriverJar を雑に jrt 対応すると以下のようになります。

public class DriverJar extends Driver {
  // ...
  void extractDriverToTempDir() throws URISyntaxException, IOException {
    URI originalUri = getDriverResourceURI();
    URI uri = maybeExtractNestedJar(originalUri);

    // Create zip filesystem if loading from jar.
    try (FileSystem fileSystem = ("jar".equals(uri.getScheme()) || "jrt".equals(uri.getScheme()))
        ? initFileSystem(uri) : null) {
      Path srcRoot = Paths.get(uri);
      // ...
    }
  }

  public static URI getDriverResourceURI() throws URISyntaxException {
    ClassLoader classloader = Thread.currentThread().getContextClassLoader();
    URL url = classloader.getResource("driver/" + platformDir());
    if (url != null) {
      return url.toURI();
    } else {
      return URI.create("jrt:/driver.bundle/driver/" +  platformDir());
    }
  }

  private FileSystem initFileSystem(URI uri) throws IOException {
    try {
      if ("jar".equals(uri.getScheme())) {
        return FileSystems.newFileSystem(uri, Collections.emptyMap());
      } else if ("jrt".equals(uri.getScheme())) {
        return FileSystems.newFileSystem(URI.create("jrt:/"), Collections.emptyMap());
      }
    } catch (FileSystemAlreadyExistsException e) {
      logger.log(System.Logger.Level.WARNING, e);
    }
    return null;
  }