Spring boot 2.7 から 3.1 にアップグレードしたときのまとめ

# はじめに

[Java  Advent Calendar 2023 25日目](https://qiita.com/advent-calendar/2023/java)の記事です。

* spring boot: 2.7 -> 3.1
* Gradle: 7.5 -> 8.1.1
* Java: openjdk8 -> temurin-17-jdk

にアップグレードしたときの知見です。
プロジェクト自体は、spring boot 2.0 時点から開始で、過去のアップグレードでできていなかったものもかなりあり、今回はそれらの洗い出しも含めての対応になりました。


# 忙しい人へのまとめ

* 使用しているフレームワーク、ライブラリの公式サイトのmigration guideをチェック
* migrationツールがある場合は極力使う
* warningフラグなどの設定で、deprecatedを使用している箇所を見つけやすくする
* linterなどで、未使用のもの、バージョンが最新でないものを見つけやすくする
* EOLを確認する


# アップグレードのためのガイドライン作成

「動かすための最小限の変更だけをいれる」という方針をとると、別のやり方への移行が遅れたり、本来すべきではない場当たり的な対応をしがちです。
そうならないために、修正内容と影響範囲の洗い出しに時間をかけるようにしました。
また、spring bootアプリが複数あったため、複数人で作業できるように、アップグレードの詳細な手順のマニュアル化も行いました。

また、アップグレード後のバージョンも含め、EOLを確認しました。アップグレードをしても、次はいつまでやるというスケジュールをたてる必要が今後はあります。


# 主な修正内容

## spring boot

公式サイトにガイドラインがあります。主だった変更点があるので、自分たちであてはまるものを確認していきます。

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

### OpenRewrite
miragtion toolがいくつか紹介されています。gradle projectだったので、openrewriteを選択しました。これで必要な修正の大半ができました。
springだけでなく、Java17の対応も含まれているので、とても便利でした。

https://docs.openrewrite.org/recipes/java/spring/boot3/upgradespringboot_3_0

### Trailing slash matchingの非推奨

URLの末尾にスラッシュのありなしの両方がマッチしていましたが、この設定がdeprecatedになり、デフォルトではスラッシュありはマッチしなくなりました。

https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0-Migration-Guide#spring-mvc-and-webflux-url-matching-changes

スラッシュなしに統一すればいいのですが、時間がかかりそうなので、別途対応する方針をとりました。
そのために、deprecatedを承知で、trailing slash matchingの設定を追加しました。

### JPAのSpecificationの可変長引数

これまではin句の可変長引数にList(`List.of('a', 'b')`)を渡しても、`in ('a', 'b')` という風に分解されてましたが、upgrade後は分解されずに、リストのインスタンスIDが渡されるようになりました。
Javaの可変長引数の型はarrayなので、正しくなったといえます。

Collection型の引数のメソッドをoverloadで追加して対応しました。

### JPA entityクラスで、join用とそのままの二つのプロパティ

Entityクラスで、`@JoinColumn`で、Join先のEntityクラスのプロパティと、それなしのプリミティブな型のプロパティの二つを持っている箇所がいくつありました。JoinColumnがあると、specificationではそのentityで指定しないと正しくクエリが生成されなくなっていました。

プロパティは、JoinColumnの方を残し、カラムのそのままの値を返すgetterを追加することで調整しました。

### なんでも`@RequestMapping`

Controllerのメソッドで、GET,POST,DELETE,UPDATEのどれからのHTTPメソッドを指定すべきところが、`@RequestMapping`のみになっている箇所がそれなりにありました。このままだと、openrewriteが一律で`@GetMapping`に書き換えてしまいます。openrewrite実行前に、手動で修正して対応しました。

### spring security

#### SecurityFilterChain

こちらを参考に修正しました。

https://qiita.com/suke_masa/items/908805dd45df08ba28d8

#### sessionへの保存

SecurityContextRepositoryを使うように修正しました。

https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html#store-authentication-manually

### jakartaへの変更

javax -> jakarataへのパッケージ変更です。openrewriteで全部やってくれました。
注意するのは、IDEのimport文の並び替えの設定です。javaxと同じ順番になるようにjakartaを追加しないと、並び替えで、jakarataのものの位置が大きく変わってしまいます。

また、messages.propertiesで、bean validationのエラーメッセージをカスタムしていた場合もjavax -> jakarataの変更が必要です。

### spring beanの循環参照

class AとBが互いにinjectionしていると、依存関係を解決するときに循環参照となります。
2.6からはデフォルトでは循環参照があれば、アプリ起動時にエラーとなります。

```
@Component
class SampleA {

   @Autowired
   private SampleB b;

}

@Component
class SampleB {

  @Autowired
  private SampleA a;

}
```

https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.6-Release-Notes#circular-references-prohibited-by-default

https://github.com/spring-projects/spring-boot/issues/27652

クラス設計が適切ならあまり起きないですが、あった場合は、循環参照にならないように、クラス設計や依存関係を見直しが必要です。

### implicit constructor injection

injectionのやり方ですが、 プログラミングのベストプラクティスの観点から、field injectionより、constructor injectionの方が、推奨されています。

```:field injection
@Service
class SampleService {

  @Autowired
  private SampleRepository repository;
   
}
```

```: constructor injection
@Service
class SampleService {

  
  private final SampleRepository repository;

  @Autowired
  public SampleService(SampleRepository repository) {
       this.repository = repository; 
  }
   
}
```

implicit constructor injectionは、コンストラクタに`@Autowired`のアノテーションが不要になるものです。できるようになったのは、2016年です。

https://spring.io/blog/2016/03/04/core-container-refinements-in-spring-framework-4-3/

```
@Service
class SampleService {

  
  private final SampleRepository repository;

  public SampleService(SampleRepository repository) {
       this.repository = repository; 
  }
   
}
```

現在では、不要な`@Autowired`があると、openrewriteで書き換えられたり、IDEの設定でwarning出したりできます。


## gradle

spring boot 3.0 であれば、7.x系がシステム要件です。
一方、gralde側のライフサイクルでは、一つ前のメジャーバージョンはsecurity updateのみになるので、このタイミングで8.x系まであげることにしました。

### deprecatedの洗い出し

`--warning-mode all` をいれることで、deprecatedなものをwarningログとして表示できるようになります。
Javaのcompile optionに追加することで、compile時のwarningログにdeprecatedの箇所などが追加されます。

```
compileJava {
  options.CompilerArgs << '-Xlint'
}
```

標準エラーに出力されるので、特定のwarningログに絞りたい場合は、下記のようにします。
```
gradlew compileJava --warning-mode all |& grep deprecated
```

### plugin DSL

gradle pluginの書き方の変更です。

`apply xxx` から

```
plugins {
  id xxx
}
```

となります。

### failOnVersionConflict

ちゃんとメンテナンスされているライブラリを使っていればあまり起きないですが、依存ライブラリで、バージョンの競合が起きることもあります。それを検知するために、あった場合はビルドエラーにする設定を入れました。

```
configurations {
    all {
        resolutionStrategy {
           failOnVersionConflict()
        }
    }
}
```


### JUnit plugin

junit-platform-gradle-pluginというgradle pluginが、JUnitの実行やレポート作成に使われていましたが、2018年で開発が止まっていました。

https://junit.org/junit5/docs/5.3.0/release-notes/index.html

pluginの設定をはずし、テスト結果を標準出力に出す設定を追加しました。

```
test {
    useJUnitPlatform()
    testLogging {
        events "PASSED", "SKIPPED", "FAILED"
    }
    afterSuite { desc, result ->
        if (!desc.parent) {
            def output = "${result.resultType}: ${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped, ${(result.endTime - result.startTime)/1000} seconds"
            println(output)
        }
    }
}
```

### gradle version plugin

使用していたライブラリは、原則、その時点の最新にあげました。
作業漏れがないかを確認しやすくするために、依存ライブラリの使用しているバージョンと、最新かどうかをチェックするプラグインを追加しました。

https://github.com/ben-manes/gradle-versions-plugin

### gralde lint plugin

余計なライブラリをbuild.gradleに書いていないかを調べるために、lintのプラグインを導入しました。完全に検知できるわけではないですが、ひとつの判断基準として有効でした。

https://github.com/nebula-plugins/gradle-lint-plugin

## thymeleaf

https://www.thymeleaf.org/doc/articles/thymeleaf31whatsnew.html

### unwrapped fragment expression

別のthymeleafテンプレートの呼び出しの記法が変わりました。

`common :: header` -> `~{common :: header}`

deprecatedなった書き方をしていると、warningログがでます。
今回は、該当箇所が多かったので、この部分の書き換えは別タスクとして切り出しました。

## Java

### distribution

JDKのdistributionの選択肢自体も増え、リブランディングなどで名前が変わることもあり、近年では情勢の変化が激しいです。

https://qiita.com/yamadamn/items/2dd26a014791b9557199

以前はoraclejdkかopenjdkを選ぶのが無難でしたが、

* openjdkからdocker imageの配布停止
  * https://hub.docker.com/_/openjdk 
* サポートを明言しているプラットフォームが最小限
  * https://wiki.openjdk.org/display/Build/Supported+Build+Platforms 

と、状況が変わっています。

[Eclipse Temurin](https://adoptium.net/supported-platforms/)が、様々なプラットフォームとdocker imageのサポートを担うように、役割分担されたようにみえます。

コンテナベースの開発環境に移行できることも考慮して、Eclipse Temurinを採用することにしました。



### best practices

openrewriteで、既存コードをJava17のベストプラクティスに沿ったものへ書き換えられました。
これによって、同じ実装でも、バージョン違いによる異なる書き方の混在を回避できました。
主な変更点は下記でした。

* text format
* text block
* pattern matching of instanceof
* switch expression
* Factory method of collection
* Stream#toList()
* Optional#isEmpty()



# 参考

https://techblog.zozo.com/entry/springboot-version-up-to-3

https://zenn.dev/red_frasco/articles/637280c126d959

https://qiita.com/kazokmr/items/ae0b70cfbc06ec344c44

https://qiita.com/arithmath/items/415a9ca6b57c4e2ca9ec

https://endoflife.date/