この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
Introduction
GraalVMは、多言語対応の仮想マシンとプラットフォームです。
Java/JVM言語/Node/LLVM言語をサポートしており、
Javaプログラムをネイティブコンパイルして高速動作が可能です。
※このへん参照
GraalVMではAOT(Ahead Of Tim)コンパイルと呼ばれるコンパイルができます。
これはJavaのコードをNative Imageと呼ばれる
実行可能形式にコンパイルできるのですが、
リフレクションをつかっている場合にそのままだと
実行できないことがあります。
本稿ではMicronaut + GraalVMで
リフレクションを使いたいケースについて試してみました。
Environment
- MacBook Pro (13-inch, M1, 2020)
- OS : MacOS 12.4
Setup
MicronautやGraalVMのインストールにはSdkManを使います。
インストールしてない場合はインストールしましょう。
% curl -s "https://get.sdkman.io" | bash
GraalVMとMicronautをインストールします。
% sdk update
% sdk install java 22.3.r17-grl
% sdk install micronaut
・・・
% java --version
openjdk 17.0.5 2022-10-18
OpenJDK Runtime Environment GraalVM CE 22.3.0 (build 17.0.5+8-jvmci-22.3-b08)
OpenJDK 64-Bit Server VM GraalVM CE 22.3.0 (build 17.0.5+8-jvmci-22.3-b08, mixed mode, sharing)
% mn --version
Micronaut Version: 3.7.3
インストールOKです。
次にアプリを実装していきます。
Try
local_grという名前でGraalVM用アプリの雛形を作成します。
% mn create-app example.local_gr \
--features=graalvm,serialization-jackson \
--build=gradle --lang=java
次に、src/main/exampleにリフレクションを使って呼び出すクラスを作成します。
シンプルなメソッドを1つ持っているだけのクラスです。
package example;
public class ReflectionService {
private void sayHello() {
System.out.println("hello");
}
}
src/main/exampleにHelloController.javaを作成。
ここでReflectionServiceのメソッドをリフレクションで呼び出します。
package example;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Optional;
@Controller("/hello")
public class HelloController {
@Get("/reflection")
public String sayHello() {
ReflectionService service = new ReflectionService();
Class<? extends ReflectionService> clazz = service.getClass();
try {
Method printHoge = clazz.getDeclaredMethod("sayHello");
printHoge.setAccessible(true);
printHoge.invoke(service, (Object[])null);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
throw new RuntimeException();
}
return "sayHello";
}
}
ネイティブコンパイルします。
3〜4分くらいかかる。
% ./gradlew nativeCompile
コンパイルできたらネイティブイメージを起動しましょう。
% ./build/native/nativeCompile/local_gr
__ __ _ _
| \/ (_) ___ _ __ ___ _ __ __ _ _ _| |_
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| | | | | (__| | | (_) | | | | (_| | |_| | |_
|_| |_|_|\___|_| \___/|_| |_|\__,_|\__,_|\__|
Micronaut (v3.7.3)
16:07:17.267 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 355ms. Server Running: http://localhost:8080
アクセスしてみます。
これは問題なく動作します。
% curl http://localhost:8080/hello/reflection
この場合はリフレクションを使っていても、コンパイル時の静的解析で検出可能だからです。
しかし、ControllerのsayHelloメソッドを次のようにすると、
クエリパラメータによって呼び出される関数が
動的に変化するため実行できません。
@Get("/reflection")
public String sayHello(Optional<String> name) {
ReflectionService service = new ReflectionService();
Class<? extends ReflectionService> clazz = service.getClass();
try {
Method printHoge = clazz.getDeclaredMethod(name.orElseThrow());
printHoge.setAccessible(true);
printHoge.invoke(service, (Object[])null);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
throw new RuntimeException();
}
return "sayHello";
}
スタックトレースをみると、NoSuchMethodExceptionが発生してエラーになっています。
% curl http://localhost:8080/hello/reflection?name=sayHello
{"message":"Internal Server Error","_links":{"self":[{"href":"/hello/reflection?name=sayHello","templated":false}]},"_embedded":{"errors":[{"message":"Internal Server Error: null"}]}}
reflection-config.jsonで設定
動的に呼び出すクラス・メソッドが変化する場合、
設定ファイルで事前にどういった定義が必要が記述しておきます。
src/main/resources/META-INF/native-image/example/local_grディレクトリに
reflection-config.jsonを下記のように作成します。
[ {
"name" : "example.ReflectionService",
"methods" : [ {
"name" : "sayHello",
"parameterTypes" : [ ]
}]
}
]
そして、build.gradleに次の定義を追加します。
graalvmNative {
binaries {
main {
buildArgs.add("-H:ReflectionConfigurationFiles=/path/your/local_gr/src/main/resources/META-INF/native-image/example/local_gr/reflection-config.json")
}
}
}
ネイティブコンパイル時の引数に
さきほどのreflection-config.jsonを指定してあげます。
再度ネイティブコンパイルして起動。
今度はちゃんとメソッドが呼び出しできています。
% ./gradlew nativeCompile
% ./build/native/nativeCompile/local_gr
#今度は動く
% curl http://localhost:8080/hello/reflection?name=sayHello
Tracing Agentでreflection-configの自動生成
今回の例のように単純なものであればいいのですが、
リフレクションを多用している場合にいちいち
設定ファイルを書いていくのは現実的ではないです。
そういった場合、Tracing Agentというツールを使って
設定ファイルの自動生成を行います。
このツールは、VMの実行時(ネイティブイメージでない状態でアプリ実行しているとき)に
アプリの動作をトレースすることにより、JSONファイルを生成するツールです。
具体的には、単体テスト実行時にその挙動をトレースして定義ファイルを生成します。
では、build.gradleでテスト実行時にTracing Agentを使うように指定します。
test {
jvmArgs "-agentlib:native-image-agent=config-merge-dir=src/main/resources/META-INF/native-image/auto/"
}
元からあるテストクラス(test/java/example/Local_grTest.java)に、
ReflectionServiceのメソッドを実行するテストを追加します。
@Test
void reflectTest() {
Class<? extends ReflectionService> clazz = ReflectionService.class;
try {
ReflectionService service = clazz.newInstance();
Method printHoge = clazz.getDeclaredMethod("sayHello");
printHoge.setAccessible(true);
printHoge.invoke(service, (Object[])null);
} catch (Exception e) {
e.printStackTrace();
}
}
テストを実行。
./gradlew test
テストが終わると、build.gradleで指定したパスに
reflection-config.jsonなどの設定ファイルもろもろが生成されてます。
・・・
{
"name":"example.ReflectionService",
"methods":[
{"name":"<init>","parameterTypes":[] },
{"name":"sayHello","parameterTypes":[] }
]
},
・・・
そしてネイティブコンパイル・起動すれば、生成したファイル郡がロードされて、
先程とおなじく動きます。
※build.gradleのgraalvmNativeセクションは削除してOK
Summary
今回はGraalVM + Micronautでリフレクションの動作を確認してみました。
GraalVMは今後Javaでパフォーマンスを求めるなら避けては通れないと思っているので、
しっかりおさえておきたいところです。