これは、なにをしたくて書いたもの?
Quarkusは、GraalVMを使ってアプリケーションをネイティブイメージにビルドできることを売りのひとつにしています。
ところで、GraalVMを使ってネイティブイメージを作ろうとするとけっこうな制限があって、アプリケーション側もそれなりに
対応する必要があります。
graal/LIMITATIONS.md at vm-1.0.0-rc16 · oracle/graal · GitHub
このあたり、Quarkusはどうしてるんだろうということで、小さなアプリケーションを使ってなにが起こっているのかを
少し追ってみることにしました。
環境
今回の環境は、こちらです。
$ java -version openjdk version "1.8.0_191" OpenJDK Runtime Environment (build 1.8.0_191-8u191-b12-2ubuntu0.18.04.1-b12) OpenJDK 64-Bit Server VM (build 25.191-b12, mixed mode) $ 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_191, vendor: Oracle Corporation, runtime: /usr/lib/jvm/java-8-openjdk-amd64/jre Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "4.15.0-48-generic", arch: "amd64", family: "unix"
GraalVM CE。
$ export GRAALVM_HOME=/usr/local/graalvm-ce $ $GRAALVM_HOME/bin/native-image --version GraalVM Version 1.0.0-rc16 CE
Quarkusは、0.14.0を使います。
お題
今回は簡単に、RESTEasy(JAX-RS)とArC(CDI)を使ったアプリケーションをビルドし、その時に生成される情報を
見てみたいと思います。
ArC
ArCは、Quarkusが実装しているCDIのサブセットです。
サポートしている機能と制限。
@ConversationScoped、Decorator、Portable Extensions、BeanManagerの一部の機能、beans.xmlを無視する、Interceptorに
関する制限などがあります。
また、依存関係をインジェクションする際には、GraalVMのSubstrate VMをターゲットにしている関係上、privateメンバーではなく、
パッケージプライベートやコンストラクタインジェクションを利用するのがおすすめされています。
でないと、リフレクションを使用するようにフォールバックしてしまうようです。
また、Portable Extensionsは使えませんが、代わりにビルド時の拡張ポイントがあり、こちらで多くの機能は代替できるようです。
あとで出てきますが、作成したプロジェクト内に含まれるArCの依存関係は、以下の2つです。
quarkus-arc
https://github.com/quarkusio/quarkus/tree/0.14.0/extensions/arc/runtime
arc
https://github.com/quarkusio/quarkus/tree/0.14.0/independent-projects/arc/runtime
とまあ、ArCの話はとりあえずこれくらいにして、先に進みましょう。
サンプルアプリケーションの作成
まずはアプリケーションの雛形を作ります。
$ mvn io.quarkus:quarkus-maven-plugin:0.14.0:create \ -DprojectGroupId=org.littlewings.quarkus \ -DprojectArtifactId=resteasy-arc
RESTEasy+ArC(JAX-RS+CDI)の最小構成のプロジェクトです。
この時のArCの依存関係を確認すると、以下のようになっています。
$ mvn dependency:tree | grep arc ... [INFO] | +- io.quarkus:quarkus-arc:jar:0.14.0:compile [INFO] | | \- io.quarkus.arc:arc:jar:0.14.0:compile
「quarkus-arc」
https://github.com/quarkusio/quarkus/tree/0.14.0/extensions/arc/runtime
「arc」
https://github.com/quarkusio/quarkus/tree/0.14.0/independent-projects/arc/runtime
ここで、ArC本体が入っているIndependent Projectsというのは、最終的にQuarkusから独立可能なスタンドアロンなプロジェクトが
含まれているものです。現時点だと、ArCとBootstrapですが。
quarkus/independent-projects at 0.14.0 · quarkusio/quarkus · GitHub
簡単なCDI管理Beanと
src/main/java/org/littlewings/quarkus/resteasyasc/HelloService.java
package org.littlewings.quarkus.resteasyasc; import javax.enterprise.context.ApplicationScoped; @ApplicationScoped public class HelloService { public String message() { return "Hello Quarkus!!"; } }
このCDI管理Beanを作成する、JAX-RSリソースクラスを作成します。JAX-RSリソースクラスは、CDI管理Beanのアノテーションを
付けない場合はSingletonになるようです。
src/main/java/org/littlewings/quarkus/resteasyasc/HelloResource.java
package org.littlewings.quarkus.resteasyasc; import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; @Path("hello") public class HelloResource { @Inject HelloService helloService; @GET @Produces(MediaType.TEXT_PLAIN) public String message() { return helloService.message(); } }
パッケージングして起動。
$ mvn package $ java -jar target/resteasy-arc-1.0-SNAPSHOT-runner.jar 2019-04-30 00:44:53,592 INFO [io.quarkus] (main) Quarkus 0.14.0 started in 0.712s. Listening on: http://[::]:8080 2019-04-30 00:44:53,613 INFO [io.quarkus] (main) Installed features: [cdi, resteasy]
確認。
$ curl localhost:8080/hello Hello Quarkus!!
これで、用意はOKです。
ビルド時に作成されたファイルを見る
ここで、targetディレクトリの中を見てみましょう。「target/lib」はアプリケーションの依存ライブラリなので、除外します。
$ find target -type f | grep -v 'target/lib' target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst target/resteasy-arc-1.0-SNAPSHOT.jar target/resteasy-arc-1.0-SNAPSHOT-runner.jar target/classes/META-INF/application-info.properties target/classes/META-INF/resources/index.html target/classes/application.properties target/classes/native-image.properties target/classes/org/littlewings/quarkus/resteasyasc/HelloResource.class target/classes/org/littlewings/quarkus/resteasyasc/HelloService.class target/maven-archiver/pom.properties target/wiring-classes/META-INF/build-config.properties target/wiring-classes/META-INF/services/io.quarkus.arc.ComponentsProvider target/wiring-classes/META-INF/quarkus-default-config.properties target/wiring-classes/io/quarkus/arc/runtime/LifecycleEventRunner_Bean.class target/wiring-classes/io/quarkus/arc/setup/Default_ComponentsProvider.class target/wiring-classes/io/quarkus/arc/ActivateRequestContextInterceptor_Bean.class target/wiring-classes/io/quarkus/arc/runtimebean/RuntimeBeanProducers.class target/wiring-classes/io/quarkus/deployment/steps/UndertowBuildStep$boot10.class target/wiring-classes/io/quarkus/deployment/steps/LoggingResourceProcessor$setupLoggingRuntimeInit4.class target/wiring-classes/io/quarkus/deployment/steps/LifecycleEventsBuildStep$startupEvent11.class target/wiring-classes/io/quarkus/deployment/steps/RuntimeBeanProcessor$build2.class target/wiring-classes/io/quarkus/deployment/steps/ResteasyScanningProcessor$setupInjection8.class target/wiring-classes/io/quarkus/deployment/steps/LoggingResourceProcessor$setupLoggingStaticInit1.class target/wiring-classes/io/quarkus/deployment/steps/UndertowArcIntegrationBuildStep$integrateRequestContext6.class target/wiring-classes/io/quarkus/deployment/steps/ArcAnnotationProcessor$build5.class target/wiring-classes/io/quarkus/deployment/steps/ConfigBuildStep$validateConfigProperties7.class target/wiring-classes/io/quarkus/deployment/steps/UndertowBuildStep$build9.class target/wiring-classes/io/quarkus/deployment/steps/ThreadPoolSetup$createExecutor3.class target/wiring-classes/io/quarkus/runner/ApplicationImpl1.class target/wiring-classes/io/quarkus/runner/AutoFeature.class target/wiring-classes/io/quarkus/runner/GeneratedMain.class target/wiring-classes/io/quarkus/runtime/generated/RunTimeConfig.class target/wiring-classes/io/quarkus/runtime/generated/RunTimeDefaultConfigSource.class target/wiring-classes/io/quarkus/runtime/generated/RunTimeConfigRoot.class target/wiring-classes/io/quarkus/runtime/generated/BuildTimeConfigRoot.class target/wiring-classes/io/quarkus/runtime/generated/BuildTimeConfig.class target/wiring-classes/org/littlewings/quarkus/resteasyasc/HelloResource_Bean.class target/wiring-classes/org/littlewings/quarkus/resteasyasc/HelloService_Bean.class target/wiring-classes/org/littlewings/quarkus/resteasyasc/HelloService_Bean$$function$$1.class target/wiring-classes/org/littlewings/quarkus/resteasyasc/HelloService_ClientProxy.class target/wiring-classes/javax/enterprise/context/control/ActivateRequestContext_Shared_AnnotationLiteral.class
「target/wiring-classes」というディレクトリに、いろいろ作成されています。
1度削除して、通常のビルド時とネイティブアプリケーションとしてのビルド時で作成されるファイルを比較してみましょう。
$ mvn clean $ mvn package $ mv target target-java $ mvn -Pnative package $ mv target target-native
差分を見てみると、特にネイティブイメージにしたからといってファイルが増えたりすることはなさそうです。
※wiring-classes/io/quarkus/deployment/steps配下は、生成する度に名前が変わるようなので、その差は無視します
$ diff -rq target-java target-native ファイル target-java/classes/META-INF/application-info.properties と target-native/classes/META-INF/application-info.properties は異なります ファイル target-java/classes/native-image.properties と target-native/classes/native-image.properties は異なります ファイル target-java/maven-archiver/pom.properties と target-native/maven-archiver/pom.properties は異なります target-native のみに存在: reports target-native のみに存在: resteasy-arc-1.0-SNAPSHOT-runner ファイル target-java/resteasy-arc-1.0-SNAPSHOT-runner.jar と target-native/resteasy-arc-1.0-SNAPSHOT-runner.jar は異なります ファイル target-java/resteasy-arc-1.0-SNAPSHOT.jar と target-native/resteasy-arc-1.0-SNAPSHOT.jar は異なります ファイル target-java/wiring-classes/META-INF/build-config.properties と target-native/wiring-classes/META-INF/build-config.properties は異なります ファイル target-java/wiring-classes/META-INF/quarkus-default-config.properties と target-native/wiring-classes/META-INF/quarkus-default-config.properties は異なります ファイル target-java/wiring-classes/io/quarkus/arc/ActivateRequestContextInterceptor_Bean.class と target-native/wiring-classes/io/quarkus/arc/ActivateRequestContextInterceptor_Bean.class は異なります ファイル target-java/wiring-classes/io/quarkus/arc/runtime/LifecycleEventRunner_Bean.class と target-native/wiring-classes/io/quarkus/arc/runtime/LifecycleEventRunner_Bean.class は異なります ファイル target-java/wiring-classes/io/quarkus/arc/setup/Default_ComponentsProvider.class と target-native/wiring-classes/io/quarkus/arc/setup/Default_ComponentsProvider.class は異なります ファイル target-java/wiring-classes/io/quarkus/deployment/steps/ArcAnnotationProcessor$build5.class と target-native/wiring-classes/io/quarkus/deployment/steps/ArcAnnotationProcessor$build5.class は異なります target-java/wiring-classes/io/quarkus/deployment/steps のみに存在: ConfigBuildStep$validateConfigProperties6.class target-native/wiring-classes/io/quarkus/deployment/steps のみに存在: ConfigBuildStep$validateConfigProperties8.class target-native/wiring-classes/io/quarkus/deployment/steps のみに存在: LoggingResourceProcessor$setupLoggingRuntimeInit3.class target-java/wiring-classes/io/quarkus/deployment/steps のみに存在: LoggingResourceProcessor$setupLoggingRuntimeInit4.class target-native/wiring-classes/io/quarkus/deployment/steps のみに存在: LoggingResourceProcessor$setupLoggingStaticInit1.class target-java/wiring-classes/io/quarkus/deployment/steps のみに存在: LoggingResourceProcessor$setupLoggingStaticInit2.class target-native/wiring-classes/io/quarkus/deployment/steps のみに存在: ResteasyScanningProcessor$setupInjection6.class target-java/wiring-classes/io/quarkus/deployment/steps のみに存在: ResteasyScanningProcessor$setupInjection8.class target-java/wiring-classes/io/quarkus/deployment/steps のみに存在: RuntimeBeanProcessor$build1.class target-native/wiring-classes/io/quarkus/deployment/steps のみに存在: RuntimeBeanProcessor$build2.class target-java/wiring-classes/io/quarkus/deployment/steps のみに存在: ThreadPoolSetup$createExecutor3.class target-native/wiring-classes/io/quarkus/deployment/steps のみに存在: ThreadPoolSetup$createExecutor4.class ファイル target-java/wiring-classes/io/quarkus/deployment/steps/UndertowArcIntegrationBuildStep$integrateRequestContext7.class と target-native/wiring-classes/io/quarkus/deployment/steps/UndertowArcIntegrationBuildStep$integrateRequestContext7.class は異なります ファイル target-java/wiring-classes/io/quarkus/deployment/steps/UndertowBuildStep$boot10.class と target-native/wiring-classes/io/quarkus/deployment/steps/UndertowBuildStep$boot10.class は異なります ファイル target-java/wiring-classes/io/quarkus/deployment/steps/UndertowBuildStep$build9.class と target-native/wiring-classes/io/quarkus/deployment/steps/UndertowBuildStep$build9.class は異なります ファイル target-java/wiring-classes/io/quarkus/runner/ApplicationImpl1.class と target-native/wiring-classes/io/quarkus/runner/ApplicationImpl1.class は異なります ファイル target-java/wiring-classes/io/quarkus/runner/AutoFeature.class と target-native/wiring-classes/io/quarkus/runner/AutoFeature.class は異なります ファイル target-java/wiring-classes/io/quarkus/runtime/generated/BuildTimeConfig.class と target-native/wiring-classes/io/quarkus/runtime/generated/BuildTimeConfig.class は異なります ファイル target-java/wiring-classes/io/quarkus/runtime/generated/RunTimeConfig.class と target-native/wiring-classes/io/quarkus/runtime/generated/RunTimeConfig.class は異なります ファイル target-java/wiring-classes/io/quarkus/runtime/generated/RunTimeConfigRoot.class と target-native/wiring-classes/io/quarkus/runtime/generated/RunTimeConfigRoot.class は異なります ファイル target-java/wiring-classes/org/littlewings/quarkus/resteasyasc/HelloService_ClientProxy.class と target-native/wiring-classes/org/littlewings/quarkus/resteasyasc/HelloService_ClientProxy.class は異なります
まず、パッと気になるのは自分が作成したクラスに対して、生成されているクラス。
target/wiring-classes/org/littlewings/quarkus/resteasyasc/HelloResource_Bean.class target/wiring-classes/org/littlewings/quarkus/resteasyasc/HelloService_Bean.class target/wiring-classes/org/littlewings/quarkus/resteasyasc/HelloService_Bean$$function$$1.class target/wiring-classes/org/littlewings/quarkus/resteasyasc/HelloService_ClientProxy.class
IDEなどで見てみると、ArCが提供するInjectableBeanインターフェースを実装したクラスが生成され、さらにクライアントプロキシも
作成されています。
JAX-RSリソースクラスに対応するクラスも、InjectableBeanとして作成されています。
これらの中で、実際のインスタンスをnewしたり、関連するクラスをInjectableReferenceProvider(後述)から取得して
依存関係を組み上げるような処理が含まれています。
今度は、Quarkusのパッケージで生成されたものを見ていきましょう。
最初に、アプリケーションのエントリポイントとなるJARファイルのMANIFEST.MFに含まれる、mainクラスの宣言を
見てみます。
$ unzip -p target/resteasy-arc-1.0-SNAPSHOT-runner.jar META-INF/MANIFEST.MF
Main-Class: io.quarkus.runner.GeneratedMain
このクラス、生成されたクラスの中に含まれています。
target/wiring-classes/io/quarkus/runner/GeneratedMain.class
中身を見ると、以下のクラスを呼び出してアプリケーションのセットアップを行っているようです。
target/wiring-classes/io/quarkus/runner/ApplicationImpl1.class
少し、目線を変えて、先ほどの自分が作成したクラスに対するBeanやクライアントプロキシの生成部分を見てみましょう。
以下のパッケージで行っているようです。
Beanやクライアントプロキシを生成しているのは、以下のクラス。
宣言されている定数値(suffix)を見ると、コンパイル時に生成されたもの(「Bean」、「ClientProxy」)と同じものを
見ることができるでしょう。
他にもGeneratorはたくさんあるので、気になる方は上記のパッケージを…。
次に、DI関連のところを少し見てみましょう。
InjectableReferenceProviderというのが、与えられたコンテキストに応じたインスタンスを取得できるインターフェースになります。
その実装として、InstanceProvider、BeanManagerProviderなど、種々のProviderがあります。
インジェクション可能なInjectableBean、InstanceHandleなどがあり、
これらのBeanを扱うのがArcContainer(とその実装)になります。
CDI#currentなどで使う、CDIのProviderなどやBeanManagerの実装もあります。
このあたりが、ArCのCDIの実体のようです。
で、CDI管理Beanをどうやって登録するかというところを少し見てみましょう。
CDI管理Beanは、ビルド時に生成されるComponentsProviderより取得します。
target/wiring-classes/io/quarkus/arc/setup/Default_ComponentsProvider.class
このクラスは、BeanProcessorおよびComponentsProviderGeneratorにより作成されます。
BeanProcessorが受け取る内容を見ると、それはもうたくさんの種類のクラスについての情報を受け取るようで…。
そして、Beanやクライアントプロキシなどの生成が行われます、と。
生成されたComponentsProviderは、必要なBeanをComponentsとして登録するような処理を実装しています。
この情報を使って、ArcContainerの実装はどのようなBeanがあるかを把握するようになっています。
なお、ComponentsProviderはService Providerの仕組みでロードされます。
$ unzip -p target/resteasy-arc-1.0-SNAPSHOT-runner.jar META-INF/services/io.quarkus.arc.ComponentsProvider io.quarkus.arc.setup.Default_ComponentsProvider
あ、ArCとは少し離れるようですが、作成したJAX-RSリソースクラスは「resteasy.scanned.resources」として埋め込まれます。
target/wiring-classes/io/quarkus/deployment/steps/UndertowBuildStep$build9.class
というわけで、リフレクションを回避しつつ、必要な処理を行うクラスを生成して動かすことで、CDIなどの仕組みを
実現しているみたいですね。
Substrate VMの機能を使ったコードは?
となると、Substrate VMの機能を使ったコードは現れないのか?というと、そんなことはありません。
Substrate VMの、@AutomaticFeatureアノテーションが付与されたクラスが生成されます。
target/wiring-classes/io/quarkus/runner/AutoFeature.class
この中では、RuntimeReflectionを使い、リフレクションの情報を登録する処理が生成されます。
JAX-RSリソースクラスのクラス自体やメソッドの情報は、ここで登録するようです。
このクラスを生成するのは、以下のクラスのようです。
まとめ
あんまりまとめがないですが、RESTEasy+ArCの簡単なアプリケーションのビルド時の情報から、中身を少し見てみました。
基本的に、ビルド時のコード生成を行ってGraalVM(Substrate VM)の制限を回避するような感じで作られているようですね。
このあたりはバージョンが進むといろいろ変わるような気はしますが、現時点の参考情報として。
追ってみて、けっこう面白かったです。