Spring Boot 2.7からSpring Boot 3.0へ切り替えて、gradleプロジェクトアプリケーションを起動できるようにするまでの奮闘記

# 概要
Spring 2.x系のEOLが2023/11とのことで、Web/APIアプリケーションのSpring Boot 3.0への切り替え対応を検証しています。
mavenを使っている場合、spring-boot-migratorなるものが提供されていて、ある程度移行を自動化できそうです。

https://github.com/spring-projects-experimental/spring-boot-migrator

一方でうちのチームはgradleを使っているのですが、このmigratorがgradleに対応していません。
そのため、ある程度手作業で切り替えをする必要がありました。
この記事ではざっくりとした移行手順方法やハマったポイントなどをメモしたいと思います。  

なお、Spring 2.7 -> 3.0の機能変更点とその対応をすべて書いていくときりがないので、
この記事は「最低限コンパイルが通せて、アプリケーションを起動できるところまでもっていく」というところまでをターゲットにしています。  

移行前のアプリケーションの環境は以下を想定しています。

| 項目 | 内容
| --- | ---
| Java | JDK8 or JDK11
| gradle | 7.4以下
| Spring Boot | 2.7

## 切り替えるにあたっての重要な変更点
Spring 2.xからSpring 3.xに切り替えてアプリケーションを動かすにあたって、重要な条件や変更点はこちらです。

| 項目 | 内容
| --- | ---
| Java | JDK17以上にアップグレードする必要があります
| gradle | 7.5以上にアップグレードする必要があります
| JavaEE | JakartaEEへの切り替えが必要です

### 補足: 公式の移行ガイド
公式にSpring Boot 2.7から3.0へ移行するためのガイドが提供されています

https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0-Migration-Guide

この記事の前提がSpring Boot 2.7なので、Spring Boot 2.6以下を使っている場合は、まずはSpring Boot 2.7まで上げることを目標にした方が良さそうです。

# 切り替え手順
移行前のアプリケーションや自作ライブラリを確認したところ、JDKが8, 11だったりgradleのバージョンが古かったりして、
いきなりSpring Boot 3.xに持っていくのが大変な状況でした。
特にJDK 8でCMS GCを使っていたのですがJDK 17では廃止されており、
GCアルゴリズムを変える必要があったのでそのあたりの検証も必要でした。

そこでこんな感じで、段階を踏んで進めると良いかなと思います。

- 手順1: gradle 7.6化(+ Spring Boot 2.7化)
- 手順2: アプリケーションのJava17化をして本番環境にリリース & 様子見
- 手順3: アプリケーションのSpring Boot 3.0化

## 手順1: アプリケーションのgradle 7.6化(+ Spring Boot 2.7化)
Spring Boot 3.0から最低でもgradle 7.5以上に上げないとビルドできなくなるので、
ひとまずgradleを最新化し、ビルドできるかをまず試します。

Spring Boot 3.0のgradleの最低要件はgradle 7.5ですが、現時点の7.x系の最新は7.6.1です。
gradle 8系もあるのですが、Springが対応しているかわからないので今回は7.6.1に上げる手順を記載します。

まずbuild.gradleにwrapper.gradleVersionが記載されている場合、そのバージョンを直します（書いてなければここはスキップでOK）
```
wrapper {
  gradleVersion = 7.6.1 // ここを書き換える
}
```
上記の記載があろうがなかろうが、以下のコマンドを実行してください。これでgradleのバージョンが最新化されると思います。
```
./gradlew wrapper --gradle-version 7.6.1
./gradlew wrapper
```

gradleの更新ができたらアプリケーションをビルドして立ち上げられるか確認しましょう。
```
./gradlew bootRun
```

長いことメンテしてないアプリケーション・ライブラリだとgradleのバージョンが古いまま放置されていることがあるかと思います。
そういった場合はbuild.gradleの内容を大幅に書き換える必要があるので、意外と時間をとるかもしれません。

同様にSpring Boot 2.6以下を使っている場合は、Spring Boot 2.7に上げましょう。
こちらもSpring 本家が移行ガイドを公開しているので、その内容にそってあげることになります。
また、この記事の末尾にも記載していますが、
@rhirabayさんがSpring Bootの過去のバージョンの変更点をわかりやすくまとめてくれている記事があります。
そちらも参考にするとよいと思います。

## 手順2: アプリケーションのJava 17化
Spring Boot 3.0への移行と同様に、Java 8からJava 17への移行もインパクトでかい変更点です。
なのでJava17対応とSpring Boot 3.0対応をまとめて行うより、まずはJava 17対応のみを実施して本番環境にリリースし、様子を見る事をお勧めします。

すでにJava17化が終わっている方はスキップしてOKです

### 手順2-1: コードの書き換え
アプリケーションのコードの書き換えはOpenRewriteというもので自動化できそうだったので、こちらを採用しました。

https://docs.openrewrite.org/running-recipes/popular-recipe-guides/migrate-to-java-17

こちらのページに載っているようにbuild.gradleに以下のような指定を足してあげます。
```
plugins {
      id("java")
      id("org.openrewrite.rewrite") version("5.37.0")
  }
  
  rewrite {
      activeRecipe("org.openrewrite.java.migrate.UpgradeToJava17")
  }
  
  repositories {
      mavenCentral() // rewrite-spring is published to Maven Central
  }
  
  dependencies {
      rewrite(platform("org.openrewrite.recipe:rewrite-recipe-bom:1.16.0"))
      rewrite("org.openrewrite.recipe:rewrite-migrate-java")
  
      // Other project dependencies
  }
```
あとはrewriteRunを実行するだけでOKです
```
./gradlew rewriteRun
```

書き換え終わったらbuildやbootRunしてアプリケーションが動くか試しましょう。

引き込んでいるライブラリがJava 17に対応してないこともあるので、
そういった場合はライブラリのJava 17対応状況などを確認して、適宜バージョンアップなどを行いましょう。

### 手順2-2: GCアルゴリズムの選択(必要があれば)
前述のようにいままでCMS GCを使っていたのですがそれが使えなくなったので、
新しいGCアルゴリズム採用する必要がありました。

ParallelGC、G1GC、ZGCを検証したのですが、以下のような経緯があって最終的にG1GCを採用しました。
※ この辺りは機会があれば記事をまとめたい...

| GC  | 試した結果
| --- | ---
| ParallelGC | メジャーGCがたまに発生し、ストップザワールドでガツンと止まることがあるので不採用
| G1GC(採用) | メジャーGCがParallelGCほど発生せず、<br>ZGCと比較してJVMヒープ以外のメモリ使用量もそれほどではないので採用。<br>コンテナメモリ1GB、JVMヒープ500MB程度のスペックでもちゃんと動いた
| ZGC | ストップザワールドがほぼないというのが魅力的で採用したかった。<br>が、JVMヒープ以外のメモリ使用が多く、<br>コンテナOOM Killを招きやすそうだったので採用見送り

個人的には特に理由がなければG1GCをおすすめしますが、
この辺りはサービス要件やサーバースペックによって向いてるGCも変わってきます。
なのでちゃんと各自で検証したうえで適切なGCを採用するとよいかなと思います。

以下のようなJVM引数を追加してG1GCを有効化します。
```
-XX:+UseG1GC
```
GCアルゴリズムは明記しないと、サーバーのスペック次第ではSerialGCが選ばれてしまうため、この指定は必ず明記しましょう。
特にKubernetes環境の場合、コンテナメモリを2GB未満 or CPU 2未満にしていると、SerialGCが選ばれてしまいます。リソースを絞っている方はご注意を。

### 手順2-3: 本番リリース
機能面でのテストや、性能試験や長時間ラン試験などをしてGCに問題ないかなど確認したら、本番リリースしましょう。

しばらく様子をみて問題なければOKです。

## 手順3 アプリケーションのSpring Boot 3.0化
Gradle 7.6化、Spring Boot 2.7化、Java 17化が終わったら、ようやく本題のSpring Boot 3.0化に着手できます。
ようやく本題です。
担当しているシステムが古い場合、ここまでの事前準備でもそこそこ時間がかかると思います。
なのでここまで読んで「やることいっぱいで大変...」と感じた方は、上司などに早めにかけあって工数を確保すると良いかもしれません。

### 手順3-1: コードの書き換え(Java EE -> Jakarta EEなど)
冒頭で述べたようにSpring Boot 3.0からJava EEからJakarta EEへの切り替えが必要になります。
他にもいろいろと書き換えないといけないのですが、これはJava 17化の時と同様にopenRewriteである程度自動化できます。

この件に関してはこちらのブログの内容をそのまま採用すればまとめて書き換えができるので、
まずはこちらの記事の内容に従ってコードを自動修正しましょう。

https://hirabay.net/?p=142

これでjavaソースコード中のjavaxのimport文がもれなくjakartaに書き換わっているはずです。

### 手順3-2: コンパイルエラーやランタイムエラーとの闘い
さて、ここからが手がかかる部分です。

openRewriteはソースコード中のJava EEのimport文をJakarta EEのimport文に置き換えてくれますが、
build.gradleのdependenciesで引き込むライブラリを自動的に修正してくれません。
そのため、openRewriteを実行しただけではコンパイルが通らず、buildもtestもbootRunも通らない状態です。
なので依存ライブラリをJava EE用からJakarta EE用に置き換えてあげたり、その他ライブラリもバージョンアップしたり切り替えたりの対応が必要です。

また、他にもSpring自体のクラスが変わってコンパイルが通らなくなっていたり、
httpclientやlogbackなどのコアライブラリのバージョンが上がったりして、
コンパイルできなかったり実行時エラーが発生したりします。
この辺りの大変さは、アプリケーションが引き込んでいるライブラリの数や種類によりますが、そこそこしんどいです...。

自分が遭遇したトラブルに関して、簡易的に対応内容などを記載します。

#### 手順3-2-1: JavaEE -> JakartaEEのライブラリ切り替え
build.gradleにある「javax」と名のついたライブラリをjakartaのライブラリに置き換えましょう。

例えばvalidation-apiの場合であればこんな感じになります。

before
```
implementation 'javax.validation:validation-api'
```

after
```
implementation 'jakarta.validation:jakarta.validation-api'
```

移行先のjakartaのライブラリ名は、spring-boot-dependenciesの中身を見るとあたりが付くと思うので、このあたりも参考にしてください。

https://github.com/spring-projects/spring-boot/blob/v3.0.4/spring-boot-project/spring-boot-dependencies/build.gradle

#### 手順3-2-2: Spring Cloud Sleuthライブラリの対応
Spring Cloud SleuthはどうやらSpring Boot 3.0のサポートを行わないようで、
[Micrometer Tracing](https://micrometer.io/docs/tracing)へ移行する必要があるそうです
https://docs.spring.io/spring-cloud-sleuth/docs/current-SNAPSHOT/reference/html/

> Spring Cloud Sleuth will not work with Spring Boot 3.x onward. The last major version of Spring Boot that Sleuth will support is 2.x.
> The core of this project got moved to Micrometer Tracing project and the instrumentations will be moved to Micrometer and all respective projects (no longer all instrumentations will be done in a single repository.

before
```
implementation 'org.springframework.cloud:spring-cloud-starter-sleuth'
```

after
```
implementation 'io.micrometer:micrometer-tracing'
implementation 'io.micrometer:micrometer-tracing-bridge-brave' // braveではなくotelでもOK
```

#### 手順3-2-3: logback 1.2.x -> 1.4の対応
xxmlファイルを用意して独自のログフォーマットの出力を行っている場合、
パーサーの仕様変更で以前のxmlファイルが読み込めなくなっていることがあります。
そうなるとアプリケーションログが何も出力されなくなる可能性があります。
ログが見えないとほかのライブラリで問題があったときのエラー情報も見れなくなってしまうため、
このライブラリの問題は最優先で解消した方が良いです。

##### エラー事例1 : ifタグのネストに関するエラー
```
-WARN in IfNestedWithinSecondPhaseElementSC - <if> elements cannot be nested within an <appender>, <logger> or <root> element
-WARN in IfNestedWithinSecondPhaseElementSC - See also http://logback.qos.ch/codes.html#nested_if_element
```

このログが出た場合はエラーメッセージの通りで、
appenderやlogger, rootタグの中にifタグを書くのがダメになったようです。

エラーメッセージ中にあるこちらのページの「Nested <if> element within <appender>, <logger> or <root> elements is disallowed」の案内に記載されているように、
ifタグをroot, appender, loggerタグの外に出してあげることで解消します。
http://logback.qos.ch/codes.html#nested_if_element

##### エラー事例2 : ログパターン文字列のパース失敗
```
Logging system failed to initialize using configuration froｍ ’null’
java.lang.illegalstateexception logback configuration error detected:
ERROR in ch.qos.logback.classic.PatternLayout("<ログパターン文字列>") - Failed to parse pattern "<ログパターン文字列>"
```
ログパターン文字列のパース処理がアップデートされた影響で、以前までの書き方だと受け付けられないログパターン文字列になっている場合、このエラーが発生します。

ログパターン文字列をいろいろと書き換えて、パース処理の妨げになっている個所を特定し、修正しましょう。
参考になるかわかりませんが、実際にパースがダメだった実例として1つ。
```\:```がパース出来なかったので```:```に直しました。

before
```
time\:%clr(%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}){yellow}
```

after
```
time:%clr(%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}){yellow}
```


#### 手順3-2-4: httpclient 4系からhttpclient 5系のライブラリ切り替え
httpclientは4系から5系に上げる必要があります。
パッケージ名やライブラリ名自体が変わっているので、build.gradleもそうですが、実装コードの方も直す必要があります。

build.gradleの場合はこんな感じに修正します。

before
```
implementation 'org.apache.httpcomponents:httpclient'
```

after
```
implementation 'org.apache.httpcomponents.client5:httpclient5'
```

なお、HttpComponentsClientHttpRequestFactoryでhttpclient5を使う場合、
setReadTimeoutメソッドが非推奨となっているのでsetSoTimeoutメソッドを使うように変えた方がよさそうです。

https://spring.pleiades.io/spring-framework/docs/current/javadoc-api/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.html

> 非推奨、削除予定: この API 要素は、将来のバージョンで削除される可能性があります。
6.0 の時点で、SocketConfig.Builder.setSoTimeout(Timeout) を推奨。上記を参照してください。

httpclient4系からhttpclient5への移行やsetSoTimeoutの設定方法は
こちらの公式の移行ガイドを参考にしてください。


https://hc.apache.org/httpcomponents-client-5.2.x/migration-guide/index.html


上記以外にもライブラリによって対応が必要なものが出てくると思いますが、
ひとまず今回の記事はここまでの紹介にしておきます。

最終的に
```
./gradlew bootRun
```
でアプリケーションがちゃんと動けばSpring Boot 3.0化の第1段階が終了します

## おわり : まだまだやることは山積み
この記事で紹介する内容はここまでです。
これでようやくSpring Boot 3.0化対応のスタートラインに立てた状態です。

この後さらに

- Spring Boot 2.7 -> 3.0の変更点への追随
- その他ライブラリの変更点の追随
- 機能面でのテスト
- 性能試験
- 負荷試験
- 本番リリース

などなど、本番リリースまでにやることはまだまだ残っています。

ただ、そもそもビルドやbootRunが通らないようであればSpring Boot 3.0の変更点の検証もテストもしようもないです。
なので今回の記事では「とにかく動かすこと」に着眼して、それまでにやらないといけないタスクとその概要を書いてみました。

どれぐらい大変なのかは現在のシステムのgradle, Java, Spring Bootのバージョンの古さによっても変わってきます。
なのでこの記事の内容を参考に、早めに最低でも1つのアプリケーションだけでもSpring Boot 3.0化を試してみて、実際かかる工数を出してみるとよいかもしれません。
そうするとシステム全体のSpring Boot 3.0化の工数が見積もりやすくなるので、Spring Boot 2.7 EOLまでにバージョンアップを完遂させるための計画を立てやすくなるかなと思います。

### 関連記事
前述のようにSpring Boot 3.0化して動かせるようになったら、変更点の確認や追随をする必要があります。
この点に関しては、@rhirabayさんがSpring Bootのバージョンアップの変更点の要点などをわかりやすくまとめた記事を公開しています。
なのでSpring公式の移行ガイドと併せて、これらの記事も読んでみると良いと思います。

https://qiita.com/rhirabay/items/f9bef61bd4fa90a91919

https://qiita.com/rhirabay/items/524caa3c255fda1c390d