【GraalVM】SpringBoot3.0 でネイティブイメージを作成

Java Developers Summit Online 2023 に参加しました!

2023年2月28日にオンライン開催された Java Developers Summit に初めて参加しました。
恥ずかしながら GraalVM 周りの仕組みや JIT コンパイルとの違いなど、初めて学ぶことが多かったです。

今回は、GraalVMSpringBoot3.0 で導入されたネイティブイメージのサポート機能を簡単なサンプルアプリケーションを作って試してみたいと思います。

GraalVM のインストール

私のローカルPCは MacBook のため、brew コマンドでインストールしました。
インストール後に java --version を実行すると下記のようになりました。

openjdk 17.0.6 2023-01-17
OpenJDK Runtime Environment GraalVM CE 22.3.1 (build 17.0.6+10-jvmci-22.3-b13)
OpenJDK 64-Bit Server VM GraalVM CE 22.3.1 (build 17.0.6+10-jvmci-22.3-b13, mixed mode, sharing)

GraalVM がインストールされたようですね!

SpringBoot3.0 のサンプルアプリケーションの準備

今回は下記を満たす簡易機能を実装しようと思います。

  • /users でユーザー情報全件を取得する
  • ユーザー情報は h2 の組み込みデータベース上に登録しておく
  • DB アクセスは JdbcTemplate で実施する(MyBatis などがネイティブ対応しているか不明だったため)

Spring Initializr で Gradle プロジェクトを作成

Spring Initializr で必要な情報を選択し、Gradle プロジェクトを作成します。

  • Gradle - Groovy
  • Java 17
  • Spring Boot 3.0.3
  • Artifact - native-image-rest-api
  • Dependencies
    • Spring Web
    • H2 Database
    • Spring Data JDBC
    • GraalVM Native Support 今回の主役です!

IntelliJ で実装開始

ダウンロードした zip ファイルを開き、builde.gralde を(大好きな) IntelliJ で開きます。
そして早速エラーが出ました。。。

No matching variant of org.springframework.boot:spring-boot-gradle-plugin:3.0.3 was found. 
The consumer was configured to find a runtime of a library compatible with Java 11,
packaged as a jar, and its dependencies declared externally,
as well as attribute 'org.gradle.plugin.api-version' with value '7.6.1' but:

Java のバージョンが 17 になっていないようなので、IntelliJ の Preferences から Gradle の Java バージョンを 17 に変更して再トライしました。

BUILD SUCCESSFUL in 10s

無事に通過しました!

IntelliJ で実装開始(再トライ)

users テーブルから user のリストを取得するということで、下記のようなクラスを一通り作成します。

  • User レコード(Java の record です)
  • UserController
  • UserQueryService
  • UserRepository(Interface)
  • UserDataSource(implements UserRepository)

また、h2 組み込みデータベースを使用するので resources 配下に sql ファイルを用意します。

  • schema.sql(users テーブルの create)
  • data.sql(数十件のユーザーデータ)

文末に GitRepository を載せておきますが、要素だけかいつまんで紹介します。

  • User
public record User(String name, int age) {
}
  • UserDataSource
@Repository
public class UserDataSource implements UserRepository {

    JdbcTemplate jdbcTemplate;

    public UserDataSource(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public List<User> findAll() {
        return jdbcTemplate.query("select * from users"
                , ((rs, rowNum) ->
                        new User(rs.getString("name"), rs.getInt("age")))
        );
    }
}

それ以外は基本的に 3 層アーキテクチャにのっとっています。

JVM 上で起動

まずはいつも通り実行してみます。

2023-03-01T17:31:24.125+09:00  INFO 51217 --- [           main] c.e.demo.NativeImageRestApiApplication   : Started NativeImageRestApiApplication in 2.594 seconds (process running for 3.031)

起動しました!
(後で 2.594 seconds とネイティブイメージでの起動時間を比較します)

実行結果も問題なさそうです。

 % curl http://localhost:8080/users
[{"name":"れなーず太郎01","age":34},{"name":"れなーず太郎02","age":34},{"name":"れなーず太郎03","age":34},{"name":"れなーず太郎04","age":34},{"name":"れなーず太郎05","age":4},{"name":"れなーず太郎07","age":34},{"name":"れなーず太郎08","age":34},{"name":"れなーず太郎09","age":34},{"name":"れなーず太郎10","age":34}]

ネイティブイメージの作成

ここから本題です。
ネイティブイメージをビルドしてみようと思います。

gradle タスクに nativeCompile なるものがあるので実行してみます。

はい、失敗しました。。。

Execution failed for task ':nativeCompile'.
> Cannot query the value of property 'javaLauncher' because it has no value available.

javaLauncher ってなんぞや???
と思いながらGraalVM のサイトなどをググりまして、build.gradle に下記を記載してみました。

  • build.gradle
graalvmNative {
  binaries {
        main {
            javaLauncher = javaToolchains.launcherFor {
                languageVersion = JavaLanguageVersion.of(17)
                vendor = JvmVendorSpec.matching("GraalVM Community")
            }
            mainClass = 'com.example.demo.NativeImageRestApiApplication'
        }
  }
}

そして再度 nativeCompile タスクを実行するとコンパイルが始まりました!(よかった)
コンパイル自体は 4m17s 要しました。やはりネイティブイメージを作成するのと、普通に jar パッキングするのとは訳が違いますね。

ネイティブイメージの起動

build/native/nativeCompile に作成されるので、そこに cd して起動してみます。

./native-image-rest-api

起動しました!

2023-03-01T17:45:16.155+09:00  INFO 51366 --- [           main] c.e.demo.NativeImageRestApiApplication   : Started NativeImageRestApiApplication in 0.129 seconds (process running for 0.156)

そして起動が速いですね!0.129 secondsです。
JVM 上だと 2.594 seconds だったので、起動時間は元の 5% 程度まで短縮しました。(すごい!)

さらに実行結果ですが、

curl http://localhost:8080/users
[{"name":"れなーず太郎01","age":34},{"name":"れなーず太郎02","age":34},{"name":"れなーず太郎03","age":34},{"name":"れなーず太郎04","age":34},{"name":"れなーず太郎05","age":4},{"name":"れなーず太郎07","age":34},{"name":"れなーず太郎08","age":34},{"name":"れなーず太郎09","age":34},{"name":"れなーず太郎10","age":34}]%

JVM で実行した場合と変わりなく、結果を取得することができました。

まとめ

なんとなく聞いたことのあった GraalVM について、サミットに参加したことで触れてみるきっかけとなりました。
やはりこういったイベントに参加することは有意義になりますね。

また、SpringBootGraalVM をサポートしたことにより、その可能性は色々と広がっていくかなと思います。
私は Java 大好きマンなので、マイクロサービスや FaaS といったものが主流になりつつある今、JVM の弱点がこのように改善されていくのは非常に嬉しく感じました。

最後に今回のサンプルコードはこちらにありますので、よかったらご覧ください。
https://github.com/leonards-architect/native-image-rest-api