🌸

OpenRewriteを使って SpringBoot Upgrade 2.7 to 3.2

2023/12/27に公開

まえがき

ようやく上げれるタイミングが来ました🎉
2023/12/21 現在、3.2 でしたので、発生した事象に対応した内容を残しておきます。

今はOpenRewriteなる、アップグレードの自動化をしてくれるやつがいるんですね。
https://docs.openrewrite.org/running-recipes/popular-recipe-guides/migrate-to-spring-3

このツールと、マイグレーションガイドに沿ってやっていきます。
作業のお供には (Bing) Copilot 🤖くんが協力してくれてます!
※GitHub Copilot Chat くんは SpringBoot 3はまだリリースされていないって言い張るので。。

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

環境

WSL2 Ubuntu 22.04

$ java --version
openjdk 17.0.8 2023-07-18
OpenJDK Runtime Environment Temurin-17.0.8+7 (build 17.0.8+7)
OpenJDK 64-Bit Server VM Temurin-17.0.8+7 (build 17.0.8+7, mixed mode, sharing)

$ gradle -version

------------------------------------------------------------
Gradle 8.3
------------------------------------------------------------

Build time:   2023-08-17 07:06:47 UTC
Revision:     8afbf24b469158b714b36e84c6f4d4976c86fcd5

Kotlin:       1.9.0
Groovy:       3.0.17
Ant:          Apache Ant(TM) version 1.10.13 compiled on January 4 2023
JVM:          17.0.8 (Eclipse Adoptium 17.0.8+7)
OS:           Linux 5.15.133.1-microsoft-standard-WSL2 amd64

OpenRewrite を導入する

https://docs.openrewrite.org/running-recipes/popular-recipe-guides/migrate-to-spring-3

1. build.gradle に追記します。

build.gradle
plugins {
    id("org.openrewrite.rewrite") version("6.6.1")
}

rewrite {
    activeRecipe("org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_2")
}

repositories {
    mavenCentral()
}

dependencies {
    rewrite("org.openrewrite.recipe:rewrite-spring:5.1.6")
}

2. コマンド実行

gradle rewriteRun コマンドを実行します。

$ ./gradlew rewriteRun

実行には少し時間がかかります。
終わると、以下のような変更結果がコンソールに出力されます。

> Task :rewriteRun
Validating active recipes
Scanning sources in project :
Using active styles []
All sources parsed, running active recipes: org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_2
Changes have been made to src/main/java/jp/app/entity/Fruit.java by:
    org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_2
        org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_1
            org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_0
                org.openrewrite.java.migrate.jakarta.JavaxMigrationToJakarta
                    org.openrewrite.java.migrate.jakarta.JavaxPersistenceToJakartaPersistence
                        org.openrewrite.java.ChangePackage: {oldPackageName=javax.persistence, newPackageName=jakarta.persistence, recursive=true}

これは javax.persistence を jakarta.persistence にパッケージ変更した、という内容ですね。
全部見ていくとつらいので、レシピで対応したけど、エラーになったものを見ていきます。

OpenRewriteした後の対応

追加された org.glassfish.jaxb:jaxb-runtime の記述削除

build.gradle に org.glassfish.jaxb:jaxb-runtime が追加されました。
https://docs.openrewrite.org/recipes/java/migrate/javax/addjaxbruntime
JakartaEE8用の対応っぽいです。

が、gradle が ビルドエラーをずっと出し続けます。

Dependencies can not be declared against the testCompileClasspath configuration.

ちょっと胡散臭いので消すと、ビルドエラーが解消されました。
以下消した対象です。多い。

build.gradle
dependencies {
  productionRuntimeClasspath "org.glassfish.jaxb:jaxb-runtime:2.3.9"
  testCompileClasspath "org.glassfish.jaxb:jaxb-runtime:2.3.9"
  compileClasspath "org.glassfish.jaxb:jaxb-runtime:2.3.9"
  rewrite "org.glassfish.jaxb:jaxb-runtime:2.3.9"
  testRuntimeClasspath "org.glassfish.jaxb:jaxb-runtime:2.3.9"
  rewriteimplementation "org.glassfish.jaxb:jaxb-runtime:2.3.9"
  rewritetestImplementation "org.glassfish.jaxb:jaxb-runtime:2.3.9"
  runtimeClasspath "org.glassfish.jaxb:jaxb-runtime:4.0.4"
}

ちなみに、SpringBootの依存関係に含まれています。
2023/12/22 現在、jaxb-runtime:4.0.4 ですね。
https://spring.pleiades.io/spring-boot/docs/current/reference/html/dependency-versions.html

追加された jakarta系の依存関係の削除

jaxb-runtime みたいにエラーになるわけではないんですが、jakarta系の以下の依存も追加されます。バージョン固定して、移行しやすくするためかな?
こちらも余計なお世話なので、今回はさよならします。

build.gradle
  implementation "jakarta.persistence:jakarta.persistence-api:3.1.0"
  implementation "jakarta.transaction:jakarta.transaction-api:2.0.1"
  implementation "jakarta.validation:jakarta.validation-api:3.0.2"

起動時エラー 循環参照

[2023/12/22 12:41:28][W][restartedMain][AbstractApplicationContext:624] Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'dataSourceScriptDatabaseInitializer' defined in class path resource [org/springframework/boot/autoconfigure/sql/init/DataSourceInitializationConfiguration.class]: Unsatisfied dependency expressed through method 'dataSourceScriptDatabaseInitializer' parameter 0: Error creating bean with name 'dataSourceScriptDatabaseInitializer': Requested bean is currently in creation: Is there an unresolvable circular reference?
[2023/12/22 12:41:28][E][restartedMain][LoggingFailureAnalysisReporter:40] 

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌──->──┐
|  dataSourceScriptDatabaseInitializer defined in class path resource [org/springframework/boot/autoconfigure/sql/init/DataSourceInitializationConfiguration.class]
└──<-──┘


Action:

Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.

色々悪戦苦闘した結果、@DependsOnDatabaseInitialization をコメントアウトすることによって、ようやくエラーログが出ました。

    @Bean
    // @DependsOnDatabaseInitialization  <- OpenRewrite がつけたこのアノテーションをコメントアウト
    public HikariDataSource createPostgresDataSource() {
        return postgresDataSourceProperties().initializeDataSourceBuilder()
                .type(HikariDataSource.class).build();
    }
Suppressed: java.lang.ClassNotFoundException: org.hibernate.dialect.PostgreSQL95Dialect
                at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
                at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
                at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:525)
                at org.hibernate.boot.registry.classloading.internal.AggregatedClassLoader.findClass(AggregatedClassLoader.java:206)
                ... 52 common frames omitted

PostgreSQL95Dialect がなくなったっぽいです。
https://github.com/spring-projects/spring-framework/issues/30488

やってる環境だとJavaで書いていたので、以下のように変更します。

- properties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQL95Dialect");
+ properties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");

もしくは、PostgreSQLDialectがデフォルトで選択されるようなので、接続するPostgreSQLのバージョンが新しいものであれば、消して良いそうです。

[2023/12/22 19:44:25][W][restartedMain][DialectFactoryImpl:152] HHH90000025: PostgreSQLDialect does not need to be specified explicitly using 'hibernate.dialect' (remove the property setting and it will be selected by default)

jakarta.servlet.ServletException: Request processing failed: java.lang.IllegalArgumentException: Name for argument of type [java.lang.Integer] not specified, and parameter name information not found in class file either

@RequestParam("id") のnameはちゃんと指定しないといけなくなったみたいです。

- public ModelAndView edit(ModelAndView mv, @RequestParam Integer id) {
+ public ModelAndView edit(ModelAndView mv, @RequestParam("id") Integer id) {

@PathVariable も同様です。

- public ModelAndView update(ModelAndView mv, @PathVariable int id) {
+ public ModelAndView update(ModelAndView mv, @PathVariable("id") int id) {

以下のようなエラーも同様の対処方法になります。

[DirectJDKLog:175] Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.IllegalArgumentException: Name for argument of type [java.lang.Integer] not specified, and parameter name information not available via reflection. Ensure that the compiler uses the '-parameters' flag.] with root cause
java.lang.IllegalArgumentException: Name for argument of type [java.lang.Integer] not specified, and parameter name information not available via reflection. Ensure that the compiler uses the '-parameters' flag.

The method getStatusCodeValue() from the type ResponseEntity<String> is deprecated since version 6.0

responseEntity.getStatusCode().value() に置き換えましょう。

ResponseEntity<String> responseEntity = teamsRestTemplate.exchange(requestEntity, String.class);
- log.info("status: %d, message: %s".formatted(responseEntity.getStatusCodeValue(), responseEntity.getBody()));
+ log.info("status: %d, message: %s".formatted(responseEntity.getStatusCode().value(), responseEntity.getBody()));

The type Base64Utils has been deprecated since version 6.0.5 and marked for removal

org.springframework.util.Base64Utils がなくなるようです。
java.util.Base64 に置き換えましょう

URLの末尾のスラッシュの扱いの変更

As of Spring Framework 6.0, the trailing slash matching configuration option has been deprecated and its default value set to false. This means that previously, the following controller would match both "GET /some/greeting" and "GET /some/greeting/":

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

これまでは value = {""} と記述していた場合は、末尾にスラッシュがあってもなくても良しなにしていたのを、厳格にしたようです。
スラッシュ付きにしたい場合は対応が必要です。

- @GetMapping(value = {""})
+ @GetMapping(value = {"/"})

SpringBoot 3.2.0 to 3.2.1 Update

ちょっと起動が遅くなったりして気になったので、調べていたところ、3.2.1で改善されるようなこと書いてたので、3.2.1に上げてみます。
https://github.com/spring-projects/spring-framework/issues/31098

java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed

3.2.0の時はエラーにはならなかったですが、3.2.1になってから厳しくなった?みたいです。
https://okuyan-techdiary.com/mysql-dbeaver-error/

ローカルの開発環境なので、allowPublicKeyRetrieval=true を接続プロパティに追加します。

Tomcat 9 -> 10 へ Upgrade

まあ当然なんですけど、ServletのSupportバージョンが変わってるのでそうですよね。
ここでは割愛します(というか、弊環境ではそこまで変更点はなかった。)
ちなみに、Jettyは Servlet 6 をまだサポートできてないので、Servlet5系にダウングレードが必要だそうです。

Jetty does not yet support Servlet 6.0. To use Jetty with Spring Boot 3.0, you will have to downgrade the Servlet API to 5.0. You can use the jakarta-servlet.version property to do so.

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

その他・気になった点

rewriteimplementation

OpenRewrite を実行したら、build.gradle に rewriteimplementation で jaxb-runtime が追加されました。

build.gradle
    rewriteimplementation "org.glassfish.jaxb:jaxb-runtime:2.3.9"
    rewritetestImplementation "org.glassfish.jaxb:jaxb-runtime:2.3.9"

見慣れないので、Copilotに聞いてみたところ、
🤖

rewriteImplementation は、Gradle ビルドスクリプトで使用される依存関係管理プラグインの一つです。このプラグインは、依存関係の解決を最適化するために、プロジェクトの依存関係を自動的に修正します。例えば、プロジェクトが使用しているライブラリのバージョンが古い場合、rewriteImplementation は自動的に最新バージョンに更新します。このプラグインは、ビルドスクリプトの plugins ブロックで有効化されます。詳細については、公式ドキュメントを参照してください

とまあ、自動でアップデート作業のサポートをしてくれるもののようです。
アップデートのコマンドが終わったら消しました。

あとがき

SecurityContext 周りも書き換えてくれる OpenRewrite 便利ですね。
ちょっと余計なところもありますが💦

私のプロジェクトで発生した内容だけ記載していますので、ゴリゴリなプロジェクトだとRestTemplateの中身が変わったり、actuator 周りの変更でいろいろあるかと思います。頑張ってください。

SpringBoot は 3.2.1 まで上げると、体感ですが少し挙動のもっさりが改善したように感じます。

それではみなさん頑張ってSpringBootのアップデートに対応していきましょう😇

参考

https://docs.openrewrite.org/running-recipes/popular-recipe-guides/migrate-to-spring-3
https://zenn.dev/cypher256/articles/b6e27b0556d012

Discussion