はじめに
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; }