はじめに
Java でリソースにアクセスする場合、file や jar といった 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; }