···11+---
22+date: 2025-12-12
33+lastmod: 2025-12-19
44+language: en
55+title: Spring Boot 4 Upgrade with OpenRewrite
66+tags:
77+ - java
88+ - spring-boot
99+params:
1010+ original: "v1.md"
1111+---
1212+1313+One of the projects I actively maintain is [GitLab Classrooms](/projects/gitlab-classrooms).
1414+1515+The code for this project is written in Spring Boot 3 and Java 25.
1616+With the recent release of Spring Boot 4, I wanted to upgrade this project quickly.
1717+1818+To do that, I have two possibilities: either I do the upgrade manually, or I use a tool to do it automatically.
1919+2020+I took the opportunity to test OpenRewrite.
2121+2222+<!--more-->
2323+2424+## OpenRewrite
2525+2626+[OpenRewrite](https://docs.openrewrite.org/) is a tool that allows performing refactoring operations on Java code.
2727+It relies on a concept of recipes, which implement transformations on the code.
2828+2929+I discovered this tool during [Jรฉrรดme Tama's talk at Devoxx France 2025](https://www.youtube.com/watch?v=aYHb7sLhsoQ).
3030+3131+## The Spring Boot 4 Recipe
3232+3333+A [Spring Boot 4 migration recipe](https://docs.openrewrite.org/recipes/java/spring/boot4/upgradespringboot_4_0-community-edition) is available for the community edition.
3434+3535+By browsing the recipe code on [Github](https://github.com/openrewrite/rewrite-spring/blob/main/src/main/resources/META-INF/rewrite/spring-boot-40.yml), it seems that the recipe does a large part of what is indicated in the Spring migration guide:
3636+3737+* upgrade of the `spring-boot-starter-parent` pom
3838+* modifications related to coordinate changes of certain maven artifacts
3939+* migration to Spring Framework and Spring Security 7
4040+* update of deprecated properties
4141+* update to testcontainers 2
4242+* migration to modular starters
4343+4444+> [!INFO]
4545+> The recipe evolves regularly, so maybe it does even more things by the time you read this article.
4646+4747+The recipe takes the form of a YAML file, and it is accompanied by code that implements the various transformations:
4848+4949+> I'm not going into the details of how OpenRewrite works, check out Jรฉrรดme Tama's talk mentioned above for more information.
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+The OpenRewrite documentation indicates that you can use a simple _Maven_ command to perform the 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+> Quite convenient, because I won't have to modify my `pom.xml`, nor add a configuration file to my project to be able to perform this one-shot migration.
136136+137137+Running the command takes a few seconds and displays the operations performed (I've cleaned up the logs a lot to make it more readable):
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 seems to have run correctly and indicates that several files have been modified. A `git status` allows to see what has been impacted:
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+* the `pom.xml` (which was expected first)
205205+* the properties configuration files (some properties were renamed)
206206+* the test files (mainly for the deprecation of `@MockBean` and `@SpyBean`)
207207+208208+A `git diff` allows to check all that:
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+At the `pom.xml` level, everything went well, all the expected modifications have been applied.
286286+287287+The new modular architecture of Spring Boot 4 was correctly handled.
288288+289289+The test code was also cleaned of the old deprecated `@MockBean`:
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+and some properties (which were in comments) were correctly renamed.
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+## The adjustments I had to do manually
318318+319319+After this execution, my code doesn't compile.
320320+321321+An import in my Spring Security configuration is not resolved
322322+323323+```java
324324+import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
325325+```
326326+327327+because this class was moved to another package:
328328+329329+```java
330330+import org.springframework.boot.security.autoconfigure.actuate.web.servlet.EndpointRequest;
331331+```
332332+333333+Once these small adjustments were made, I ran my unit tests.
334334+335335+This time, I got an error message related to Spring Security at startup:
336336+337337+```text
338338+Caused by: java.lang.IllegalArgumentException: pattern must start with a /
339339+```
340340+341341+I hadn't paid attention to this change in the migration guides, so I might have missed it. Regardless, it's not a very complicated change, I easily applied it.
342342+343343+Once these last adjustments were made, the tests pass correctly ๐:
344344+345345+
346346+347347+## Conclusion
348348+349349+It took me about 1 hour to migrate my project from Spring Boot 3.5 to Spring Boot 4.0.
350350+351351+OpenRewrite clearly made the work easier, it modified all my dependencies, and migrated deprecated annotations (which would have been tedious).
352352+I still had to finalize the migration manually, and I couldn't skip reading the [Spring Boot 4.0 Migration Guide](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide).
353353+354354+I think that Spring Boot 4 support in OpenRewrite is still in its early stages (the version introducing support was published on December 5, 2025, and the update for the modular architecture was published on December 16), so it's not impossible that the operations I had to do manually will be automated in the future.
355355+356356+Anyway, 1 hour of work to migrate a project of about 3,000 lines of code, I think it's quite efficient.