CLOVER🍀

That was when it all began.

GraalVMでネイティブイメージを作る時に、リソースファイルを含めるようにする

これは、なにをしたくて書いたもの?

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ファイルの中身が対象になるようですね。

https://github.com/oracle/graal/blob/vm-19.0.2/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java#L138-L162

https://github.com/oracle/graal/blob/vm-19.0.2/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ResourcesFeature.java#L184-L225

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