MyBatis+Spring BootをGraalVMでNative Image化して動かしてみよう！！

データベースへアクセスする際のライブラリとしてMyBatisを使用しているSpring Bootアプリケーションを、Nativeイメージ化して動かす方法を紹介したいと思います。この投稿では、非常にシンプルなデータアクセスを行うサンプルアプリケーションを、GraalVMを使ってNativeイメージ化して実行してみます。

# 利用する主なプロダクト・ライブラリ

* [GraalVM 22.0.0.2](https://www.graalvm.org/) (OpenJDK 17)
* [spring-boot 2.6.3](https://github.com/spring-projects/spring-boot)
* [mybatis-spring-boot 2.2.2](https://github.com/mybatis/spring-boot-starter) (mybatis 3.5.9 and mybatis-spring 2.0.7)
* [spring-native 0.11.2](https://github.com/spring-projects-experimental/spring-native)
* [mybatis-spring-native 0.1.0-SNAPSHOT](https://github.com/mybatis/spring-native)

> **IMPORTANT:**
>
> **spring-nativeとmybatis-spring-nativeはGAバージョンではないので、今後のバージョンアップにより本投稿に記載している内容で動かなくなる可能性はある点を補足しておきます。また、spring-natvieは最終的にはSpring Framework 6に組み込まれてリリースされる予定になっています。**

# spring-nativeって何者？

spring-nativeは、Spring(Spring Boot)で作成したアプリケーションをGraalVMを使ってNativeイメージ化して動かすために必要な仕組みやツールなどを提供してくれているライブラリです。詳しくは[リファレンスページ](https://docs.spring.io/spring-native/docs/current/reference/htmlsingle/)をご覧ください。

# mybatis-spring-nativeって何者？

mybatis-spring-nativeは、spring-nativeの仕組みを利用して「MyBatisから提供しているライブラリ」をNativeイメージ化して動かすために必要な設定や機能を提供するライブラリです。

> **NOTE:**
>
> 投稿時点では、以下のライブラリがサポート対象になっています。
>
> * mybatis
> * mybatis-spring
> * mybatis-spring-boot
> * mybatis-thymelaf
> * mybatis-freemarker
> * mybatis-velocity
> * mybatis-dynamic-sql

# 動かしてみよう！！

[Quick Start](https://github.com/mybatis/spring-native/wiki/Quick-Start)をベースに実際にNativeイメージ化してアプリケーションを動かしてみましょう。

# GraalVMのインストール

以下のページを参考に、各自の環境に合わせてGraalVMをインストールしてください。

* https://www.graalvm.org/22.0/docs/getting-started/#install-graalvm

なお、GraalVMをインストールしたら、以下の環境変数を設定してください。

* `JAVA_HOME`
* `GRAALVM_HOME`

本投稿では、Docker Containerを使う方法で記載します。

```コマンド実行例
docker run -v $(pwd):/work -v $HOME/.m2:/root/.m2 -it -w /work \
  -e JAVA_HOME=/opt/graalvm-ce-java17-22.0.0.2 \
  -e GRAALVM_HOME=/opt/graalvm-ce-java17-22.0.0.2 \
　　　　-e LANG=C.utf8　\
  ghcr.io/graalvm/graalvm-ce:22.0.0.2 bash
```

> **NOTE:**
>
> Dockerコンテナを使う場合は、Mavenビルド中に「'Error: Image build request failed with exit status 137'」というエラーが発生するかもしれません。これはNativeイメージ作成中にメモリ不足が発生した時に出るエラーであり、Dockerに割り当てるメモリを増やすことで解決することができます。

# プロジェクト作成

SPRING INITIALIZR経由でサンプルアプリケーション開発用のプロジェクトを作成します。

```:コマンド実行例
curl -s https://start.spring.io/starter.tgz\
     -d name=mybatis-sample\
     -d artifactId=mybatis-sample\
     -d dependencies=mybatis,h2,native\
     -d baseDir=mybatis-sample\
     | tar -xzvf - && cd mybatis-sample
```

```:コマンド実行結果
mybatis-sample/
mybatis-sample/mvnw.cmd
mybatis-sample/.gitignore
mybatis-sample/src/
mybatis-sample/src/main/
mybatis-sample/src/main/java/
mybatis-sample/src/main/java/com/
mybatis-sample/src/main/java/com/example/
mybatis-sample/src/main/java/com/example/mybatissample/
mybatis-sample/src/main/java/com/example/mybatissample/MybatisSampleApplication.java
mybatis-sample/src/main/resources/
mybatis-sample/src/main/resources/application.properties
mybatis-sample/src/test/
mybatis-sample/src/test/java/
mybatis-sample/src/test/java/com/
mybatis-sample/src/test/java/com/example/
mybatis-sample/src/test/java/com/example/mybatissample/
mybatis-sample/src/test/java/com/example/mybatissample/MybatisSampleApplicationTests.java
mybatis-sample/.mvn/
mybatis-sample/.mvn/wrapper/
mybatis-sample/.mvn/wrapper/maven-wrapper.properties
mybatis-sample/.mvn/wrapper/maven-wrapper.jar
mybatis-sample/pom.xml
mybatis-sample/HELP.md
mybatis-sample/mvnw
```

# mybatis-spring-nativeを追加

SPRING INITIALIZR経由で作成したプロジェクトにはmybatis-spring-nativeは含まれていないので、`pom.xml`に追加する必要があります。

```xml
<dependency>
  <groupId>org.mybatis.spring.native</groupId>
  <artifactId>mybatis-spring-native-core</artifactId>
  <version>0.1.0-SNAPSHOT</version>
</dependency>
```

また、mybatis-spring-nativeはまだSNAPSHOTバージョンなので、リポジトリ設定にSonatype OSSのSNAPSHOTリポジトリを追加してください。

```xml
<repository>
  <id>sonatype-oss-snapshots</id>
  <name>Sonatype OSS Snapshots Repository</name>
  <url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>
```

# Nativeイメージをビルドしてみる

まだサンプル実装を追加していませんが、Nativeイメージが作成できるか確認してみましょう！！

```:コマンド実行例
./mvnw package -Pnative -DskipTests
```

# Nativeイメージを実行してみる

```:コマンド実行例
./target/mybatis-sample
```

```:コマンド実行結果
2022-02-05 07:36:51.798  INFO 568 --- [           main] o.s.nativex.NativeListener               : AOT mode enabled

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.6.3)

2022-02-05 07:36:51.800  INFO 568 --- [           main] c.e.m.MybatisSampleApplication           : Starting MybatisSampleApplication v0.0.1-SNAPSHOT using Java 17.0.2 on bba9728656ca with PID 568 (/work/mybatis-sample/target/mybatis-sample started by root in /work/mybatis-sample)
2022-02-05 07:36:51.800  INFO 568 --- [           main] c.e.m.MybatisSampleApplication           : No active profile set, falling back to default profiles: default
2022-02-05 07:36:51.813  INFO 568 --- [           main] c.e.m.MybatisSampleApplication           : Started MybatisSampleApplication in 0.044 seconds (JVM running for 0.046)
```

起動時間を見てみると・・・「**0.046秒**」であることがわかります。早いっすね。どれくらい起動時間が早いかを比較するために、実行可能jarをjavaコマンドを使用して実行してみましょう。

```:コマンド実行例
java -jar ./target/mybatis-sample-0.0.1-SNAPSHOT-exec.jar
```

```:コマンド実行結果
2022-02-05 07:39:43.931  INFO 640 --- [           main] o.s.nativex.NativeListener               : AOT mode disabled

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::       (v0.0.1-SNAPSHOT)

2022-02-05 07:39:43.985  INFO 640 --- [           main] c.e.m.MybatisSampleApplication           : Starting MybatisSampleApplication v0.0.1-SNAPSHOT using Java 17.0.2 on bba9728656ca with PID 640 (/work/mybatis-sample/target/mybatis-sample-0.0.1-SNAPSHOT-exec.jar started by root in /work/mybatis-sample)
2022-02-05 07:39:43.985  INFO 640 --- [           main] c.e.m.MybatisSampleApplication           : No active profile set, falling back to default profiles: default
2022-02-05 07:39:44.607  WARN 640 --- [           main] o.m.s.mapper.ClassPathMapperScanner      : No MyBatis mapper was found in '[com.example.mybatissample]' package. Please check your configuration.
2022-02-05 07:39:44.925  INFO 640 --- [           main] c.e.m.MybatisSampleApplication           : Started MybatisSampleApplication in 1.27 seconds (JVM running for 1.632)
```

起動時間を見てみると・・・「**1.632秒**」なので、Nativeイメージの方が圧倒的に速いことがわかります！！ （そのかわりビルドには時間かかりますけどね・・・ :sweat_smile:）

# SQLファイルの作成

`src/main/resources/schema.sql` に以下のSQLを記載して保存する。

```sql
CREATE TABLE city
(
  id      INT PRIMARY KEY auto_increment,
  name    VARCHAR,
  state   VARCHAR,
  country VARCHAR
);
```

# ドメインクラスの作成

`src/main/java/com/example/mybatissample/City.java` に以下のコードを記載して保存する。

```java
package com.example.mybatissample;

public class City {

  private Long id;
  private String name;
  private String state;
  private String country;

  public Long getId() {
    return this.id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public String getName() {
    return this.name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getState() {
    return this.state;
  }

  public void setState(String state) {
    this.state = state;
  }

  public String getCountry() {
    return this.country;
  }

  public void setCountry(String country) {
    this.country = country;
  }

  @Override
  public String toString() {
    return getId() + "," + getName() + "," + getState() + "," + getCountry();
  }

}
```

# Mapperインターフェースの作成

`src/main/java/com/example/mybatissample/CityMapper.java` に以下のコードを記載して保存する。

```java
package com.example.mybatissample;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface CityMapper {

  @Insert("INSERT INTO city (name, state, country) VALUES(#{name}, #{state}, #{country})")
  @Options(useGeneratedKeys = true, keyProperty = "id")
  void insert(City city);

  @Select("SELECT id, name, state, country FROM city WHERE id = #{id}")
  City findById(long id);

}
```

# アプリケーションクラスの修正

`src/main/java/com/example/mybatissample/MybatisSampleApplication.java` を以下のコードに置き換えて保存する。

```java
package com.example.mybatissample;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class MybatisSampleApplication {

  public static void main(String[] args) {
    SpringApplication.run(MybatisSampleApplication.class, args);
  }

  private final CityMapper cityMapper;

  public MybatisSampleApplication(CityMapper cityMapper) {
    this.cityMapper = cityMapper;
  }

  @Bean
  CommandLineRunner sampleCommandLineRunner() {
    return args -> {
      City city = new City();
      city.setName("San Francisco");
      city.setState("CA");
      city.setCountry("US");
      cityMapper.insert(city);
      System.out.println(this.cityMapper.findById(city.getId()));
    };
  }

}
```

# Nativeイメージのビルド&実行

```:コマンド実行例
./mvnw package -Pnative -DskipTests
```

```:コマンド実行例
./target/mybatis-sample
```

```:コマンド実行結果
2022-02-05 07:56:55.364  INFO 1163 --- [           main] o.s.nativex.NativeListener               : AOT mode enabled

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.6.3)

2022-02-05 07:56:55.366  INFO 1163 --- [           main] c.e.m.MybatisSampleApplication           : Starting MybatisSampleApplication v0.0.1-SNAPSHOT using Java 17.0.2 on bba9728656ca with PID 1163 (/work/mybatis-sample/target/mybatis-sample started by root in /work/mybatis-sample)
2022-02-05 07:56:55.366  INFO 1163 --- [           main] c.e.m.MybatisSampleApplication           : No active profile set, falling back to default profiles: default
2022-02-05 07:56:55.381  INFO 1163 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2022-02-05 07:56:55.384  INFO 1163 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2022-02-05 07:56:55.387  INFO 1163 --- [           main] c.e.m.MybatisSampleApplication           : Started MybatisSampleApplication in 0.058 seconds (JVM running for 0.06)
1,San Francisco,CA,US
2022-02-05 07:56:55.388  INFO 1163 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2022-02-05 07:56:55.389  INFO 1163 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
```

```:コマンド実行例
java -jar ./target/mybatis-sample-0.0.1-SNAPSHOT-exec.jar
```

```:コマンド実行結果
2022-02-05 08:00:59.837  INFO 1255 --- [           main] o.s.nativex.NativeListener               : AOT mode disabled

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.6.3)

2022-02-05 08:00:59.896  INFO 1255 --- [           main] c.e.m.MybatisSampleApplication           : Starting MybatisSampleApplication v0.0.1-SNAPSHOT using Java 17.0.2 on bba9728656ca with PID 1255 (/work/mybatis-sample/target/mybatis-sample-0.0.1-SNAPSHOT-exec.jar started by root in /work/mybatis-sample)
2022-02-05 08:00:59.897  INFO 1255 --- [           main] c.e.m.MybatisSampleApplication           : No active profile set, falling back to default profiles: default
2022-02-05 08:01:00.861  INFO 1255 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2022-02-05 08:01:01.019  INFO 1255 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2022-02-05 08:01:01.104  INFO 1255 --- [           main] c.e.m.MybatisSampleApplication           : Started MybatisSampleApplication in 1.552 seconds (JVM running for 1.908)
1,San Francisco,CA,US
2022-02-05 08:01:01.149  INFO 1255 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2022-02-05 08:01:01.152  INFO 1255 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
```

DBアクセス処理の実行時間は、Nativeイメージが「**0.001秒**」、実行可能jarが「**0.045秒**」なので、Nativeイメージの方が圧倒的に速いことがわかります！！

# まとめ

基本的にはアプリケーションのコードを変更することなく、Nativeイメージ化できました。
ただし・・・実際のアプリケーションでは、spring-nativeがサポートしていないライブラリを使っている(使う)ことも多いと思うので、それらのライブラリをNativeイメージ化するための設定が必要になる可能性は高いかな〜と思いますが、これはspring-nativeの機能を自身のアプリケーション内で使うことで解決することができます。是非、spring-nativeを使ったNativeイメージ化にチャレンジしてみてください！！