このエントリは以下のエントリをベースにしています。
This entry is based on the following, written by Christian Wimmer [VM and compiler researcher at Oracle Labs, and Project lead for GraalVM native image generation (Substrate VM)]
https://medium.com/graalvm/introducing-the-tracing-agent-simplifying-graalvm-native-image-configuration-c3b56c486271
tl;dr:
トレースエージェントは、GraalVMやその他の互換VM上で動作するJavaアプリケーションの挙動を記録し、リフレクションやJNI、リソース、プロキシ利用のための設定ファイルをGraalVM Native Image Generatorに提供する。
以下のコマンドで有効化する。
java -agentlib:native-image-agent=…
Introduction
GraalVMでのネイティブイメージ生成では、静的解析と事前コンパイルを適用して、Javaアプリケーション用にネイティブイメージと呼ばれる最適化されたネイティブ実行可能ファイルを構築します。これには到達可能なアプリケーションクラスの閉世界仮説(closed-world assumption)が必要です。つまり、静的解析で処理できるように、すべてのクラスはネイティブイメージ生成時にわかっている必要があります。
GraalVM Native Image
https://www.graalvm.org/docs/reference-manual/aot-compilation/
閉世界仮説はJavaのリフレクションのopen-worldアプローチと矛盾します。java.lang.reflect パッケージの機能を使い、アプリケーション開発者はクラスやメソッド、フィールドを名前で検索し、それらにアクセスしたり呼び出したりします。通常は、名前を設定ファイルから呼び出したり、実行時に動的に設定したりします。ネイティブイメージ生成にあたってもリフレクションはサポートするものの、ネイティブイメージ生成時にリフレクション対象のすべての要素をリスト化しておく必要があります。必要なJSONファイルの構造については以下のドキュメントで説明されています。
Reflection on Substrate VM
https://github.com/oracle/graal/blob/master/substratevm/REFLECTION.md
このエントリでは、Java HotSpot VM上での実行時、つまり、ネイティブ・イメージとしてではない状態でアプリケーションを実行しているときのアプリケーションの動作を観察することによって、JSONファイルを生成する新しいトレース・エージェントをご紹介します。 これは、アプリケーションの開発とテストはJava HotSpot VMを使用して行い、その後デプロイ前に最終的なアプリケーションのみをネイティブイメージに変換するという、共通のワークフローを活用します。つまり、 開発およびテスト中にJava HotSpot VMをトレースすることによって、必要なリフレクション設定ファイルを作成します。
トレースエージェントは、GraalVM Community EditionおよびGraalVM Enterprise Editionの両方に含まれています。有効化するには、以下のオプションを使います。エージェントのコマンドは以下の例で紹介します。
-agentlib:native-image-agent=...
Example
例としてリフレクションを使う最小限の”Hello, world!”アプリケーションを使います。このエントリでは環境変数JAVA_HOMEはGraalVM(19.0以後)のインストール先が指定されており、native-imageツールがインストール済みとします。
GraalVM Native Image – prerequisites
https://www.graalvm.org/docs/reference-manual/aot-compilation/#install-native-image
以下がReflection APIを使ったサンプルJavaアプリケーションです。
public class HelloReflection { | |
public static void foo() { | |
System.out.println("Running foo"); | |
} | |
public static void bar() { | |
System.out.println("Running bar"); | |
} | |
public static void main(String[] args) { | |
for (String arg : args) { | |
try { | |
HelloReflection.class.getMethod(arg).invoke(null); | |
} catch (ReflectiveOperationException ex) { | |
System.out.println("Exception running " + arg + ": " + ex.getClass().getSimpleName()); | |
} | |
} | |
} | |
} |
mainメソッドでコマンドライン引数として渡された名前の全メソッドを呼び出します。簡単のために2個のメソッド(fooとbar)だけにしておきます。コマンドラインでその他の名前を渡した場合には例外が発生します。
以下のようにサンプルを実行すると
$JAVA_HOME/bin/java HelloReflection foo xyz
以下のような結果が帰ってきます。
Running foo Exception running xyz: NoSuchMethodException
想定通り、メソッドfooがリフレクションの結果判明しましたが、存在しないメソッドであるxyzは見つかりませんでした。
前述の通り、ネイティブイメージの生成にはリフレクション設定ファイルが必要です。リフレクション設定ファイルがない場合、リフレクションを使ってメソッドfooにアクセスできません。混乱を避けるため、リフレクション設定ファイルがないのにリフレクションが使われていることがわかった場合には、 ネイティブイメージ生成ツールが検知します。例えば以下を実行した場合、
$JAVA_HOME/bin/native-image HelloReflection
アプリケーションのネイティブイメージではなく、いわゆるフォールバックイメージだけが生成されます。
… Warning: Reflection method java.lang.Class.getMethod invoked at HelloReflection.main(HelloReflection.java:14) Warning: Abort stand-alone image build due to reflection use without configuration. Warning: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception … Warning: Image 'helloreflection' is a fallback-image that requires a JDK for execution (use --no-fallback to suppress fallback image generation).
フォールバックイメージは単なるJava HotSpot VMのランチャーです。これはおそらく開発者が本当に欲しているものではありませんが、実行時にすぐに失敗するネイティブイメージを生成しないことを確実にすることが必要で、これは想定される動作です。
./helloreflection foo xyz Running foo Exception running xyz: NoSuchMethodException
–no-fallback というオプションをつけると、明示的にフォールバックイメージの生成を無効にできます。
$JAVA_HOME/bin/native-image --no-fallback HelloReflection
この場合、Java HotSpot VMがなくても動作するネイティブイメージを生成しますが、リフレクションを使ったメソッドへのアクセスはできません。
./helloreflection foo xyz Exception running foo: NoSuchMethodException Exception running xyz: NoSuchMethodException
Reflection Tracing Agent
完全なリフレクション設定ファイルを一から書き出すこともできますが、面倒です。そのため、Java HotSpot VMに対するすべてのリフレクション検索操作を追跡してリフレクション設定ファイルを生成する、Java HotSpot VM用のエージェントを提供します。トレースされる操作には、Class.forName、Class.getMethod、Class.getFieldOperationsなどがあります。エージェントはGraalVMのダウンロードバイナリに含まれています。
mkdir -p META-INF/native-image $JAVA_HOME/bin/java -agentlib:native-image-agent=config-output-dir=META-INF/native-image HelloReflection foo xyz
このコマンドを実行すると、reflection-config.jsonファイルを含むMETA-INF/native-imageというディレクトリを作成します。その他いくつかのファイルをこのディレクトリに作成しますが、これらについては後述します。reflection-config.jsonファイルがあると、リフレクションによりHelloReflection.fooメソッドにアクセスできるようになります。
[
{
"name":"HelloReflection",
"methods":[{ "name":"foo", "parameterTypes":[] }]
}
]
ネイティブイメージ生成ツールは自動的に設定ファイルをMETA-INF/native-imageもしくはそのサブディレクトリから見つけ出します。これはnative-image.propertiesファイルを自動検出するのと同じお作法です。
Simplifying native-image generation with Maven plugin and embeddable configuration
https://medium.com/graalvm/simplifying-native-image-generation-with-maven-plugin-and-embeddable-configuration-d5b283b92f57
https://logico-jp.io/2019/03/25/simplifying-native-image-generation-with-maven-plugin-and-embeddable-configuration/
$JAVA_HOME/bin/native-image HelloReflection
コマンドを実行すると、リフレクションでメソッドfooを検索できるネイティブイメージを生成します。注意いただきたいのは、–no-fallbackオプションを渡す必要がない、ということです。リフレクション設定ファイルは、アプリケーションがリフレクションを使用するという事実にもかかわらず、フォールバックイメージを生成してはならないという開発者の意図を伝えています。このネイティブイメージは期待どおりに動作します。
./helloreflection foo xyz Running foo Exception running xyz: NoSuchMethodException
そして期待通りにネイティブイメージは瞬時に起動します。

Completeness of Reflection Configuration
トレースエージェントとネイティブイメージツールは、トレースされたリフレクションの使用法、または提供されたリフレクション設定ファイルが完全であることを自動的に確認できません。 ここまでサンプルを実行する際に、コマンドラインでメソッドbarの名前を指定していません。 このメソッドは、Java HotSpot VMでサンプルを実行したときに見つかります。
$JAVA_HOME/bin/java HelloReflection bar Running bar
しかし前章で生成したネイティブイメージを実行すると、メソッドbarが見つかりません。
./helloreflection bar Exception running bar: NoSuchMethodException
対処するためには、reflection-config.jsonを手作業で修正してメソッドbarを追加するか、トレースエージェントを実行して設定ファイルを増やす必要があります。
$JAVA_HOME/bin/java -agentlib:native-image-agent=config-merge-dir=META-INF/native-image HelloReflection bar
新しい設定ファイルで上書きするのではなく、既存の設定ファイルを拡張するようにエージェントに指示するconfig-merge-dirという別のオプションがあります。ネイティブイメージを再ビルドすると、メソッドbarにもアクセスできるようになりました。
$JAVA_HOME/bin/native-image HelloReflection … ./helloreflection foo bar xyz Running foo Running bar Exception running xyz: NoSuchMethodException
実際のアプリケーションでは、トレースエージェントと手作業による検査と設定ファイルの変更を組み合わせることを推奨します。アプリケーションが提供するすべてのテストスイートでJava HotSpot VM上で実行すると、かなり完成度の高い設定ファイルを作成できます。完全性はテストスイートのコードカバレッジによって異なります。100%のアプリケーションコードカバレッジを持つ理想的なテストスイートは、完全性が保証された設定ファイルを生成します。ただし、実際にはテストスイートはアプリケーションのすべてのパスを通すテストを実行することはありません。そのため、実際のアプリケーションでは手動による設定ファイルの検査と変更が必要になる可能性があります。
JNI, Resource, and Proxy Configuration
ネイティブイメージ生成ツールはリフレクションのためだけでなく、静的解析でネイティブイメージに何を入れるかを自動的に決定できない他のいくつかの機能のためにも設定ファイルが必要です。
- JNI
Java Native Interface (JNI) on Substrate VM
https://github.com/oracle/graal/blob/master/substratevm/JNI.md
JNIを使ってCのコードからアクセスされるクラス、メソッド、フィールドを、リフレクション設定ファイルと同じ構造を持つファイルを使い登録しなければなりません。 - Resources
Accessing resources in Substrate VM images
https://github.com/oracle/graal/blob/master/substratevm/RESOURCES.md
アプリケーションデータファイルはクラスと並んでJARファイルに含まれていることが多々あります。上記URLに記載の通り、実行時に利用可能なすべてのリソースは、正規表現の構文を使用して指定されなければなりません。 - Proxy
Dynamic proxies on Substrate VM
https://github.com/oracle/graal/blob/master/substratevm/DYNAMIC_PROXY.md
java.lang.reflect.Proxyの内部実装では、java.lang.reflect.Proxyに渡されたインターフェースのすべての組み合わせに対応するクラスを生成します。これらの組み合わせtは上記URLに記載の通り、構成ファイルで提供される必要があります。
トレースエージェントは、アプリケーションによるJNI、リソース、Proxyの利用状況も追跡し、適切な設定ファイル(jni-config.json、resource-config.json、proxy-config.json)を生成します。
Conclusions
トレースエージェントは、Java HotSpot VM上で実行されているアプリケーションの動作を監視し、自動的にネイティブイメージ生成ツールを構成するための設定ファイルに書き込みます。このトレースエージェントが、アプリケーションを初めてネイティブイメージとして実行し、継続的インテグレーションビルド/テストシステムの一部として実行するのに役立つツールであることを願っています。このエージェントは最近追加されたものなので、バグに遭遇した場合や特定の機能が欠けている場合はお知らせください。
トレースエージェントを利用するには、まずGraalVMをダウンロードしてください。
GraalVM Downloads
https://www.graalvm.org/downloads/
その後、guユーティリティでnative-imageコンポーネントをインストールする必要があります。
gu install native-image