SpringBoot2.7.x以前から3.2.xへOpenRewriteでアップグレード

## 概要
アップグレードの作業を出来るだけ再現しやすい手順にまとめようと思います。
このアップグレードには下記の要素が含まれます。

- Java11からJava21へのバージョンアップ
- Gradleのバージョンアップ
- Spring Boot3.2へのグレードアップをOpenRewriteで半自動化

インパクトが大きい変更が多いので、2.7.x以前の環境も残したうえで、3.2.xで開発できる状態を目指します。

## 環境
この記事で想定している環境は下記となります。

- Gitを使用
- Gradleを使用（Mavenでも応用はできると思います）
- Spring Bootを使用
- OpenRewriteを使用
- 筆者のIDEはEclipse
- 筆者のOSはWindows

IDEやOSによる差異はあまり多くないと思いますが、参考にしていただければと思います。

## 下準備
### 現（2023/11/21）時点でのJava21およびSpringの環境を確認
現時点ではツールやライブラリのJava21対応が遅れている場合があります。

例えばEclipseではJava21対応のためにマーケットプレイスからプラグインをインストールする必要があります。
また、Spring Bootの言語サーバーに問題があり、起動時にエラーが報告されます。ネットワーク周りにバグがあるようで、これに起因するのか、 **現時点ではSTSのメニューからのアップグレードは失敗します。**

また、STSの問題として、JPAで複合キーを使用している場合に、[IDがドメインの期待する型と異なる旨の警告が表示](https://github.com/spring-projects/sts4/issues/1139)されます。
これについてはIDE上の表示だけの問題でビルドや起動は可能です。また、近日中に修正される見込みです。

上記以外にも問題が存在する可能性があります。
利用しているIDEやプラグインの対応状況、バグが報告されていないかを先にチェックしておくと、アップグレードによって問題が発生しているのか、そうでないのかの切り分けが早くできるようになります。

### プロジェクトを新しいローカルリポジトリへclone
アップグレード前の環境を残せるよう、新しいローカルリポジトリへプロジェクトをcloneします。
IDEによっては環境がフォルダに依存しますので、今までの環境と別のフォルダを新たに作成して、その中へcloneする必要があるかもしれません。
Eclipseの場合、同じプロジェクト名がワークスペース内に共存できないため、新しいワークスペースを作成しておく必要があります。
cloneしたら、まずはこれまでの環境で動くかを確認しておきましょう。

### SpringSecurityの設定のアップグレード
SpringSecurityを採用している場合、あらかじめ設定を手動である程度修正しておきます。

SpringSecurityの設定については、[バージョン5から6にかけて大幅に書式が変更されて](https://qiita.com/suke_masa/items/908805dd45df08ba28d8)きました。
これら変更もOpenRewriteのレシピ（詳しくは後述）に含まれてはいますが、完全ではないのが実情のようです。

各設定の中でも、`.and()`が削除されることから、ラムダ式を使った書式への変更と、`WebSecurityCustomizer.ignoring()`ではなく`requestMatchers(...).permitAll()`を使用した設定に変更しておく方がこの後の作業が減らせるかと思います。
変更後は、起動と動作確認を行い、アップグレード前の状態としてコミットしておきましょう。

## OpenWreriteを使ったアップグレード
実際のアップグレード手順についてです。
STSのメニューから実行できれば何の問題もありませんが、先述の通りエラーによって使用できませんので、ここではSTSが内部で利用している[OpenRewrite](https://docs.openrewrite.org/)を手動で導入します。

OpenRewriteはソースコードを静的に分析して、ソースコードを書き換えます。書き換える内容は[レシピ](https://docs.openrewrite.org/recipes)としてまとめられており、[Spring関連も専用のカテゴリが用意](https://docs.openrewrite.org/recipes/java/spring)されています。

一つ一つはかなり細かい修正ですが、あるレシピから別のレシピを参照することが可能となっており、Spring Bootのアップグレードについても複合レシピとして統合されており、大変便利です。

### OpenRewriteの設定をbuild.gradleに記入する
OpenRewriteのレシピを使用するには、builde.gradleにプラグインとレシピへの依存性を追記するだけです。しかもこの例では「Migrate to Spring Boot 3.2」を適用するだけで大部分の修正に対応します。

Spring Boot3.2のレシピの「[Usage](https://docs.openrewrite.org/recipes/java/spring/boot3/upgradespringboot_3_2#usage)」を参考にOpenWreriteの設定をbuild.gradleに追記します。plugiに1行、rewriteブロック、dependencyに1行と簡単です。
また、Mavenの場合もPOMに追記で利用可能なようです。タブの切り替えで確認できますので、参照してください。

またこの時点でSpring Bootには基本的には含まれていない、選択的なライブラリに対するレシピがないか確認しておくと良いかと思います。手作業は減らせるのであれば減らしていきましょう。

### rewiteコマンドを実行する
OpenRewriteの設定を記述したなら、Gradle Wrapperからコマンド（タスク）を実行するだけです。
いきなり実行するのが怖い場合も、`rewriteDryRun`を実行すると`/build/reports/rewrite`内にGitのパッチ形式で変更内容が出力されます。
オプションの`--info`を付けておくとコンソールにエラーログも出力されるため、（頑張って読めば）どの処理が失敗しているかを確認することもできます。
処理には数分からの時間がかかりますが、少し待ちましょう。

```bat
#　慎重を期すならDryRunでパッチを先に確認
.\gradlew.bat rewriteDryRun
#　必要に応じて--infoを付けて詳細なログを表示させる
.\gradlew.bat rewriteRun --info
```

### Gradle（Wrapper）のバージョンアップを段階的に行う
GradleでOpenRewriteを実行した場合、Gradle Wrapperのバージョンアップもレシピに含まれています。
とは言っても、gradle-wrapper.propertiesを書き換えてくれるだけで、実際にGradleのコマンドを実行してくれるわけではありません。[^1]
手動でコマンドを実行してGradleのバージョンをあげる必要があります。

このとき、アップグレード後のGradleのバージョンによって、必要になるJavaのバージョンが異なることに注意してください。
[Javaとの互換性の表](https://docs.gradle.org/current/userguide/compatibility.html#java)を参考に、必要なバージョンのJavaをインストールしておく
必要があります。
今回のレシピでは[Migrate to Spring Boot 3.0](https://docs.openrewrite.org/recipes/java/spring/boot3/upgradespringboot_3_0)の時点でGradle7.4が指定されますので、Java17がインストールされていない場合にはインストールが必要です。
Java21とGradle8.4以上にする場合も、GradleのバージョンとJavaのバージョンが密接にかかわるため、現在の環境によっては一旦`.\gradlew.bat wrapper`でGradle7.4（のWrapper）をインストールを行い、Java17に切り替えたうえで、改めて8.4以上にする必要があります。そして、8.4の実行にはJava21が必要になります。

Javaのインストールとバージョンの切り替えの作業には「[Scoop](https://github.com/ScoopInstaller/Scoop#readme)」を利用することをお勧めします。利用法などは[私の過去記事](https://qiita.com/talesleaves/items/0880bf31359715035a3c)などを参照してください。
[^1]:環境によるのかもしれません。

### build.gradleの修正
SpringのDependency Management Pluginを使用している場合、レシピからは依存性の設定が書き換えられません。[^2]
このため、build.gradleに手作業で修正を入れる必要があります。
[Spring Initializr](https://start.spring.io/)で生成されるbuild.gradleと比較して修正するのが一番良いかもしれません。

[^2]:dependenciesブロックに直接書いている場合は書き変わると思われますが試していません。

## レシピの漏れ・副作用を修正する
OpenRewriteのレシピによる修正も完全ではなく、やはりIDEの構文チェックや実際に起動させての動作確認が必要になります。
利用しているライブラリの違いで網羅することは難しいとは思いますが、私が実際に手で修正したものを挙げてゆきます。

### （Security）HttpSecurity.requestMatcher(...)をsecurityMatcher(...)に
レシピから漏れているようなので、手修正が必要です。

### （Security）SecurityFilterChainが複数ある場合は固有の名前に変更
セキュリティ設定の実体となる`SecurityFilterChain`が複数ある場合をOpenWreriteのレシピでは考慮していません。
rewrite後のメソッド名が同じになってしまうためBean名も同一となり、起動に失敗します。
メソッド名を一意に修正して、Bean名が重複しないように修正します。

### AuthenticationManagerBuilder.inMemoryAuthentication()をInMemoryUserDetailsManagerに
これは不完全に書き換えられてしまうため、ユーザー設定を再度コーディングする必要があります。
returnされるInMemoryUserDetailsManagerにUser.Builderで設定していたユーザー名やパスワードを設定して`build()`したUserを渡すだけです。概念的には以下のような変更になります。

```java:修正前
    /**
     * ConfigurerAdapter内で
     */
    @Override
    protected void configure(
            AuthenticationManagerBuilder authenticationManagerBuilder)
            throws Exception {

        authenticationManagerBuilder.inMemoryAuthentication()
                .withUser("USER1").password("PASSWORD")
                .roles("USER");
    }
```
```java:修正後
    /**
     * Config内のどこかで
     */
    @Bean
    InMemoryUserDetailsManager inMemoryUserConfig() {
        return new InMemoryUserDetailsManager(User.builder()
                .username("USER1").password("PASSWORD")
                .roles("USER").build());
    }
```
また、同系統の`jdbcAuthentication()`や`ldapAuthentication()`も同様の影響を受ける可能性があるかと思います。

### （Thymeleaf）importするパッケージ名を5から6に修正する
Thymeleafに独自のDiarectを導入している場合など、import文のパッケージ名が変更されません。これも「5」を「6」に修正する必要があります。

### （Data JPA/Hibernate）`@Query`のJPQLの型チェックが厳密になった
これが一番厄介かもしれません。
今までは数字は対応する型に自動で変換されていましたが、エンティティのフィールドに宣言されている型で記述されていないとエラーとなります。フィールドがLongであれば、数字の後ろに"l"を付ける必要があります。
このほか、Booleanは"0"または"1"では不可ですし、文字列の"0"とIntegerの0は比較できません。
ひとつづつ、虱潰しに修正していきましょう。

## 最後に
非推奨となっているメソッドやアノテーションを対応する新しいものに変換してくれるだけでも、相当量の修正を減らすことが出来ます。
Hibernateの破壊的アップデートだけはいただけませんね。