···11---
22date: 2025-12-12
33+lastmod: 2025-12-19
34language: fr
45title: Montée de version Spring Boot 4 avec OpenRewrite
56tags:
67 - java
78 - spring-boot
99+url: '/:year/:month/:day/:slug/v1'
1010+params:
1111+ updated: "v2.md"
812---
9131014Un des projets que je maintiens activement est [GitLab Classrooms](projects/gitlab-classrooms).
···11+---
22+date: 2025-12-12
33+lastmod: 2025-12-19
44+language: fr
55+title: Montée de version Spring Boot 4 avec OpenRewrite
66+tags:
77+ - java
88+ - spring-boot
99+params:
1010+ original: "v1.md"
1111+---
1212+1313+Un des projets que je maintiens activement est [GitLab Classrooms](projects/gitlab-classrooms).
1414+1515+Le code de ce projet est écrit en Spring Boot 3 et Java 25.
1616+Avec la sortie récente de Spring Boot 4, je voulais faire la montée de version de ce projet rapidement.
1717+1818+Pour ça, j'ai donc deux possibilités : soit je fais la montée de version à la main, soit j'utilise un outil pour le faire automatiquement.
1919+2020+J'en ai donc profité pour tester OpenRewrite.
2121+2222+<!--more-->
2323+2424+## OpenRewrite
2525+2626+[OpenRewrite](https://docs.openrewrite.org/) est un outil qui permet d'exécuter des opérations de refactoring sur du code Java.
2727+Il s'appuie sur un concept de recette (le terme utilisé est _recipes_), qui implémente des transformations sur le code.
2828+2929+J'avais découvert cet outil lors du [talk de Jérôme Tama à Devoxx France 2025](https://www.youtube.com/watch?v=aYHb7sLhsoQ).
3030+3131+## La recette Spring Boot 4
3232+3333+Une [recette de migration Spring Boot 4](https://docs.openrewrite.org/recipes/java/spring/boot4/upgradespringboot_4_0-community-edition) est disponible pour la version communautaire.
3434+3535+En parcourant le code de la recette sur [Github](https://github.com/openrewrite/rewrite-spring/blob/main/src/main/resources/META-INF/rewrite/spring-boot-40.yml), il semble que la recette fait une grande partie de ce qui est indiqué dans le guide de migration de Spring :
3636+3737+* la montée de version du pom `spring-boot-starter-parent`
3838+* les modifications liées aux changements de coordonnées de certains artifacts maven
3939+* la migration vers Spring Framework et Spring Security 7
4040+* la mise à jour des properties dépréciées
4141+* la mise à jour vers testcontainers 2
4242+* la migration vers les starters modulaires
4343+4444+> [!INFO]
4545+> La recette évolue régulièrement, donc peut-être qu'elle fait encore plus de choses au moment où vous lisez cet article.
4646+4747+La recette prend la forme d'un fichier YAML, et elle est accompagnée de code qui implémente les différentes transformations :
4848+4949+> Je ne rentre pas dans les détails du fonctionnement d'OpenRewrite, allez voir le talk de Jérôme Tama indiqué plus haut pour plus d'informations.
5050+5151+```yaml
5252+type: specs.openrewrite.org/v1beta/recipe
5353+name: org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_0
5454+displayName: Migrate to Spring Boot 4.0
5555+description: >-
5656+ Migrate applications to the latest Spring Boot 4.0 release. This recipe will modify an application's build files,
5757+ make changes to deprecated/preferred APIs.
5858+tags:
5959+ - spring
6060+ - boot
6161+recipeList:
6262+ - org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_5
6363+ - org.openrewrite.java.spring.framework.UpgradeSpringFramework_7_0
6464+ - org.openrewrite.java.spring.security7.UpgradeSpringSecurity_7_0
6565+ - org.openrewrite.java.spring.batch.SpringBatch5To6Migration
6666+ - org.openrewrite.java.spring.boot4.SpringBootProperties_4_0
6767+ - org.openrewrite.java.spring.boot4.ReplaceMockBeanAndSpyBean
6868+ - org.openrewrite.hibernate.MigrateToHibernate71
6969+ - org.openrewrite.java.testing.testcontainers.Testcontainers2Migration
7070+ - org.openrewrite.java.spring.boot4.MigrateToModularStarters
7171+ - org.openrewrite.java.dependencies.UpgradeDependencyVersion:
7272+ groupId: org.springframework.boot
7373+ artifactId: "*"
7474+ newVersion: 4.0.x
7575+ overrideManagedVersion: false
7676+ - org.openrewrite.java.dependencies.UpgradeDependencyVersion:
7777+ groupId: org.springframework.boot
7878+ artifactId: spring-boot-dependencies
7979+ newVersion: 4.0.x
8080+ overrideManagedVersion: true
8181+ - org.openrewrite.maven.UpgradePluginVersion:
8282+ groupId: org.springframework.boot
8383+ artifactId: spring-boot-maven-plugin
8484+ newVersion: 4.0.x
8585+ - org.openrewrite.maven.UpgradeParentVersion:
8686+ groupId: org.springframework.boot
8787+ artifactId: spring-boot-starter-parent
8888+ newVersion: 4.0.x
8989+ - org.openrewrite.gradle.plugins.UpgradePluginVersion:
9090+ pluginIdPattern: org.springframework.boot
9191+ newVersion: 4.0.x
9292+9393+ # Replace deprecated starters with their new names
9494+ # https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide#deprecated-starters
9595+ - org.openrewrite.java.dependencies.ChangeDependency:
9696+ oldGroupId: org.springframework.boot
9797+ oldArtifactId: spring-boot-starter-oauth2-authorization-server
9898+ newArtifactId: spring-boot-starter-security-oauth2-authorization-server
9999+ - org.openrewrite.java.dependencies.ChangeDependency:
100100+ oldGroupId: org.springframework.boot
101101+ oldArtifactId: spring-boot-starter-oauth2-client
102102+ newArtifactId: spring-boot-starter-security-oauth2-client
103103+ - org.openrewrite.java.dependencies.ChangeDependency:
104104+ oldGroupId: org.springframework.boot
105105+ oldArtifactId: spring-boot-starter-oauth2-resource-server
106106+ newArtifactId: spring-boot-starter-security-oauth2-resource-server
107107+ - org.openrewrite.java.dependencies.ChangeDependency:
108108+ oldGroupId: org.springframework.boot
109109+ oldArtifactId: spring-boot-starter-web
110110+ newArtifactId: spring-boot-starter-webmvc
111111+ - org.openrewrite.java.dependencies.ChangeDependency:
112112+ oldGroupId: org.springframework.boot
113113+ oldArtifactId: spring-boot-starter-web-services
114114+ newArtifactId: spring-boot-starter-webservices
115115+ # https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide#aop-starter-pom
116116+ - org.openrewrite.java.dependencies.RemoveDependency:
117117+ groupId: org.springframework.boot
118118+ artifactId: spring-boot-starter-aop
119119+ unlessUsing: org.aspectj.lang.annotation.*
120120+ - org.openrewrite.java.dependencies.ChangeDependency:
121121+ oldGroupId: org.springframework.boot
122122+ oldArtifactId: spring-boot-starter-aop
123123+ newArtifactId: spring-boot-starter-aspectj
124124+```
125125+126126+La doc d'OpenRewrite indique qu'on peut utiliser une simple commande _Maven_ pour effectuer la migration :
127127+128128+```shell
129129+mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
130130+ -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-spring:RELEASE \
131131+ -Drewrite.activeRecipes=org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_0 \
132132+ -Drewrite.exportDatatables=true
133133+```
134134+135135+> Plutôt pratique, car je n'aurai pas à modifier mon `pom.xml`, ni ajouter de fichier de configuration dans mon projet pour pouvoir effectuer cette migration en one-shot.
136136+137137+L'exécution de la commande prend quelques secondes et affiche les opérations effectuées (j'ai beaucoup nettoyé les logs pour que ce soit plus lisible) :
138138+139139+```shell
140140+[INFO] --- rewrite:6.25.0:run (default-cli) @ gitlab-classrooms ---
141141+[INFO] Using active recipe(s) [org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_0]
142142+[INFO] Using active styles(s) []
143143+[INFO] Validating active recipes...
144144+[INFO] Project [gitlab-classrooms] Resolving Poms...
145145+[INFO] Project [gitlab-classrooms] Parsing source files
146146+[INFO] Running recipe(s)...
147147+[INFO] Printing available datatables to: target/rewrite/datatables/2025-12-12_17-33-51-247
148148+149149+[WARNING] Changes have been made to pom.xml by:
150150+[WARNING] org.openrewrite.java.spring.boot4.MigrateToModularStarters
151151+[WARNING] org.openrewrite.maven.UpgradeParentVersion: {groupId=org.springframework.boot, artifactId=spring-boot-starter-parent, newVersion=4.0.x}
152152+[WARNING] org.openrewrite.java.testing.testcontainers.Testcontainers2Migration
153153+154154+[WARNING] Changes have been made to src/main/resources/application-local.properties by:
155155+[WARNING] org.openrewrite.text.FindAndReplace: {find=javax., replace=jakarta., filePattern=**/*.js;**/*.ts;**/*.properties}
156156+157157+[WARNING] Changes have been made to src/test/java/fr/univ_lille/gitlab/classrooms/adapters/jpa/PostgresqlJPAAdaptersTest.java by:
158158+[WARNING] org.openrewrite.java.testing.testcontainers.Testcontainers2Migration
159159+[WARNING] org.openrewrite.java.spring.boot4.MigrateToModularStarters
160160+161161+[WARNING] Changes have been made to src/test/java/fr/univ_lille/gitlab/classrooms/mvc/ExportControllerMVCTest.java by:
162162+[WARNING] org.openrewrite.java.spring.boot4.ReplaceMockBeanAndSpyBean
163163+[WARNING] org.openrewrite.java.ChangeType: {oldFullyQualifiedTypeName=org.springframework.boot.test.mock.mockito.MockBean, newFullyQualifiedTypeName=org.springframework.test.context.bean.override.mockito.MockitoBean}
164164+165165+[...]
166166+167167+[WARNING] Please review and commit the results.
168168+[WARNING] Estimate time saved: 1h 16m
169169+[INFO] ------------------------------------------------------------------------
170170+[INFO] BUILD SUCCESS
171171+[INFO] ------------------------------------------------------------------------
172172+[INFO] Total time: 20.769 s
173173+[INFO] Finished at: 2025-12-12T17:33:51+01:00
174174+[INFO] ------------------------------------------------------------------------
175175+```
176176+177177+OpenRewrite semble s'être correctement exécuté et indique que plusieurs fichiers ont été modifiés. Un `git status` permet de se rendre compte de ce qui a été impacté :
178178+179179+```shell
180180+git status
181181+On branch feature/migration-spring-boot-4
182182+Changes not staged for commit:
183183+ (use "git add <file>..." to update what will be committed)
184184+ (use "git restore <file>..." to discard changes in working directory)
185185+ modified: pom.xml
186186+ modified: src/main/resources/application-local.properties
187187+ modified: src/test/java/fr/univ_lille/gitlab/classrooms/adapters/jpa/PostgresqlJPAAdaptersTest.java
188188+ modified: src/test/java/fr/univ_lille/gitlab/classrooms/api/UploadJunitGradingRestControllerMVCTest.java
189189+ modified: src/test/java/fr/univ_lille/gitlab/classrooms/assignments/AssignmentScoreServiceImplTest.java
190190+ modified: src/test/java/fr/univ_lille/gitlab/classrooms/assignments/AssignmentServiceImplTest.java
191191+ modified: src/test/java/fr/univ_lille/gitlab/classrooms/domain/classrooms/ClassroomStudentControllerTest.java
192192+ modified: src/test/java/fr/univ_lille/gitlab/classrooms/mvc/ClassroomControllerMVCTest.java
193193+ modified: src/test/java/fr/univ_lille/gitlab/classrooms/mvc/ExportControllerMVCTest.java
194194+ modified: src/test/java/fr/univ_lille/gitlab/classrooms/mvc/assignments/AssignmentMVCControllerMVCTest.java
195195+ modified: src/test/java/fr/univ_lille/gitlab/classrooms/mvc/assignments/StudentAssignmentResetGradeMVCControllerMVCTest.java
196196+ modified: src/test/java/fr/univ_lille/gitlab/classrooms/mvc/dashboard/DashboardControllerMVCTest.java
197197+ modified: src/test/java/fr/univ_lille/gitlab/classrooms/quiz/QuizAnswerControllerMVCTest.java
198198+ modified: src/test/java/fr/univ_lille/gitlab/classrooms/quiz/QuizEditionControllerMVCTest.java
199199+ modified: src/test/java/fr/univ_lille/gitlab/classrooms/quiz/QuizServiceImplTest.java
200200+201201+no changes added to commit (use "git add" and/or "git commit -a")
202202+```
203203+204204+* le `pom.xml` (ce qui était attendu en premier lieu)
205205+* les fichiers de configuration properties (certaines properties ont été renommées)
206206+* les fichiers de test (surtout pour la dépréciation de `@MockBean` et `@SpyBean`)
207207+208208+Un `git diff` permet de vérifier tout ça :
209209+210210+```shell
211211+git diff pom.xml
212212+213213+@@ -6,7 +6,7 @@
214214+ <parent>
215215+ <groupId>org.springframework.boot</groupId>
216216+ <artifactId>spring-boot-starter-parent</artifactId>
217217+- <version>3.5.6</version>
218218++ <version>4.0.0</version>
219219+ <relativePath/> <!-- lookup parent from repository -->
220220+ </parent>
221221+222222+@@ -44,7 +44,7 @@
223223+- <jacoco-maven-plugin.version>0.8.13</jacoco-maven-plugin.version>
224224++ <jacoco-maven-plugin.version>0.8.14</jacoco-maven-plugin.version>
225225+226226+@@ -57,7 +57,7 @@
227227+ <dependency>
228228+ <groupId>org.springframework.boot</groupId>
229229+- <artifactId>spring-boot-starter-web</artifactId>
230230++ <artifactId>spring-boot-starter-webmvc</artifactId>
231231+ </dependency>
232232+233233+@@ -67,12 +67,12 @@
234234+ <dependency>
235235+ <groupId>org.springframework.boot</groupId>
236236+- <artifactId>spring-boot-starter-oauth2-client</artifactId>
237237++ <artifactId>spring-boot-starter-security-oauth2-client</artifactId>
238238+ </dependency>
239239+240240+ <dependency>
241241+ <groupId>org.springframework.boot</groupId>
242242+- <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
243243++ <artifactId>spring-boot-starter-security-oauth2-resource-server</artifactId>
244244+ </dependency>
245245+246246+@@ -91,8 +91,8 @@
247247+ <dependency>
248248+- <groupId>org.flywaydb</groupId>
249249+- <artifactId>flyway-core</artifactId>
250250++ <groupId>org.springframework.boot</groupId>
251251++ <artifactId>spring-boot-starter-flyway</artifactId>
252252+ </dependency>
253253+254254+@@ -127,22 +127,32 @@
255255++ <dependency>
256256++ <groupId>org.springframework.boot</groupId>
257257++ <artifactId>spring-boot-starter-webmvc-test</artifactId>
258258++ <scope>test</scope>
259259++ </dependency>
260260++
261261++ <dependency>
262262++ <groupId>org.springframework.boot</groupId>
263263++ <artifactId>spring-boot-starter-data-jpa-test</artifactId>
264264++ <scope>test</scope>
265265++ </dependency>
266266+267267+@@ -136,7 +136,7 @@
268268+269269+ <dependency>
270270+ <groupId>org.testcontainers</groupId>
271271+- <artifactId>postgresql</artifactId>
272272++ <artifactId>testcontainers-postgresql</artifactId>
273273+ <scope>test</scope>
274274+ </dependency>
275275+276276+ <dependency>
277277+- <groupId>org.springframework.security</groupId>
278278+- <artifactId>spring-security-test</artifactId>
279279++ <groupId>org.springframework.boot</groupId>
280280++ <artifactId>spring-boot-starter-security-test</artifactId>
281281+ <scope>test</scope>
282282+ </dependency>
283283+```
284284+285285+Au niveau du `pom.xml`, tout s'est bien passé, l'ensemble des modifications attendues ont bien été appliquées.
286286+287287+La nouvelle architecture modulaire de Spring Boot 4 a bien été traitée.
288288+289289+Le code de tests a aussi bien été nettoyé des anciens `@MockBean` dépréciés :
290290+291291+```text
292292+@@ -37,10 +37,10 @@ class ClassroomControllerMVCTest {
293293+ @Autowired
294294+ private MockMvc mockMvc;
295295+296296+- @MockBean
297297++ @MockitoBean
298298+ private ClassroomService classroomService;
299299+300300+- @MockBean
301301++ @MockitoBean
302302+ private Gitlab gitlab;
303303+```
304304+305305+et certaines properties (qui étaient en commentaires) ont bien été renommées.
306306+307307+```text
308308+ # generate full creation sql script if needed
309309+-spring.jpa.properties.javax.persistence.schema-generation.create-source=metadata
310310+-spring.jpa.properties.javax.persistence.schema-generation.scripts.action=update
311311+-spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=update.sql
312312++spring.jpa.properties.jakarta.persistence.schema-generation.create-source=metadata
313313++spring.jpa.properties.jakarta.persistence.schema-generation.scripts.action=update
314314++spring.jpa.properties.jakarta.persistence.schema-generation.scripts.create-target=update.sql
315315+```
316316+317317+## Les ajustements que j'ai dû faire à la main
318318+319319+En sortie de cette exécution, mon code ne compile pas.
320320+321321+Un import dans ma configuration Spring Security n'est pas résolu
322322+323323+```java
324324+import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
325325+```
326326+327327+car cette classe a été déplacée dans un autre package :
328328+329329+```java
330330+import org.springframework.boot.security.autoconfigure.actuate.web.servlet.EndpointRequest;
331331+```
332332+333333+Une fois ces petits ajustements faits, je lance mes tests unitaires.
334334+335335+Cette fois-ci, j'obtiens un message d'erreur lié à Spring Security au démarrage :
336336+337337+```text
338338+Caused by: java.lang.IllegalArgumentException: pattern must start with a /
339339+```
340340+341341+Je n'avais pas fait attention à cette modification dans les guides de migration, donc je suis peut-être passé à travers. Quoi qu'il en soit, ce n'est pas une modification très compliquée, je l'ai facilement appliquée.
342342+343343+Une fois ces derniers ajustements faits, les tests passent correctement 🎉 :
344344+345345+
346346+347347+## Conclusion
348348+349349+Cela m'a pris environ 1h pour migrer mon projet de Spring Boot 3.5 vers Spring Boot 4.0.
350350+351351+OpenRewrite m'a clairement facilité le travail, il a modifié toutes mes dépendances, et migré les annotations dépréciées (ce qui aurait été fastidieux).
352352+J'ai quand même dû finaliser la migration à la main, et je n'ai pas pu me passer de la lecture du [Spring Boot 4.0 Migration Guide](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide).
353353+354354+Je pense que le support de Spring Boot 4 dans OpenRewrite n'en est qu'à ses débuts (la version qui introduit le support a été publiée le 5 décembre 2025, et la mise à jour pour l'architecture modulaire a été publiée le 16 décembre), donc il n'est pas impossible que les opérations que j'ai dû faire manuellement soient automatisées dans le futur.
355355+356356+Quoi qu'il en soit, 1h de travail pour migrer un projet d'environ 3 000 lignes de code, je pense que c'est plutôt efficace.