はじめに
マイクロサービス向けの Java フレームワーク Helidon は GraalVM のネイティブ・イメージ作成に対応していますが、実践的な構成でもネイティブ・イメージにできるか、検証してみたいと思います。
具体的には、Helidon MP を使って
( REST Client ) → JAX-RS → JPA/JTA → Oracle JDBC Driver → ( Oracle Database )
のスタックで構成されるサーバ・アプリケーションをネイティブ・イメージにできるか&正常動作するか試してみましょう。
手順
- GraalVM, Maven, Oracle Database, Helidon CLI の準備
- Helidon CLI を使って JPA サンプル・アプリケーションを作成する
- データベースを H2 から Oracleに変更する
- ネイティブ・イメージを生成する
GraalVM, Maven, Oracle Database, Helidon CLI の準備
GraalVM のインストール
https://www.graalvm.org/downloads/ からダウンロードできます。
今回は GraalVM Community 22.1 を使用します。
ネイティブ・イメージのモジュールはダウンロードするだけでは入っていませんので、別途インストールします。
# ファイルの解凍
$ tar xvf graalvm-ce-java11-linux-amd64-22.1.0.tar.gz
# ネイティブ・イメージ機能のインストール
$ ./graalvm-ce-java11-22.1.0/bin/gu install native-image
環境変数 JAVA_HOME を設定して、$JAVA_HOME/bin に PATHを通します。
Java のバージョンを確認して、GraalVM が起動することを確認します。
$ java -version
openjdk version "11.0.15" 2022-04-19
OpenJDK Runtime Environment GraalVM CE 22.1.0 (build 11.0.15+10-jvmci-22.1-b06)
OpenJDK 64-Bit Server VM GraalVM CE 22.1.0 (build 11.0.15+10-jvmci-22.1-b06, mixed mode, sharing)
Maven の準備
省略します... 😊
Oracle Database の準備
省略します... 😊
JPA/Hibernate の設定によって、アプリケーションで使われるテーブルが自動的に作成されますので、しかるべき権限を持った Oracle Database ユーザの ID とパスワード、JDBC 接続文字列を確認しておいて下さい。
Hleidon CLI のインストール
ダウンロードの方法は こちら にあります。
Linuxの場合だと
curl -O https://helidon.io/cli/latest/linux/helidon
chmod +x ./helidon
sudo mv ./helidon /usr/local/bin/
動作確認します。
$ helidon version
build.date 2022-02-04 19:19:36 UTC
build.version 2.3.3
build.revision 600f89b3
project.helidon.version 2.5.0
project.flavor MP
latest.helidon.version 2.5.0
Helidon CLI を使って JPA サンプル・アプリケーションを作成する
Helidon CLI を使って新しいプロジェクトを作成します。helidon init
でインタラクティブに構成を決めていきます。Helidon flavor は MP、archetype は database を選択します。
$ helidon init
Looking up latest Helidon version
Helidon version (default: 2.5.0):
Helidon flavor
(1) SE
(2) MP
Enter selection (default: 1): 2
Select archetype
(1) bare | Minimal Helidon MP project suitable to start from scratch
(2) quickstart | Sample Helidon MP project that includes multiple REST operations
(3) database | Helidon MP application that uses JPA with an in-memory H2 database
Enter selection (default: 1): 3
Project name (default: database-mp):
Project groupId (default: me.opc-helidon):
Project artifactId (default: database-mp):
Project version (default: 1.0-SNAPSHOT):
Java package name (default: me.opc.mp.database):
Switch directory to /home/opc/work/native-image/database-mp to use CLI
Start development loop? (default: n):
プロジェクトの関連ファイルが一式生成されたら、とりあえず動かしてみましょう。
$ ls
database-mp
$ cd database-mp
# 実行可能 jar を作成 - テストも走る
$ mvn package
# 起動
$ java -jar target/database-mp.jar
...
この時点でサーバーのログが出力されて、起動されていることが確認できると思います。
では別のターミナルを開いてリクエストを投げてみます。
$ curl http://localhost:8080/pokemon
[{"id":1,"name":"Bulbasaur","type":12},{"id":2,"name":"Charmander","type":10},{"id":3,"name":"Squirtle","type":11},{"id":4,"name":"Caterpie","type":7},{"id":5,"name":"Weedle","type":7},{"id":6,"name":"Pidgey","type":3}]
サーバーは正常に動作していますね。
データベースを H2 から Oracleに変更する
元々のプロジェクトでは、データベースは H2 が使われていて、同じ JVM 内で動作します。 これを Oracle データベースに変更します。pom.xml、micorprofile-config.properties、persistence.xml の3つのファイルを修正します。
pom.xml
JDBCドライバをH2からOracle JDBC ドライバに変更します。
<!-- replace JDBC driver from H2 to Oracle -->
<!-- dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency -->
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc11</artifactId>
<version>21.5.0.0</version>
</dependency>
src/main/resources/META-INF/microprofile-config.properties
JDBCの接続情報を設定します。
OracleデータベースのJDBC接続文字列は、jdbc:oracle:thin:
から始まるいつものヤツです。
javax.sql.DataSource.test.dataSourceClassName=oracle.jdbc.pool.OracleDataSource
javax.sql.DataSource.test.dataSource.url=<OracleデータベースのJDBC接続文字列>
javax.sql.DataSource.test.dataSource.user=<ユーザ名>
javax.sql.DataSource.test.dataSource.password=<パスワード>
src/main/resources/META-INF/persistence.xml
hibernate.dialect
プロパティを Oracle 用に変更します。
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<persistence-unit name="test" transaction-type="JTA">
<jta-data-source>test</jta-data-source>
<class>me.opc.mp.database.Pokemon</class>
<class>me.opc.mp.database.PokemonType</class>
<properties>
<property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
<property name="javax.persistence.sql-load-script-source" value="META-INF/init_script.sql"/>
<!-- property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/ -->
<property name="hibernate.dialect" value="org.hibernate.dialect.Oracle12cDialect"/>
</properties>
</persistence-unit>
</persistence>
Oracle データベースとの接続動作確認
では、再度 jar を作成して接続を確かめます。
$ mvn clean package
$ java -jar target/database-mp.jar
起動に成功したら、別ターミナルから動作確認してみましょう。
$ curl http://localhost:8080/pokemon/1
{"id":1,"name":"Bulbasaur","type":12}
成功!
ネイティブ・イメージを生成する
いよいよネイティブ・イメージの生成に入ります。
Helidon はネイティブ・イメージを生成するための Maven プロファイルを持っているので、この仕組みを使います。
結構時間がかかるのでじっと我慢...
$ mvn clean -Pnative-image package -DskipTests=true
...
[INFO] ========================================================================================================================
[INFO] GraalVM Native Image: Generating 'database-mp' (executable)...
[INFO] ========================================================================================================================
...
[INFO] [1/7] Initializing... (12.0s @ 0.43GB)
[INFO] [2/7] Performing analysis... [************] (159.8s @ 5.52GB)
[INFO] 29,323 (94.56%) of 31,011 classes reachable
[INFO] 52,806 (70.01%) of 75,424 fields reachable
[INFO] 193,935 (70.58%) of 274,783 methods reachable
[INFO] 1,659 classes, 3,007 fields, and 27,648 methods registered for reflection
[INFO] 78 classes, 117 fields, and 59 methods registered for JNI access
[INFO] [3/7] Building universe... (11.2s @ 6.50GB)
[INFO] [4/7] Parsing methods... [****] (18.1s @ 2.79GB)
[INFO] [5/7] Inlining methods... [*****] (10.0s @ 4.57GB)
[INFO] [6/7] Compiling methods... [***************] (250.5s @ 8.60GB)
[INFO] [7/7] Creating image... (16.7s @ 2.95GB)
...
[INFO] ------------------------------------------------------------------------------------------------------------------------
[INFO] 61.3s (12.3% of total time) in 152 GCs | Peak RSS: 11.30GB | CPU load: 3.37
[INFO] ------------------------------------------------------------------------------------------------------------------------
[INFO] Produced artifacts:
[INFO] /home/opc/work/native-image/database-mp/target/database-mp (executable)
[INFO] /home/opc/work/native-image/database-mp/target/database-mp.build_artifacts.txt
[INFO] ========================================================================================================================
[INFO] Finished generating 'database-mp' in 8m 15s.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 08:23 min
[INFO] Finished at: 2022-05-23T02:59:54Z
[INFO] ------------------------------------------------------------------------
正常終了しました! target ディレクトリ直下に database-mp
というバイナリが出力されています。
では、さっそく実行してみましょう。
$ ./target/database-mp
...
Exception in thread "main" org.hibernate.service.spi.ServiceException: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment]
...
Caused by: org.hibernate.boot.registry.selector.spi.StrategySelectionException: Could not instantiate named strategy class [org.hibernate.dialect.Oracle10gDialect]
...
Caused by: java.lang.ClassNotFoundException: org.hibernate.dialect.Oracle12cDialect
at java.lang.Class.forName(DynamicHub.java:1121)
at org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl.classForName(ClassLoaderServiceImpl.java:130)
... 52 more
起動失敗です! ClassNotFoundException です。
これは org.hibernate.dialect.Oracle12cDialect
が persistence.xml 内で指定されているクラス名であり、起動時に Hibernate がこのクラスを動的にロードしているためだと推測できます。
ということで、ネイティブ・イメージ生成のための構成情報を追加で書かないといけないことが分かりました。
native-image agent を使って、構成ファイルを自動生成する
ネイティブ・イメージ生成のための準備をより簡単かつ便利にするために、GraalVM は Java VM 上で実行される動的機能のすべての使用状況を追跡するエージェント (注: experimental な機能です) を提供しています。
コマンドラインからでも起動できるのですが、再利用性を考慮して Maven Exec Plugin からエージェントを動かすように設定します。
以下の記述を pom.xml (<plugins>
配下)に追加します
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>native-image-agent</id>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>${JAVA_HOME}/bin/java</executable>
<workingDirectory>${project.basedir}</workingDirectory>
<arguments>
<argument>-agentlib:native-image-agent=config-output-dir=${project.basedir}/src/main/resources/META-INF/native-image/${project.groupId}/${project.artifactId}</argument>
<argument>-jar</argument>
<argument>${project.build.directory}/${project.artifactId}.jar</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
エージェントで生成されるファイル類は、このドキュメント に従って META-INF/native-image/<groupId>/<artifactId>
配下に置かれるようオプションを指定しています。
では、エージェントを起動します。
mvn exec:exec@native-image-agent
java の起動オプションに-agentlib
を追加しているだけなので、普通に Helidon サーバーが起動してきます。
エージェントが仕込まれたアプリケーションを動かす
エージェントを仕込んでも動的機能を捕捉できなれば意味がありません。
ひと通りアプリケーション・コードの主要なパスを全部通したと思える程度に動作させてからアプリケーションを終了します。
今回の場合だと、REST の全エンドポイントにリクエストが送られたこと、Database の CRUD 処理が漏れなく実施されたことを確認すべきです。
META-INF/native-image/<groupId>/<artifactId>
ディレクトリに数種類のファイルが作成されているはずです。
再度ネイティブ・イメージの生成をやってみる
$ mvn clean -Pnative-image package -DskipTests=true
...
今度はネイティブ・イメージが生成される前にエラーとなりました!
native-image.properties に追加の設定を追加する
ここからはマニュアル作業です。エラーの原因を分析して、設定を追加していきます。
META-INF/native-image/<groupId>/<artifactId>
配下に native-image.properties
ファイルを作成して、このファイルに追加の設定を記述し、エラーが無くなるまでネイティブ・イメージ生成を繰り返します。
ネイティブ・イメージの生成がエラーになった際、その原因が同時に出力されていますので、その情報を頼りに設定を追加します。--trace-object-instantiation=...
でエラーの原因をトレースしたり、それに基づいて--initialize-at-run-time=...
で初期化処理をランタイムに設定したり、等々...
今回は試行錯誤の結果、native-image.properties に以下の設定を行いました。
Args = \
--initialize-at-run-time=com.arjuna.ats.internal.jta.recovery.arjunacore
--initialize-at-run-time
はクラスの他パッケージも指定でき、その場合配下のクラス全体に適用されます。
では、再度ネイティブ・イメージを生成してみます。
$ mvn clean -Pnative-image package -DskipTests=true
# 正常終了!
正常終了になりました!では生成されたネイティブ・イメージを実行します。
# ネイティブ・イメージを起動してみる
$ ./target/database-mp
2022.05.23 04:12:06 INFO io.helidon.common.LogConfig Thread[main,5,main]: Logging at runtime configured using classpath: /logging.properties
2022.05.23 04:12:06 INFO io.helidon.tracing.tracerresolver.TracerResolverBuilder Thread[main,5,main]: TracerResolver not configured, tracing is disabled
2022.05.23 04:12:06 WARNING io.helidon.microprofile.tracing.TracingCdiExtension Thread[main,5,main]: helidon-microprofile-tracing is on the classpath, yet there is no tracer implementation library. Tracing uses a no-op tracer. As a result, no tracing will be configured for WebServer and JAX-RS
2022.05.23 04:12:06 INFO io.helidon.microprofile.security.SecurityCdiExtension Thread[main,5,main]: Authentication provider is missing from security configuration, but security extension for microprofile is enabled (requires providers configuration at key security.providers). Security will not have any valid authentication provider
2022.05.23 04:12:06 INFO io.helidon.microprofile.security.SecurityCdiExtension Thread[main,5,main]: Authorization provider is missing from security configuration, but security extension for microprofile is enabled (requires providers configuration at key security.providers). ABAC provider is configured for authorization.
2022.05.23 04:12:06 INFO org.hibernate.jpa.internal.util.LogHelper Thread[main,5,main]: HHH000204: Processing PersistenceUnitInfo [name: test]
2022.05.23 04:12:06 INFO org.hibernate.dialect.Dialect Thread[main,5,main]: HHH000400: Using dialect: org.hibernate.dialect.Oracle10gDialect
2022.05.23 04:12:06 INFO org.hibernate.engine.transaction.jta.platform.internal.JtaPlatformInitiator Thread[main,5,main]: HHH000490: Using JtaPlatform implementation: [io.helidon.integrations.cdi.hibernate.CDISEJtaPlatform$Proxy$_$$_WeldClientProxy]
2022.05.23 04:12:06 INFO org.hibernate.tool.schema.internal.SchemaCreatorImpl Thread[main,5,main]: HHH000476: Executing import script 'resource:/META-INF/init_script.sql'
2022.05.23 04:12:06 INFO io.helidon.microprofile.server.ServerCdiExtension Thread[main,5,main]: Registering JAX-RS Application: HelidonMP
2022.05.23 04:12:06 INFO io.helidon.webserver.NettyWebServer Thread[nioEventLoopGroup-2-1,10,main]: Channel '@default' started: [id: 0x96f62eb7, L:/0:0:0:0:0:0:0:0:8080]
2022.05.23 04:12:06 INFO io.helidon.microprofile.server.ServerCdiExtension Thread[main,5,main]: Server started on http://localhost:8080 (and all other host addresses) in 660 milliseconds (since JVM startup).
2022.05.23 04:12:06 INFO io.helidon.common.HelidonFeatures Thread[features-thread,5,main]: Helidon MP 2.5.0 features: [CDI, Config, Fault Tolerance, Health, JAX-RS, JPA, JTA, Metrics, Open API, REST Client, Security, Server, Tracing, Web Client]
2022.05.23 04:12:06 INFO io.helidon.common.HelidonFeatures.experimental Thread[features-thread,5,main]: You are using experimental features. These APIs may change, please follow changelog!
2022.05.23 04:12:06 INFO io.helidon.common.HelidonFeatures.experimental Thread[features-thread,5,main]: Experimental feature: Web Client (WebClient)
...
無事起動してきました! RESTのエンドポイントにアクセスして正常に動作することを確認してください。
まとめ
Java の動的機能を多く使っているアプリケーションは、GraalVM が捕捉できるように構成情報を漏れなく追加するのが面倒なので、ネイティブ・イメージ生成までたどり着く前に挫折しがちですが、エージェントを使えば相当な手間が省けます。それでも、クラスの初期化のタイミングをビルドタイムにするかランタイムするかなどの設定は相変わらず必要ですので、このあたりはコツコツやりましょう。今回、設定を一つだけ追加しましたが、それほど時間はかかりませんでした。
参考
- GraalVM のページ
- Oracle GraalVM Enterprise Edition 日本語ドキュメンテーション
- Helidon のネイティブ・イメージ関するドキュメンテーション - SE編 MP編