Spring Boot 2.6.6 から 3.1.2 へのバージョンアップ対応

# 背景
BIPROGY では Spring Boot ベースのサンプルアプリケーション「Dressca」を作成し、様々な案件で雛形として使っています。 
この Dressca は Spring Boot のバージョン 2.6 系で作られていたのですが、サポート期限が 2022/11/24 、有償サポートも 2024/02/24 となっていることから、 3.1 系へとバージョンアップする必要がありました。
最近の OSS は破壊的な変更は少なくなりつつあるものの、 Spring Boot 2 系から 3 系へのメジャーバージョンアップであることや Jakarta EE9 への対応などがあるため、それなりの非互換が予想されました。
本記事は、バージョンアップ作業で（予想通り）様々な非互換、非推奨に直面し、試行錯誤しながら対応した記録です。
これから Spring Boot 3 系にバージョンアップしようとしている誰かの支えになれると幸いです。

# バージョンアップの詳細
Spring Bootのバージョン 2.6 系から 3.1 系へとバージョンアップすることで、 Java や Gradle　のバージョンアップも必要となります。それぞれ以下のようにバージョンアップしました。

- Spring Boot : 2.6.6 → 3.1.2
- Java : 11 → 17
- Gradle : 7.4.1 → 8.2.1

# バージョンアップの流れ
Spring Boot のバージョンアップは build.gradle を書き換えて、コマンドによってバージョンアップすることもできますが、今回はSpring Boot 3.1.2 のプロジェクトを新規に作成し、ソースコードを移行する方式をとることにしました。

Dressca はマルチプロジェクトです。Web 、バッチ、コア機能、共通機能などのサブプロジェクトに分かれています。そのためルートプロジェクト、サブプロジェクトの雛形をそれぞれ作成し、組み合わせることでマルチプロジェクトの雛形としています。雛形作成から設定、ソースコードの移行までは簡単に手順を紹介しつつ、エラー解消について詳しく記載していきます。

バージョンアップまでの作業の流れは以下の通りです。

- サブプロジェクトの雛形作成
- ルートプロジェクトの雛形作成
- ルートプロジェクトの設定移行
- サブプロジェクトの設定移行
- サブプロジェクトのソースコード移行
- ビルドエラーの解消
- AP 起動時のエラー解消
- 打鍵テストによる不具合の確認と解消

## サブプロジェクトの雛形作成
雛形作成には [Spring Initializr](https://start.spring.io/) を利用しました。
Spring Initializr は言語や Spring Boot のバージョン等、プロジェクトの必要事項を設定し GENERATE ボタンをクリックすることで、プロジェクトの雛形を作成してくれます。今回は以下のように設定しました。

- Project：Gradle - Groovy
- Language : Java
- Spring Boot：3.1.2
- Project Metadata
    - Packaging : JDK
    - Java : 17
    - Dependencies : 各サブプロジェクトで利用している Spring ライブラリを選択
    - 上記以外の項目 : Package name 等は現行のプロジェクトの設定値を使用

以下はバッチ系のサブプロジェクトの雛形を作成した際の設定イメージとなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743777/83133079-9f45-df74-fe59-1c7adff11b9d.png)

## ルートプロジェクトの雛形作成
ルートプロジェクトについて、 Gradle の init タスクにより、basic なプロジェクトの雛形を作成しました。使用する Gradle のバージョンは先に作成したサブプロジェクトの Gradle のバージョンに合わせます。
※Gradle のバージョンはサブプロジェクト内にある gradle-wrapper.properties というファイル（<サブプロジェクト名>\gradle\wrapper\gradle-wrapper.properties）で確認することができます。

今回は以下のようになっていたため、Gradle 8.2.1 をダウンロードしました。
`distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip`

ダウンロード後はローカルに解凍し、 Java 17 の環境で gradle init コマンドを実行して、 basic なプロジェクトを作成します。途中でプロジェクトについての選択肢が出てきますが、 Spring Initializr でプロジェクトを作成したときの設定値を参考とすればあまり悩むことはないと思います。（Gradle のバージョンによって選択肢等が変わるので、実施時のキャプチャの掲載は割愛します）

## ルートプロジェクトの設定移行
以下ファイルについて、現行のファイルに合わせて修正したり、存在しないファイルをコピーします。
- settings.gradle

現行のファイルと比較し、差分があれば反映します。
マルチプロジェクトのため、現行と同様、末尾に以下のようにサブプロジェクトのinclude設定を追加しました。
`include '<サブプロジェクト名1>', '<サブプロジェクト名2>', '<サブプロジェクト名3>'`

- build.gradle

現行のファイルと比較し、差分があれば反映します。
主に不足しているライブラリに関する記述を転記します。各ライブラリのバージョンは [Maven Repository](https://mvnrepository.com/) を見て出来る限り最新化します。

- gradle.properties

現行に存在する場合、移行します。
※中身は Gradle の Java 起動オプションの設定などのため、基本的には修正は不要です。

- その他

checkstyle に関する設定ファイルやアプリで使用する画像などの静的なコンテンツを現行からコピーしました。
この辺りは個々のプロジェクト次第で変わるため、詳細は割愛します。

## サブプロジェクトの設定移行
ルートプロジェクトに続いてサブプロジェクトの設定を移行しました。
サブプロジェクトの移行作業開始前にサブプロジェクトの雛形（Spring Initializr からダウンロードした zip ファイルを解凍）をルートプロジェクト直下に配置しています。

- settings.gradle

現行のファイルと比較し、差分があれば反映します。

- build.gradle

現行のファイルと比較し、差分があれば反映します。
ルートプロジェクトと同様、主に不足しているライブラリに関する記述を転記します。各ライブラリのバージョンは  [Maven Repository](https://mvnrepository.com/) を見て出来る限り最新化します。

## サブプロジェクトのソースコード移行
現行の各サブプロジェクトの src ディレクトリの内容を 生成された雛形にコピーするだけです。
注意点として、雛形には java と test に 1 つずつデフォルトの java ファイルが生成されているので、削除してからコピーしてください。

## ビルドエラーの解消
VS Code などの開発用のエディタや IDE を用いている場合、この時点でエラーや非推奨が出ているかと思います。またそれらを解消しても、ビルドを実行してみるとエラーとなることがあります。それぞれ調査し、解消していきます。

- javax から jakarta への変更

Java のバージョンを 11 から 17 にバージョンアップしたことで、 Java EE から Jakarta EE9 へ変更にされています。そのため以下のようにパッケージ名を javax から jakarta に変更する必要があります。
※すべての javax が jakarta になったわけではないため注意が必要です。javax.sql.dataSource など変更されていないものがあるため、エディタ上でエラーとなっている import のみを修正するようにします。
```java
// 変更前
import javax.XXX;
```
```java
// 変更後
import jakarta.XXX;
```

- FlatFileItemWriter の setResource メソッドのエラー解消

Spring Boot のバージョンアップによる影響でエラーが発生しました。調べてみると Spring Batch 周りの非互換のようです。
ファイル出力で利用している FlatFileItemWriter クラスの setResource メソッドの引数の型が Resource インタフェースから WritableResource インタフェースに変更されたことが原因でした。
setResource メソッドの引数としている変数の型を WritableResource インタフェースの実装クラスである FileSystemResource に変更することで解消することができます。

```java
// 変更前
FlatFileItemWriter<CatalogItem> writer = new FlatFileItemWriter<>();
Resource outputResource;
outputResource = new FileSystemResource("<出力ファイルのパス>");
writer.setResource(outputResource);
```
```java
// 変更後
FlatFileItemWriter<CatalogItem> writer = new FlatFileItemWriter<>();
FileSystemResource outputResource;
outputResource = new FileSystemResource("<出力ファイルのパス>");
writer.setResource(outputResource);
```

- FlatFileItemWriter の write メソッドのエラー解消

Spring Boot のバージョンアップによる影響でエラーが発生しました。先ほどと同様 FlatFileItemWriter クラスの非互換なので Spring Batch が原因です。
ファイル出力で利用している FlatFileItemWriter クラスの write メソッドが write(java.util.List\<? extends T> items) から write(Chunk\<? extends T> items) に変更されたことが原因でした。
リストを Chunk 型のオブジェクトに変換することで解消することができます。

```java
// 変更前
List<CatalogItem> itemList = new ArrayList<>();
... 中略 ...
writer.write(itemList);
```
```java
// 変更後
List<CatalogItem> itemList = new ArrayList<>();
... 中略 ...
writer.write(new Chunk<>(itemList));
```

- JobBuilderFactory 、 StepBuilderFactory の非推奨の対応

Spring Boot のバージョンアップによる影響で非推奨が発生しました。こちらも Spring Batch 周りの非互換のようです。
JobBuilderFactory, StepBuilderFactory が非推奨になりました。代わりに JobBuilder, StepBuilder を利用するように修正することでエラーを解消しました。
修正の際には[こちらの記事](https://zenn.dev/red_frasco/articles/637280c126d959)を参考にさせていただきました。
当初非推奨ということで対応の優先度を下げていたのですが、ビルドしてみると JUnit でエラーが発生してしまったので、対応は必須となります。
Dressca のコードは以下のように修正しています。

```java
// 変更前
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
... 中略 ...
  public Step catalogItem_tasklet_step1(CatalogItemTasklet catalogItemTasklet) {
    return stepBuilderFactory.get("catalogItem_tasklet_step1").tasklet(catalogItemTasklet).build();
  }

  public Job catalogItem_tasklet_job(@Qualifier("catalogItem_tasklet_step1") Step step1) {
    return jobBuilderFactory.get("catalogItem_tasklet_job").incrementer(new RunIdIncrementer())
        .start(step1).build();
  }
 
  public Step catalogItem_step1(MyBatisPagingItemReader<CatalogItem> catalogItemReader,
      CatalogItemProcessor catalogItemProcessor,  FlatFileItemWriter<CatalogItem> catalogItemWriter) {
    return stepBuilderFactory.get("catalogItem_step1").<CatalogItem, CatalogItem>chunk(2)
        .reader(catalogItemReader)
        .processor(catalogItemProcessor).writer(catalogItemWriter)
        .build();
  }

  public Job catalogItem_job(JobCompletionNotificationListener listener,
      @Qualifier("catalogItem_step1") Step step1) {
    return jobBuilderFactory.get("catalogItem_job").incrementer(new RunIdIncrementer())
        .listener(listener).flow(step1).end().build();
  }
```
```java
// 変更後
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.transaction.PlatformTransactionManager;
... 中略 ...
  public Step catalogItem_tasklet_step1(JobRepository jobRepository, PlatformTransactionManager transactionManager, CatalogItemTasklet catalogItemTasklet) {
    return new StepBuilder("catalogItem_tasklet_step1", jobRepository).tasklet(catalogItemTasklet, transactionManager).build();
  }

  public Job catalogItem_tasklet_job(JobRepository jobRepository, @Qualifier("catalogItem_tasklet_step1") Step step1) {
    return new JobBuilder("catalogItem_tasklet_job", jobRepository).incrementer(new RunIdIncrementer())
        .start(step1).build();
  }

  public Step catalogItem_step1(JobRepository jobRepository, PlatformTransactionManager transactionManager,
      MyBatisPagingItemReader<CatalogItem> catalogItemReader,
      CatalogItemProcessor catalogItemProcessor,
      FlatFileItemWriter<CatalogItem> catalogItemWriter) {
    return new StepBuilder("catalogItem_step1", jobRepository).<CatalogItem, CatalogItem>chunk(2, transactionManager)
        .reader(catalogItemReader)
        .processor(catalogItemProcessor).writer(catalogItemWriter)
        .build();
  }

  public Job catalogItem_job(JobCompletionNotificationListener listener,
      JobRepository jobRepository, @Qualifier("catalogItem_step1") Step step1) {
    return new JobBuilder("catalogItem_job", jobRepository).incrementer(new RunIdIncrementer())
        .listener(listener).flow(step1).end().build();
  }
```

- JobExecutionListenerSupport の非推奨の対応

Spring Boot のバージョンアップによる影響で非推奨が発生しました。これも Spring Batch 周りの非互換のようです。
JobExecutionListenerSupport を継承していたクラスで以下非推奨のメッセージが出ました。
`Deprecated as of 5.0, in favor of the default methods on the JobExecutionListener`
メッセージにある通り、JobExecutionListener を使用することで対応します。具体的には JobExecutionListenerSupport の extends から JobExecutionListener の implements に変更することで非推奨を解消しました。

```java
// 変更前
import org.springframework.batch.core.listener.JobExecutionListenerSupport;
... 中略 ...
public class JobCompletionNotificationListener extends JobExecutionListenerSupport {
```
```java
// 変更後
import org.springframework.batch.core.JobExecutionListener;
... 中略 ...
public class JobCompletionNotificationListener implements JobExecutionListener {
```

- CheckStyle の設定最新化

CheckStyle 周りの設定にてエラーが発生し、ビルドに失敗しました。
Gradle stacktrace などで詳細なエラーを出力し、調査してみると CheckStyle のバージョンアップに伴い、廃止されたプロパティなどが結構あるようです。プロパティについては [CheckStyle の公式ページ](https://checkstyle.sourceforge.io/)に説明があり、また変更点についてリリースノートに記載があるため、公式のページを確認しながら移行を進めることができますが、 [google_checks.xml](https://github.com/checkstyle/checkstyle/blob/master/src/main/resources/google_checks.xml) や [sun_checks.xml](https://github.com/checkstyle/checkstyle/blob/master/src/main/resources/sun_checks.xml) を参考にするという方法もあります。
今回の修正では LineLength モジュールを記載する箇所の修正と、 JavadocMethod モジュールのプロパティの変更に対応することで、 CheckStyle のエラーを解消しました。

- openapi-gradle-plugin のバージョンアップによるエラーの対応

ビルド時、 Web 用サブプロジェクトの generateOpenApiDocs タスクで以下のようなエラーが発生しました。
generateOpenApiDocs タスクはビルド処理の中で AP サーバーを一時的に立ち上げ、 AP で定義した API のドキュメントを json 形式でダウンロードするためのタスクです。

```
Reason: Task ':web:forkedSpringBootRun' uses this output of task ':web:compileTestJava' without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed.
Possible solutions:
  1. Declare task ':web:compileTestJava' as an input of ':web:forkedSpringBootRun'.
  2. Declare an explicit dependency on ':web:compileTestJava' from ':web:forkedSpringBootRun' using Task#dependsOn. 
  3. Declare an explicit dependency on ':web:compileTestJava' from ':web:forkedSpringBootRun' using Task#mustRunAfter.
... 複数のタスクで同様のエラーが発生するため以下略 ...
```

springdoc-openapi-gradle-plugin を 1.4.0 から 1.7.0 に変更したのですが、以下にある通り、 1.6.0 のバグと類似しています。
[springdoc-openapi-gradle-plugin 1.6.0 の回避策について](https://github.com/springdoc/springdoc-openapi-gradle-plugin/issues/102)

上記に記載されている 1.6.0 の回避策にてエラーを無視して次のタスクに進むことはできたのですが、 1.7.0 ではバグ解消済みのはずなので、何とかならないか試行錯誤することに。
結果として、以下のように修正することでエラーが発生しなくなりました。
※ workingDir の指定は [1.7.0 の仕様](https://github.com/springdoc/springdoc-openapi-gradle-plugin/pull/103)に記載があり、追記しました。これを設定することで、 1.6.0 と類似したエラーが出なくなりました。代わりに workingDir で設定した $rootDir が存在しない、というエラーが発生するように。色々試してみたところ、openApi タスクにて出力する api-specification.json の出力先を root 直下（\\$rootDir）や build 直下（\\$buildDir） としている場合、出力先のディレクトリが見つからない、というエラーが発生してしまうようです。今回は元々 api-specification.json を root 直下に出力していましたが、 root 直下に出力用のディレクトリ「api-docs」を作成し、そこに出力することとしました。


```gradle
// 変更前
openApi {
    apiDocsUrl.set("http://localhost:8080/api-docs")
	outputDir.set(file("$rootDir"))
    outputFileName.set("api-specification.json")
}
```
```gradle
// 変更後
afterEvaluate {
    tasks.named("forkedSpringBootRun") {
		workingDir("$rootDir/api-docs")
	}
}
openApi {
    apiDocsUrl.set("http://localhost:8080/api-docs")
    outputDir.set(file("$rootDir/api-docs"))
    outputFileName.set("api-specification.json")
}
```

- バッチ用のサブプロジェクトの JUnit が失敗する問題の対応

Spring Boot が 2 系から 3 系に変わったことで、 Spring Batch のバージョンも 4 系から 5 系に変わっています。Spring Boot の 5 系からバッチのメタデータテーブルに大きな変更があり、ローカル動作環境の DB のテーブルを新しくする必要がありました。
[Spring Boot の公式ページ](https://docs.spring.io/spring-batch/docs/5.0.0/reference/html/schema-appendix.html)にメタデータテーブルの ddl が紹介されているため、それをローカル動作環境の DB に適用することで解消することができます。

DB のテーブルを新しくしたところ、ビルド時の JUnit が正常に終了するようになり、ようやくビルドが成功するようになりました。

## AP 起動時のエラー解消

幸いにも AP 起動時にエラーは発生しませんでした。
続いてサンプルアプリの動作確認をしました。

## 打鍵テストによる不具合の確認と解消

サンプルアプリの打鍵テストを行い、動作確認をしました。
打鍵テストシナリオがない場合は、主要な機能などを色々操作して確認することになるかと思いますが、 OSS の場合、パッケージ入替による動作確認などをする機会が増えると思いますので、誰がテストしても同じ動作確認ができるよう、テストシナリオを用意しておくことがおすすめです。
主要な画面、機能を一通りテストしましたが、幸い問題は発生しませんでした。
アプリも問題なく動作しましたので、これで Spring Boot のバージョンアップ対応は完了となります。

# Spring Boot のバージョンアップ対応のまとめ

Java EE から Jakarta EE9 への変更に伴うパッケージ名の変更は想定していましたが、想像していた以上に Spring Boot のバージョンアップによる非互換対応が多かったです（ほとんど Spring Batch 周りでした）。
また OpenApi の対応には少々苦戦しました。OSS の場合、リリースノートなどに非互換の情報が載っておらず試行錯誤するケースも多いので、注意が必要です。

Spring Boot のバージョンアップ作業で発生した非互換、非推奨対応について紹介しました。バージョンアップを検討させている方の参考となれば幸いです。