これは、なにをしたくて書いたもの?
GraalVMを使ってネイティブイメージを作る時に、リソースの取得(Class.getResourceやClass.getResourceAsStream)に
制限があるという話なのですが。
このあたりに書かれている内容を実践すると、それが可能になるようなので。
https://github.com/oracle/graal/blob/vm-19.0.2/substratevm/RESOURCES.md
ちょっと試してみようかなと。
環境
今回の環境は、こちらです。
$ java -version openjdk version "1.8.0_212" OpenJDK Runtime Environment (build 1.8.0_212-20190523183340.buildslave.jdk8u-src-tar--b03) OpenJDK 64-Bit GraalVM CE 19.0.2 (build 25.212-b03-jvmci-19-b04, mixed mode) $ native-image --version GraalVM Version 19.0.2 CE $ mvn -version Apache Maven 3.6.1 (d66c9c0b3152b2e69ee9bac180bb8fcc8e6af555; 2019-04-05T04:00:29+09:00) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 1.8.0_212, vendor: Oracle Corporation, runtime: /usr/local/graalvm-ce/jre Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "4.15.0-51-generic", arch: "amd64", family: "unix"
お題
簡単なJavaアプリケーションを作り、クラスパス上にいくつかリソースファイルを用意して、その内容を読み込むようにします。
これを、ネイティブイメージに変換して確認してみましょう。
プログラムとリソースファイルを用意する
いくつか、src/main/resources配下にファイルを用意してみます。
src/main/resources/configuration.properties
message = Hello World!!
src/main/resources/foo.properties
foo = bar
src/main/resources/hoge.txt
Hoge
src/main/resources/sub/app.properties
app = sub / app.properties
src/main/resources/sub/fuga.txt
Fuga
これらのファイルを読み込んで、内容を出力するだけのクラスを用意します。
src/main/java/org/littlewings/App.java
package org.littlewings; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.Properties; public class App { public static void main(String... args) throws IOException { try (InputStream is = App.class.getClassLoader().getResourceAsStream("configuration.properties")) { System.out.println("=== load properties = configuration.properties"); if (is != null) { Properties properties = new Properties(); properties.load(is); System.out.printf(" message = %s%n", properties.getProperty("message")); } else { System.out.println("=== configuration.properties not found"); } } try (InputStream is = App.class.getClassLoader().getResourceAsStream("foo.properties")) { System.out.println("=== load properties = foo.properties"); if (is != null) { Properties properties = new Properties(); properties.load(is); System.out.printf(" foo = %s%n", properties.getProperty("foo")); } else { System.out.println("=== foo.properties not found"); } } try (InputStream is = App.class.getClassLoader().getResourceAsStream("hoge.txt")) { System.out.println("=== load txt = hoge.txt"); if (is != null) { try (InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8); BufferedReader reader = new BufferedReader(isr)) { System.out.println(" " + reader.readLine()); } } else { System.out.println("=== hoge.txt not found"); } } try (InputStream is = App.class.getClassLoader().getResourceAsStream("sub/app.properties")) { System.out.println("=== load properties = sub/app.properties"); if (is != null) { Properties properties = new Properties(); properties.load(is); System.out.printf(" app = %s%n", properties.getProperty("app")); } else { System.out.println("=== sub/app.properties not found"); } } try (InputStream is = App.class.getClassLoader().getResourceAsStream("sub/fuga.txt")) { System.out.println("=== load txt = sub/fuga.txt"); if (is != null) { try (InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8); BufferedReader reader = new BufferedReader(isr)) { System.out.println(" " + reader.readLine()); } } else { System.out.println("=== sub/fuga.txt not found"); } } } }
なお、ファイルが見つからなかったら「not found」というようにしています。
プロジェクト構成は、こんな感じです。
$ find src pom.xml -type f src/main/resources/foo.properties src/main/resources/configuration.properties src/main/resources/sub/app.properties src/main/resources/sub/fuga.txt src/main/resources/hoge.txt src/main/java/org/littlewings/App.java pom.xml
Mavenビルドして
$ mvn compile
まずはJavaで確認。各ファイルの中身を参照できていることを確認します。
$ java -cp target/classes org.littlewings.App === load properties = configuration.properties message = Hello World!! === load properties = foo.properties foo = bar === load txt = hoge.txt Hoge === load properties = sub/app.properties app = sub / app.properties === load txt = sub/fuga.txt Fuga
続いて、ネイティブイメージを作成してみます。
$ native-image -cp target/classes org.littlewings.App app-launcher Build on Server(pid: 29064, port: 34033) [app-launcher:29064] classlist: 346.26 ms [app-launcher:29064] (cap): 1,311.66 ms [app-launcher:29064] setup: 2,094.39 ms [app-launcher:29064] (typeflow): 3,620.20 ms [app-launcher:29064] (objects): 2,350.94 ms [app-launcher:29064] (features): 198.84 ms [app-launcher:29064] analysis: 6,348.38 ms [app-launcher:29064] (clinit): 147.51 ms [app-launcher:29064] universe: 380.39 ms [app-launcher:29064] (parse): 535.51 ms [app-launcher:29064] (inline): 1,173.65 ms [app-launcher:29064] (compile): 6,465.35 ms [app-launcher:29064] compile: 8,798.08 ms [app-launcher:29064] image: 1,092.35 ms [app-launcher:29064] write: 217.22 ms [app-launcher:29064] [total]: 19,388.52 ms
実行。
$ ./app-launcher === load properties = configuration.properties === configuration.properties not found === load properties = foo.properties === foo.properties not found === load txt = hoge.txt === hoge.txt not found === load properties = sub/app.properties === sub/app.properties not found === load txt = sub/fuga.txt === sub/fuga.txt not found
一気にファイルが読めなくなりました。
さて、これをなんとかしましょう。
「-H:IncludeResources」オプションを使う
ネイティブイメージにリソースファイルを含めるようにするには、以下のドキュメントを参考にしてオプションでファイルを指定します。
https://github.com/oracle/graal/blob/vm-19.0.2/substratevm/RESOURCES.md
ここでは、まず「-H:IncludeResources」オプションを使って、正規表現で対象のファイルを指定してみます。
こんな感じで、「.properties」拡張子のファイルをターゲットにしてみましょう。
$ native-image -cp target/classes -H:IncludeResources='.*.properties$' org.littlewings.App app-launcher
すると、「.properties」なファイルが読めるようになりました。
$ ./app-launcher === load properties = configuration.properties message = Hello World!! === load properties = foo.properties foo = bar === load txt = hoge.txt === hoge.txt not found === load properties = sub/app.properties app = sub / app.properties === load txt = sub/fuga.txt === sub/fuga.txt not found
「.txt」拡張子の方も含めたいところですね。ひとつの「-H:IncludeResources」オプション内で正規表現のOR(|)を使ってもいいですし、
「-H:IncludeResources」オプションを複数回指定してもOKです。
今回は、「-H:IncludeResources」オプションを複数回指定することにします。
$ native-image -cp target/classes -H:IncludeResources='.*.properties$' -H:IncludeResources='.*.txt$' org.littlewings.App app-launcher
確認。「.properties」、「.txt」な拡張子のファイルの両方が読めるようになりました。
$ ./app-launcher === load properties = configuration.properties message = Hello World!! === load properties = foo.properties foo = bar === load txt = hoge.txt Hoge === load properties = sub/app.properties app = sub / app.properties === load txt = sub/fuga.txt Fuga
「-H:ResourceConfigurationFiles」オプションを使う
もうひとつの方法は、「-H:IncludeResources」で指定していた内容を、JSONの設定ファイルとして用意する方法です。
https://github.com/oracle/graal/blob/vm-19.0.2/substratevm/RESOURCES.md
2つの正規表現を指定して、こんなファイルを作成。
src/main/resources/resources-config.json
{ "resources": [ { "pattern": ".*.properties$" }, { "pattern": ".*.txt$" } ] }
このファイルの絶対パスを、「-H:ResourceConfigurationFiles」オプションで指定してビルド。
$ native-image -cp target/classes -H:ResourceConfigurationFiles=`pwd`/src/main/resources/resources-config.json org.littlewings.App app-launcher
実行。この方法でも、リソースファイルが読めるようになったことが確認できます。
$ ./app-launcher === load properties = configuration.properties message = Hello World!! === load properties = foo.properties foo = bar === load txt = hoge.txt Hoge === load properties = sub/app.properties app = sub / app.properties === load txt = sub/fuga.txt Fuga
ちょっと試しに、「.txt」拡張子のパターンの方を外してみましょう。
src/main/resources/resources-config.json
{ "resources": [ { "pattern": ".*.properties$" } ] }
ネイティブイメージビルド後、「.txt」拡張子のファイルが読めなくなったことが確認できました。
$ ./app-launcher === load properties = configuration.properties message = Hello World!! === load properties = foo.properties foo = bar === load txt = hoge.txt === hoge.txt not found === load properties = sub/app.properties app = sub / app.properties === load txt = sub/fuga.txt === sub/fuga.txt not found
「-H:IncludeResources」や-H:ResourceConfigurationFiles」で指定するパターンは、どのパス?
「-H:IncludeResources」や-H:ResourceConfigurationFiles」でネイティブイメージに含めるファイルを正規表現で指定する
わけですが、その評価対象のパスはどうもクラスパスやJARファイルの中身が対象になるようですね。
Maven Shade PluginでJARファイルを対象にしてみる
JARファイルも対象になるということなので、JARファイルをネイティブイメージにする時など、JARファイルに含まれる
リソースファイルを使ったパターンもちょっと試してみましょう。
pom.xmlに、Maven Shade Pluginの設定を追加。
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.2.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>org.littlewings.App</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build>
パッケージング。
$ mvn package
単体で動作することを確認。
$ java -jar target/native-image-include-resource-files-0.0.1-SNAPSHOT.jar === load properties = configuration.properties message = Hello World!! === load properties = foo.properties foo = bar === load txt = hoge.txt Hoge === load properties = sub/app.properties app = sub / app.properties === load txt = sub/fuga.txt Fuga
続いて、JARファイルを指定しつつ、今回は「-H:IncludeResources」オプションを使ってネイティブイメージを作成します。
$ native-image -H:IncludeResources='.*.properties$' -H:IncludeResources='.*.txt$' -jar target/native-image-include-resource-files-0.0.1-SNAPSHOT.jar app-launcher
もちろん、「-H:ResourceConfigurationFiles」オプションを利用してもOKです。
$ native-image -H:ResourceConfigurationFiles=`pwd`/src/main/resources/resources-config.json -jar target/native-image-include-resource-files-0.0.1-SNAPSHOT.jar app-launcher
確認。
$ ./app-launcher === load properties = configuration.properties message = Hello World!! === load properties = foo.properties foo = bar === load txt = hoge.txt Hoge === load properties = sub/app.properties app = sub / app.properties === load txt = sub/fuga.txt Fuga
リソースファイルが読み込めました。
一応、オプションを外してみたパターンも確認してみましょう。
$ native-image -jar target/native-image-include-resource-files-0.0.1-SNAPSHOT.jar app-launcher
リソースファイルが読み込めなくなっていることが確認できました、と。
$ ./app-launcher === load properties = configuration.properties === configuration.properties not found === load properties = foo.properties === foo.properties not found === load txt = hoge.txt === hoge.txt not found === load properties = sub/app.properties === sub/app.properties not found === load txt = sub/fuga.txt === sub/fuga.txt not found