はじめに
Java Advent Calendar 2023 25日目の記事です。
- 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
公式サイトにガイドラインがあります。主だった変更点があるので、自分たちであてはまるものを確認していきます。
miragtion toolがいくつか紹介されています。gradle projectだったので、openrewriteを選択しました。これで必要な修正の大半ができました。
springだけでなく、Java17の対応も含まれているので、とても便利でした。
Trailing slash matchingの非推奨
URLの末尾にスラッシュのありなしの両方がマッチしていましたが、この設定がdeprecatedになり、デフォルトではスラッシュありはマッチしなくなりました。
スラッシュなしに統一すればいいのですが、時間がかかりそうなので、別途対応する方針をとりました。
そのために、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
こちらを参考に修正しました。
sessionへの保存
SecurityContextRepositoryを使うように修正しました。
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;
}
クラス設計が適切ならあまり起きないですが、あった場合は、循環参照にならないように、クラス設計や依存関係を見直しが必要です。
implicit constructor injection
injectionのやり方ですが、 プログラミングのベストプラクティスの観点から、field injectionより、constructor injectionの方が、推奨されています。
@Service
class SampleService {
@Autowired
private SampleRepository repository;
}
@Service
class SampleService {
private final SampleRepository repository;
@Autowired
public SampleService(SampleRepository repository) {
this.repository = repository;
}
}
implicit constructor injectionは、コンストラクタに@Autowired
のアノテーションが不要になるものです。できるようになったのは、2016年です。
@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系まであげることにしました。
--warning-mode all
をいれることで、deprecatedなものをwarningログとして表示できるようになります。
plugin DSL
gradle pluginの書き方の変更です。
apply xxx
から
plugins {
id xxx
}
となります。
failOnVersionConflict
ちゃんとメンテナンスされているライブラリを使っていればあまり起きないですが、依存ライブラリで、バージョンの競合が起きることもあります。それを検知するために、あった場合はビルドエラーにする設定を入れました。
configurations {
all {
resolutionStrategy {
failOnVersionConflict()
}
}
}
JUnit plugin
junit-platform-gradle-pluginというgradle pluginが、JUnitの実行やレポート作成に使われていましたが、2018年で開発が止まっていました。
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
使用していたライブラリは、原則、その時点の最新にあげました。
作業漏れがないかを確認しやすくするために、依存ライブラリの使用しているバージョンと、最新かどうかをチェックするプラグインを追加しました。
gralde lint plugin
余計なライブラリをbuild.gradleに書いていないかを調べるために、lintのプラグインを導入しました。完全に検知できるわけではないですが、ひとつの判断基準として有効でした。
thymeleaf
unwrapped fragment expression
別のthymeleafテンプレートの呼び出しの記法が変わりました。
common :: header
-> ~{common :: header}
deprecatedなった書き方をしていると、warningログがでます。
今回は、該当箇所が多かったので、この部分の書き換えは別タスクとして切り出しました。
Java
distribution
JDKのdistributionの選択肢自体も増え、リブランディングなどで名前が変わることもあり、近年では情勢の変化が激しいです。
以前はoraclejdkかopenjdkを選ぶのが無難でしたが、
- openjdkからdocker imageの配布停止
- サポートを明言しているプラットフォームが最小限
と、状況が変わっています。
Eclipse Temurinが、様々なプラットフォームと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()
参考