springbootアプリでカスタムJREを使おう

こんばんは、GxP高田です。
これは[グロースエクスパートナーズ Advent Calendar 2022のカレンダー](https://qiita.com/advent-calendar/2022/gxp)の22日目の記事です。2が多めで縁起がいいですね。
サンタクロースも走り回る師走を忙しくお過ごしの皆様に、さっと読めて来年からと言わず明日から使える知識をお届けしたいと思います。

# Javaアプリのコンテナベースイメージどれにする?
アプリの実行環境として、Dockerに代表されるようなコンテナで実行されることがほとんどになってきました。
Javaを稼働させるためには、Java実行環境（Java Runtime Environment）が必要になるため、どうしてもコンテナイメージサイズが大きくなりがちです。どのイメージがいいのか、イメージサイズ、セキュリティ、メンテナンスが続くのか、などで大いにお悩みなのではないでしょうか。
JDK11から利用可能になったjlinkというツールを使うと、必要な機能に絞った可搬性の高いカスタムJREを作成できます。カスタムJREによって、ベースイメージにJDKを含む必要がなくなるため、ベースイメージ選択肢を広げることができます。

早速やってみましょう。

## 環境
今回はwindows11のwsl上でeclipse-temulin17.0.5で実行しています
```
$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=22.04
DISTRIB_CODENAME=jammy
DISTRIB_DESCRIPTION="Ubuntu 22.04.1 LTS"

$ java -version
openjdk version "17.0.5" 2022-10-18
OpenJDK Runtime Environment Temurin-17.0.5+8 (build 17.0.5+8)
OpenJDK 64-Bit Server VM Temurin-17.0.5+8 (build 17.0.5+8, mixed mode, sharing)
```
アプリは2020年のアドベントカレンダーの記事「[Testcontainersでspring Data JPAをいい感じにテストする](https://qiita.com/yTakada-gxp/items/458154a91c9794fb99dc)」で作ったものを再利用します。springboot3にupdateしたり、RESTful APIを追加していますが本記事とは無関係なので割愛します。
[リポジトリはこちら](https://github.com/YasuhiroTakada/testcontainers-spring-demo)。
なお、以降に出てくるコマンドはチェックアウトしたディレクトリで実行しているものとします。

<details><summary>コンパイルなど</summary>
 
まずはコンパイル。gradleアプリなのでbuildコマンドでjarまで作成しましょう。
```
$ ./gradlew build
$ ls -l ./buid/libs/
合計 39800
drwxrwxrwx 1 root root     4096 12月 20 13:39 ./
drwxrwxrwx 1 root root     4096 12月 20 13:39 ../
-rwxrwxrwx 1 root root     9826 12月 20 13:39 testcontainer-spring-demo-2022.12-plain.jar*
-rwxrwxrwx 1 root root 40741269 12月 20 13:39 testcontainer-spring-demo-2022.12.jar*
```
</details>

## カスタムJREを作ろう
### jdepsで依存するモジュールを調べる
jlinkでは、モジュールという単位で必要な機能を列挙してカスタムJREを作成します。JDK9以降には、必要なモジュールを知るために、jdepsというコマンドラインツールが用意されています。
jdepsにはいろいろなオプションがありますが、jlinkのモジュールを判断するために必要なのは`--class-path`, `--print-module-deps`, `--ignore-missing-deps`です。
使い方はヘルプを見てみましょう。出力が多いので絞って結果を表示します。
```
$ jdeps --help
...
  -cp <path>
  -classpath <path>
  --class-path <path>           クラス・ファイルを検索する場所を指定します
...
  --print-module-deps           モジュール依存性のカンマ区切りリスト
                                を出力する--list-reduced-depsと同じです。
                                この出力は、これらのモジュールとその推移的な
                                依存性を含むカスタム・イメージを作成するために
                                jlink --add-modulesで使用できます。
  --ignore-missing-deps         欠落している依存性を無視します。
...
```

:::note warn
依存関係の指定方法
springbootアプリはjar内部に`/BOOT-INF/lib/`というディレクトリに依存ライブラリを保持し、起動時に読み込む仕組みを持っています。これらのjarをjdepsのクラスパスに含める必要があります。
:::

#### jdeps: 依存ライブラリ指定なしで（正しくない！）
```
$ jdeps --print-module-deps --ignore-missing-deps ./build/lib/testcontainer-spring-demo-2022.12.jar
java.base,java.logging
```
#### jdeps: 依存ライブラリ指定あり
```
# jarを展開
$ unzip testcontainer-spring-demo-2022.12.jar -d flatten 
$ jdeps -R -cp "flatten/BOOT-INF/lib/*" --print-module-deps --ignore-missing-deps --multi-release 17 build/libs/testcontainer-spring-demo-2022.12.jar
java.base,java.compiler,java.desktop,java.instrument,java.management,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql.rowset,jdk.jfr,jdk.unsupported
```
出力結果のモジュール数が2と15で全然違いますね。

## jlinkでカスタムJREを作ってみる
jdepsコマンドの結果により依存するモジュールが分かったところで、いよいよカスタムJREを作成します。jlinkコマンドを実行します。

```
$ jlink \
         --add-modules java.base,java.compiler,java.desktop,java.instrument,java.management,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql.rowset,jdk.jfr,jdk.unsupported \
         --strip-debug \
         --no-man-pages \
         --no-header-files \
         --compress=2 \
         --output ./javaruntime
```
:::note info
jlinkコマンドのオプション
`add-modules`以外の指定したオプションについては
[Oracleのjlinkコマンド](https://docs.oracle.com/javase/jp/17/docs/specs/man/jlink.html)や`man jlink`を確認してください
:::

javaruntimeディレクトリが作成されます。サイズを測ってみましょう。54MBという結果でした。

```
$ du -sh javaruntime/
54M     javaruntime/
```
コンテナ化した際の容量も比較してみましょう。ベースイメージが異なるので比較にあまり意味はないですが、小さくなった実感は得られるのではないでしょうか。
```
$ docker images
REPOSITORY                             TAG            IMAGE ID       CREATED              SIZE
local/demo-2022.12/eclipse-temurin17   latest         ff176a736649   About a minute ago   545MB
local/demo-2022.12/custom-jre          latest         36130c388e5b   2 minutes ago        166MB
```
eclipse-temurin17としているイメージは`eclipse-temurin:17.0.5_8-jdk`をベースイメージにアプリのjarファイルを追加したもの、custom-jreはjlinkで作成したカスタムJREを格納したイメージです。

::: note info
イメージの作成方法
詳細はリポジトリ中の[Dockerfile](https://github.com/YasuhiroTakada/testcontainers-spring-demo/blob/main/Dockerfile), [Dockerfile.eclipse-temurin17](https://github.com/YasuhiroTakada/testcontainers-spring-demo/blob/main/Dockerfile.eclipse-temurin17)を参照してください
:::


## まとめ
jdeps、jlinkというコマンドラインツールを用いてカスタムJREを作る方法をご紹介しました。Microsoft AzureでJavaを稼働させる際の公式ドキュメント、eclipse-temurinのdockerhubの説明にも手順が紹介されているなど、徐々に浸透してきている印象ですが、springbootでのモジュール一覧の取得方法などのノウハウと合わせて紹介しました。
JDK8時代の開発のノウハウをアップデートする一助になると幸いです。

## おまけ
### カスタムJREはjfrでプロファイルできるの？
できますよ。
JavaにはJava Fright Recorderという、パフォーマンス分析のための情報収集ツールが存在します。カスタムJREでも`jdk.jfr`モジュールをjlinkコマンドのadd-modulesオプションに加えることで可能になります。

サンプルのDockerfileでは起動設定でjfrを有効にしています。
```
ENTRYPOINT ["java","-XX:StartFlightRecording=name=app-profile,filename=/app-profile.jfr,dumponexit=true,maxsize=10m,settings=profile,disk=true","-jar","/app.jar"]
```

アプリを起動するとオプションの`filename`で指定した場所にファイルが作成されます。
```
docker exec -ti testcontainers-spring-demo-app-1 /bin/bash              
root@71b169124d59:/# ls -l
total 39852
-rw-r--r--   1 root root        0 Dec 20 15:39 app-profile.jfr
```
# 参考
* [jlinkでカスタムJREを作成しコンテナ・サイズを65%削減 - Spring Boot - Qiita](https://qiita.com/yoshioterada/items/bcdbc8967bbfda91a2cc)
* [jlink を使用した Java ランタイム | Microsoft Learn](https://learn.microsoft.com/ja-jp/java/openjdk/java-jlink-runtimes)
* [eclipse-temurin - Official Image | Docker Hub](https://hub.docker.com/_/eclipse-temurin)
* [OpenJDK での JDK フライトレコーダーの使用 OpenJDK 17 | Red Hat Customer Portal](https://access.redhat.com/documentation/ja-jp/openjdk/17/html/using_jdk_flight_recorder_with_openjdk/index)