CLOVER🍀

That was when it all began.

RESTEasy(JAX-RS)+ArC(CDI)の利用に見る、Quarkusアプリケーションのビルド結果の中身

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

Quarkusは、GraalVMを使ってアプリケーションをネイティブイメージにビルドできることを売りのひとつにしています。

ところで、GraalVMを使ってネイティブイメージを作ろうとするとけっこうな制限があって、アプリケーション側もそれなりに
対応する必要があります。

graal/LIMITATIONS.md at vm-1.0.0-rc16 · oracle/graal · GitHub

このあたり、Quarkusはどうしてるんだろうということで、小さなアプリケーションを使ってなにが起こっているのかを
少し追ってみることにしました。

環境

今回の環境は、こちらです。

Java、Apache Maven。

$ 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のサブセットです。

Cdi Reference

サポートしている機能と制限。

Supported Features

Limitations

@ConversationScoped、Decorator、Portable Extensions、BeanManagerの一部の機能、beans.xmlを無視する、Interceptorに
関する制限などがあります。

また、依存関係をインジェクションする際には、GraalVMのSubstrate VMをターゲットにしている関係上、privateメンバーではなく、
パッケージプライベートやコンストラクタインジェクションを利用するのがおすすめされています。

Private Members

でないと、リフレクションを使用するようにフォールバックしてしまうようです。

また、Portable Extensionsは使えませんが、代わりにビルド時の拡張ポイントがあり、こちらで多くの機能は代替できるようです。

Build Time Extension Points

あとで出てきますが、作成したプロジェクト内に含まれる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やクライアントプロキシの生成部分を見てみましょう。

以下のパッケージで行っているようです。

https://github.com/quarkusio/quarkus/tree/0.14.0/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor

Beanやクライアントプロキシを生成しているのは、以下のクラス。

https://github.com/quarkusio/quarkus/blob/0.14.0/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java

https://github.com/quarkusio/quarkus/blob/0.14.0/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ClientProxyGenerator.java

宣言されている定数値(suffix)を見ると、コンパイル時に生成されたもの(「Bean」、「ClientProxy」)と同じものを
見ることができるでしょう。

他にもGeneratorはたくさんあるので、気になる方は上記のパッケージを…。

次に、DI関連のところを少し見てみましょう。

InjectableReferenceProviderというのが、与えられたコンテキストに応じたインスタンスを取得できるインターフェースになります。

https://github.com/quarkusio/quarkus/blob/0.14.0/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InjectableReferenceProvider.java

その実装として、InstanceProvider、BeanManagerProviderなど、種々のProviderがあります。

https://github.com/quarkusio/quarkus/blob/0.14.0/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InstanceProvider.java

https://github.com/quarkusio/quarkus/blob/0.14.0/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/BeanManagerProvider.java

インジェクション可能なInjectableBean、InstanceHandleなどがあり、

https://github.com/quarkusio/quarkus/blob/0.14.0/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InjectableBean.java

https://github.com/quarkusio/quarkus/blob/0.14.0/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InstanceHandle.java

これらのBeanを扱うのがArcContainer(とその実装)になります。

https://github.com/quarkusio/quarkus/blob/0.14.0/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcContainer.java

https://github.com/quarkusio/quarkus/blob/0.14.0/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcContainerImpl.java

CDI#currentなどで使う、CDIのProviderなどやBeanManagerの実装もあります。

https://github.com/quarkusio/quarkus/blob/0.14.0/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcCDIProvider.java

https://github.com/quarkusio/quarkus/blob/0.14.0/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/BeanManagerImpl.java

このあたりが、ArCのCDIの実体のようです。

で、CDI管理Beanをどうやって登録するかというところを少し見てみましょう。

CDI管理Beanは、ビルド時に生成されるComponentsProviderより取得します。

target/wiring-classes/io/quarkus/arc/setup/Default_ComponentsProvider.class

このクラスは、BeanProcessorおよびComponentsProviderGeneratorにより作成されます。

https://github.com/quarkusio/quarkus/blob/0.14.0/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java#L217

https://github.com/quarkusio/quarkus/blob/0.14.0/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ComponentsProviderGenerator.java#L63

BeanProcessorが受け取る内容を見ると、それはもうたくさんの種類のクラスについての情報を受け取るようで…。

https://github.com/quarkusio/quarkus/blob/0.14.0/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java#L84-L92

そして、Beanやクライアントプロキシなどの生成が行われます、と。

https://github.com/quarkusio/quarkus/blob/0.14.0/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java#L159-L165

生成されたComponentsProviderは、必要なBeanをComponentsとして登録するような処理を実装しています。

https://github.com/quarkusio/quarkus/blob/0.14.0/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/Components.java

この情報を使って、ArcContainerの実装はどのようなBeanがあるかを把握するようになっています。

https://github.com/quarkusio/quarkus/blob/0.14.0/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcContainerImpl.java#L94-L116

なお、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リソースクラスのクラス自体やメソッドの情報は、ここで登録するようです。

このクラスを生成するのは、以下のクラスのようです。

https://github.com/quarkusio/quarkus/blob/0.14.0/core/deployment/src/main/java/io/quarkus/deployment/steps/SubstrateAutoFeatureStep.java#L51

まとめ

あんまりまとめがないですが、RESTEasy+ArCの簡単なアプリケーションのビルド時の情報から、中身を少し見てみました。

基本的に、ビルド時のコード生成を行ってGraalVM(Substrate VM)の制限を回避するような感じで作られているようですね。

このあたりはバージョンが進むといろいろ変わるような気はしますが、現時点の参考情報として。
追ってみて、けっこう面白かったです。