GraalVMのNative ImageでReflectionを使う
はじめに
よくSpringのアプリケーションをNative Image化するときDynamic ProxyやReflectionをゴリゴリ使っているので、設定が大変みたいな話を聞いたことがあるのですが。
じゃあ、実際に使うためにはどんな設定が必要なんだろう。というのが気になったのでまとめます。
このブログでは以下のことを目指します。
- Reflectionを使ったアプリケーションをNative Image化する際の設定方法を知る
GraalVMやNative Imageについては以前にまとめたブログを書いたので、興味があればそちらを参照していただくか、調べればより良い資料がたくさん出てくるので検索をしてみてください。
そもそも、なぜ設定が必要なのか?
GrarlVMはNative Image化の際にAOT(Ahead Of Time)コンパイルを行います。 AOTコンパイルを行なう際にGraalVMはメモリーフットプリントをなるべく削減するような最適化を行います。この最適化を行なうためには、すべてのクラスはコンパイル時に特定されてなければならず特定されない動的に読み込まれるようなクラスはデフォルトではイメージ内に含まれないようになっているようです。
この制約のため、Dynamic ProxyやReflectionを用いた動的なクラスの読み込みを必要とする実装に関しては事前にその対象となるクラスを設定ファイルに記述し、明示的にコンパイルの対象に含めてやる必要があります。
実際にやったことがあるわけでは無いので想像の域になってしまうのですが、おそらくSpringなどの場合はこの設定の対象となるクラスを特定して設定を記述するハードルが高いのだと思います。
ちなみに、SpringのアプリケーションをNative Image化をサポートするためのプロジェクトがあり、将来的には開発者が自分で設定を書くことなくNative Image化できるようになるようです。
設定をしてみる
環境
動作環境は以下です。
$ uname -srvmpio Linux 5.3.0-51-generic #44~18.04.2-Ubuntu SMP Thu Apr 23 14:27:18 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux $ java -version openjdk version "11.0.6" 2020-01-14 OpenJDK Runtime Environment GraalVM CE 19.3.1 (build 11.0.6+9-jvmci-19.3-b07) OpenJDK 64-Bit Server VM GraalVM CE 19.3.1 (build 11.0.6+9-jvmci-19.3-b07, mixed mode, sharing) # 動作確認で利用 $ docker -v Docker version 19.03.8, build afacb8b7f0
Reflectionを設定なしで使ってみる
まず、Native Imageに含まれるSubstrate VMはReflectionの一部機能をサポートします。
コンパイル時の静的解析で検出可能なメソッドへのアクセスや、フィールドに関しては追加設定なしで、Native Image化できます。
例えば以下のようなコードは追加設定の必要がありません。
Service.java
public class Service { private void printHoge() { System.out.println("hoge"); } }
MainNotProblem.java
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class MainNoProblem { public static void main(String[] args) { Service service = new Service(); Class<? extends Service> clazz = service.getClass(); try { Method printHoge = clazz.getDeclaredMethod("printHoge"); printHoge.setAccessible(true); printHoge.invoke(service, null); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); throw new RuntimeException(); } } }
このコードは、Native イメージ化しても問題なく動きます。
$ javac MainNotProblem.java $ native-image MainNotProblem $ ./mainnoproblem hoge
しかし、以下のように呼び出しメソットが動的に変わりうる実装の場合は話が変わってきます。
MainWithProblem
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class MainWithProblem { public static void main(String[] args) { callServiceMethod("printHoge"); } static private void callServiceMethod(String methodName) { Service service = new Service(); Class<? extends Service> clazz = service.getClass(); try { Method printHoge = clazz.getDeclaredMethod(methodName); printHoge.setAccessible(true); printHoge.invoke(service, null); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); throw new RuntimeException(); } } }
Native Image化をすると以下のようなワーニングが出力されます。
$ javac MainWithProblem.java $ native-image MainWithProblem Build on Server(pid: 15873, port: 43623) [mainwithproblem:15873] classlist: 98.56 ms [mainwithproblem:15873] (cap): 296.80 ms [mainwithproblem:15873] setup: 438.70 ms [mainwithproblem:15873] (typeflow): 3,715.13 ms [mainwithproblem:15873] (objects): 4,673.33 ms [mainwithproblem:15873] (features): 115.27 ms [mainwithproblem:15873] analysis: 8,679.57 ms [mainwithproblem:15873] (clinit): 89.11 ms [mainwithproblem:15873] universe: 214.90 ms Warning: Reflection method java.lang.Class.getDeclaredMethod invoked at MainWithProblem.callServiceMethod(MainWithProblem.java:14) Warning: Aborting stand-alone image build due to reflection use without configuration. Warning: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception Build on Server(pid: 15873, port: 43623) [mainwithproblem:15873] classlist: 95.86 ms [mainwithproblem:15873] (cap): 283.66 ms [mainwithproblem:15873] setup: 429.13 ms [mainwithproblem:15873] (typeflow): 3,949.59 ms [mainwithproblem:15873] (objects): 4,872.56 ms [mainwithproblem:15873] (features): 118.84 ms [mainwithproblem:15873] analysis: 9,147.13 ms [mainwithproblem:15873] (clinit): 93.36 ms [mainwithproblem:15873] universe: 244.90 ms [mainwithproblem:15873] (parse): 310.87 ms [mainwithproblem:15873] (inline): 948.00 ms [mainwithproblem:15873] (compile): 2,238.19 ms [mainwithproblem:15873] compile: 3,753.78 ms [mainwithproblem:15873] image: 380.07 ms [mainwithproblem:15873] write: 70.03 ms [mainwithproblem:15873] [total]: 14,161.68 ms Warning: Image 'mainwithproblem' is a fallback image that requires a JDK for execution (use --no-fallback to suppress fallback image generation).
静的に読み込むクラスやメソッド名が特定できない場合は、Native Imageはfallback image
と呼ばれるJDKを必要とする代替イメージを作成します。
このバイナリをJavaのない環境で動かそうとすると以下のようなエラーが吐かれます。
Dockerfile
FROM ubuntu:latest COPY src/mainwithproblem . ENTRYPOINT ["./mainwithproblem"]
$ docker build -t native-image-with-ref . $ docker run native-image-with-ref Error: No bin/java and no environment variable JAVA_HOME
また、このfallback Imageを作成したくない場合はイメージ作成時に--no-fallback
オプションをつけイメージを作成します。
そうした場合イメージは実行時にNoSuchMethodException
を吐いて落ちるようになります。
$ native-image MainWithProblem --no-fallback $ $ ./mainwithproblem java.lang.NoSuchMethodException: Service.printHoge() at java.lang.Class.getDeclaredMethod(DynamicHub.java:2475) at MainWithProblem.callServiceMethod(MainWithProblem.java:14) at MainWithProblem.main(MainWithProblem.java:6) Exception in thread "main" java.lang.RuntimeException at MainWithProblem.callServiceMethod(MainWithProblem.java:19) at MainWithProblem.main(MainWithProblem.java:6)
設定を記述してみる
上記のMainWithProblem.java
のようなコードでスタンドアローンなNative Imageを作成する場合は動的に呼び出される可能性のあるクラスやメソッドのために、設定を記述してやる必要があります。
今回作成したサービスクラスに対して設定を記述すると以下のようになります。
reflection-config.json
[ { "name": "Service", "methods": [ { "name": "printHoge", "parameterTypes": [] } ] } ]
name
の項目にはパッケージ名を含んだクラス名を記述します。また、Serviceが持つメソッドやその引数の定義をmethods
に記述しています。
今回は1つのメソッドを持つクラスでしたので上記の設定で終わりですが、もしクラスがフィールドを持つ場合はその設定も別途記述してやる必要があります。
設定のサンプルはこちらの「Manual configuration」の章にあるので参考にしてみてください。
それでは作成した設定を使って、Native Imageを作成してみます。
設定を読み込ませるには-H:ReflectionConfigurationFiles
オプションを使います。
$ native-image MainWithProblem -H:ReflectionConfigurationFiles=/path/to/your/config/reflect-config.json Build on Server(pid: 15873, port: 43623) [mainwithproblem:15873] classlist: 108.43 ms [mainwithproblem:15873] (cap): 297.78 ms [mainwithproblem:15873] setup: 438.91 ms [mainwithproblem:15873] (typeflow): 5,419.24 ms [mainwithproblem:15873] (objects): 7,770.77 ms [mainwithproblem:15873] (features): 113.38 ms [mainwithproblem:15873] analysis: 13,485.59 ms [mainwithproblem:15873] (clinit): 127.45 ms [mainwithproblem:15873] universe: 280.51 ms [mainwithproblem:15873] (parse): 411.53 ms [mainwithproblem:15873] (inline): 1,054.60 ms [mainwithproblem:15873] (compile): 2,214.91 ms [mainwithproblem:15873] compile: 3,892.63 ms [mainwithproblem:15873] image: 442.91 ms [mainwithproblem:15873] write: 77.34 ms [mainwithproblem:15873] [total]: 18,771.70 ms $ ./mainwithproblem hoge
先ほどとは違いログにワーニングは吐かれなくなりました。
-H:ReflectionConfigurationFiles
オプションは複数の設定ファイルを読み込むことができその場合は,
で繋いで設定を読み込ませます。
作成した、イメージはJDKがない環境でも動作します。
$ docker build -t native-image-with-ref . $ docker run native-image-with-ref hoge
今回はreflectionを試しましたがDynamic Proxyをしたい場合は別の設定を記述する必要があるようなので、公式を参考にしてください。