Source code of my website
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

๐ŸŒ : add english translation

+356
+356
content/posts/2025-12-05-openrewrite-spring-boot-4/v2/index.en.md
··· 1 + --- 2 + date: 2025-12-12 3 + lastmod: 2025-12-19 4 + language: en 5 + title: Spring Boot 4 Upgrade with OpenRewrite 6 + tags: 7 + - java 8 + - spring-boot 9 + params: 10 + original: "v1.md" 11 + --- 12 + 13 + One of the projects I actively maintain is [GitLab Classrooms](/projects/gitlab-classrooms). 14 + 15 + The code for this project is written in Spring Boot 3 and Java 25. 16 + With the recent release of Spring Boot 4, I wanted to upgrade this project quickly. 17 + 18 + To do that, I have two possibilities: either I do the upgrade manually, or I use a tool to do it automatically. 19 + 20 + I took the opportunity to test OpenRewrite. 21 + 22 + <!--more--> 23 + 24 + ## OpenRewrite 25 + 26 + [OpenRewrite](https://docs.openrewrite.org/) is a tool that allows performing refactoring operations on Java code. 27 + It relies on a concept of recipes, which implement transformations on the code. 28 + 29 + I discovered this tool during [Jรฉrรดme Tama's talk at Devoxx France 2025](https://www.youtube.com/watch?v=aYHb7sLhsoQ). 30 + 31 + ## The Spring Boot 4 Recipe 32 + 33 + 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. 34 + 35 + 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: 36 + 37 + * upgrade of the `spring-boot-starter-parent` pom 38 + * modifications related to coordinate changes of certain maven artifacts 39 + * migration to Spring Framework and Spring Security 7 40 + * update of deprecated properties 41 + * update to testcontainers 2 42 + * migration to modular starters 43 + 44 + > [!INFO] 45 + > The recipe evolves regularly, so maybe it does even more things by the time you read this article. 46 + 47 + The recipe takes the form of a YAML file, and it is accompanied by code that implements the various transformations: 48 + 49 + > I'm not going into the details of how OpenRewrite works, check out Jรฉrรดme Tama's talk mentioned above for more information. 50 + 51 + ```yaml 52 + type: specs.openrewrite.org/v1beta/recipe 53 + name: org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_0 54 + displayName: Migrate to Spring Boot 4.0 55 + description: >- 56 + Migrate applications to the latest Spring Boot 4.0 release. This recipe will modify an application's build files, 57 + make changes to deprecated/preferred APIs. 58 + tags: 59 + - spring 60 + - boot 61 + recipeList: 62 + - org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_5 63 + - org.openrewrite.java.spring.framework.UpgradeSpringFramework_7_0 64 + - org.openrewrite.java.spring.security7.UpgradeSpringSecurity_7_0 65 + - org.openrewrite.java.spring.batch.SpringBatch5To6Migration 66 + - org.openrewrite.java.spring.boot4.SpringBootProperties_4_0 67 + - org.openrewrite.java.spring.boot4.ReplaceMockBeanAndSpyBean 68 + - org.openrewrite.hibernate.MigrateToHibernate71 69 + - org.openrewrite.java.testing.testcontainers.Testcontainers2Migration 70 + - org.openrewrite.java.spring.boot4.MigrateToModularStarters 71 + - org.openrewrite.java.dependencies.UpgradeDependencyVersion: 72 + groupId: org.springframework.boot 73 + artifactId: "*" 74 + newVersion: 4.0.x 75 + overrideManagedVersion: false 76 + - org.openrewrite.java.dependencies.UpgradeDependencyVersion: 77 + groupId: org.springframework.boot 78 + artifactId: spring-boot-dependencies 79 + newVersion: 4.0.x 80 + overrideManagedVersion: true 81 + - org.openrewrite.maven.UpgradePluginVersion: 82 + groupId: org.springframework.boot 83 + artifactId: spring-boot-maven-plugin 84 + newVersion: 4.0.x 85 + - org.openrewrite.maven.UpgradeParentVersion: 86 + groupId: org.springframework.boot 87 + artifactId: spring-boot-starter-parent 88 + newVersion: 4.0.x 89 + - org.openrewrite.gradle.plugins.UpgradePluginVersion: 90 + pluginIdPattern: org.springframework.boot 91 + newVersion: 4.0.x 92 + 93 + # Replace deprecated starters with their new names 94 + # https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide#deprecated-starters 95 + - org.openrewrite.java.dependencies.ChangeDependency: 96 + oldGroupId: org.springframework.boot 97 + oldArtifactId: spring-boot-starter-oauth2-authorization-server 98 + newArtifactId: spring-boot-starter-security-oauth2-authorization-server 99 + - org.openrewrite.java.dependencies.ChangeDependency: 100 + oldGroupId: org.springframework.boot 101 + oldArtifactId: spring-boot-starter-oauth2-client 102 + newArtifactId: spring-boot-starter-security-oauth2-client 103 + - org.openrewrite.java.dependencies.ChangeDependency: 104 + oldGroupId: org.springframework.boot 105 + oldArtifactId: spring-boot-starter-oauth2-resource-server 106 + newArtifactId: spring-boot-starter-security-oauth2-resource-server 107 + - org.openrewrite.java.dependencies.ChangeDependency: 108 + oldGroupId: org.springframework.boot 109 + oldArtifactId: spring-boot-starter-web 110 + newArtifactId: spring-boot-starter-webmvc 111 + - org.openrewrite.java.dependencies.ChangeDependency: 112 + oldGroupId: org.springframework.boot 113 + oldArtifactId: spring-boot-starter-web-services 114 + newArtifactId: spring-boot-starter-webservices 115 + # https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide#aop-starter-pom 116 + - org.openrewrite.java.dependencies.RemoveDependency: 117 + groupId: org.springframework.boot 118 + artifactId: spring-boot-starter-aop 119 + unlessUsing: org.aspectj.lang.annotation.* 120 + - org.openrewrite.java.dependencies.ChangeDependency: 121 + oldGroupId: org.springframework.boot 122 + oldArtifactId: spring-boot-starter-aop 123 + newArtifactId: spring-boot-starter-aspectj 124 + ``` 125 + 126 + The OpenRewrite documentation indicates that you can use a simple _Maven_ command to perform the migration: 127 + 128 + ```shell 129 + mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \ 130 + -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-spring:RELEASE \ 131 + -Drewrite.activeRecipes=org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_0 \ 132 + -Drewrite.exportDatatables=true 133 + ``` 134 + 135 + > 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. 136 + 137 + 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): 138 + 139 + ```shell 140 + [INFO] --- rewrite:6.25.0:run (default-cli) @ gitlab-classrooms --- 141 + [INFO] Using active recipe(s) [org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_0] 142 + [INFO] Using active styles(s) [] 143 + [INFO] Validating active recipes... 144 + [INFO] Project [gitlab-classrooms] Resolving Poms... 145 + [INFO] Project [gitlab-classrooms] Parsing source files 146 + [INFO] Running recipe(s)... 147 + [INFO] Printing available datatables to: target/rewrite/datatables/2025-12-12_17-33-51-247 148 + 149 + [WARNING] Changes have been made to pom.xml by: 150 + [WARNING] org.openrewrite.java.spring.boot4.MigrateToModularStarters 151 + [WARNING] org.openrewrite.maven.UpgradeParentVersion: {groupId=org.springframework.boot, artifactId=spring-boot-starter-parent, newVersion=4.0.x} 152 + [WARNING] org.openrewrite.java.testing.testcontainers.Testcontainers2Migration 153 + 154 + [WARNING] Changes have been made to src/main/resources/application-local.properties by: 155 + [WARNING] org.openrewrite.text.FindAndReplace: {find=javax., replace=jakarta., filePattern=**/*.js;**/*.ts;**/*.properties} 156 + 157 + [WARNING] Changes have been made to src/test/java/fr/univ_lille/gitlab/classrooms/adapters/jpa/PostgresqlJPAAdaptersTest.java by: 158 + [WARNING] org.openrewrite.java.testing.testcontainers.Testcontainers2Migration 159 + [WARNING] org.openrewrite.java.spring.boot4.MigrateToModularStarters 160 + 161 + [WARNING] Changes have been made to src/test/java/fr/univ_lille/gitlab/classrooms/mvc/ExportControllerMVCTest.java by: 162 + [WARNING] org.openrewrite.java.spring.boot4.ReplaceMockBeanAndSpyBean 163 + [WARNING] org.openrewrite.java.ChangeType: {oldFullyQualifiedTypeName=org.springframework.boot.test.mock.mockito.MockBean, newFullyQualifiedTypeName=org.springframework.test.context.bean.override.mockito.MockitoBean} 164 + 165 + [...] 166 + 167 + [WARNING] Please review and commit the results. 168 + [WARNING] Estimate time saved: 1h 16m 169 + [INFO] ------------------------------------------------------------------------ 170 + [INFO] BUILD SUCCESS 171 + [INFO] ------------------------------------------------------------------------ 172 + [INFO] Total time: 20.769 s 173 + [INFO] Finished at: 2025-12-12T17:33:51+01:00 174 + [INFO] ------------------------------------------------------------------------ 175 + ``` 176 + 177 + OpenRewrite seems to have run correctly and indicates that several files have been modified. A `git status` allows to see what has been impacted: 178 + 179 + ```shell 180 + git status 181 + On branch feature/migration-spring-boot-4 182 + Changes not staged for commit: 183 + (use "git add <file>..." to update what will be committed) 184 + (use "git restore <file>..." to discard changes in working directory) 185 + modified: pom.xml 186 + modified: src/main/resources/application-local.properties 187 + modified: src/test/java/fr/univ_lille/gitlab/classrooms/adapters/jpa/PostgresqlJPAAdaptersTest.java 188 + modified: src/test/java/fr/univ_lille/gitlab/classrooms/api/UploadJunitGradingRestControllerMVCTest.java 189 + modified: src/test/java/fr/univ_lille/gitlab/classrooms/assignments/AssignmentScoreServiceImplTest.java 190 + modified: src/test/java/fr/univ_lille/gitlab/classrooms/assignments/AssignmentServiceImplTest.java 191 + modified: src/test/java/fr/univ_lille/gitlab/classrooms/domain/classrooms/ClassroomStudentControllerTest.java 192 + modified: src/test/java/fr/univ_lille/gitlab/classrooms/mvc/ClassroomControllerMVCTest.java 193 + modified: src/test/java/fr/univ_lille/gitlab/classrooms/mvc/ExportControllerMVCTest.java 194 + modified: src/test/java/fr/univ_lille/gitlab/classrooms/mvc/assignments/AssignmentMVCControllerMVCTest.java 195 + modified: src/test/java/fr/univ_lille/gitlab/classrooms/mvc/assignments/StudentAssignmentResetGradeMVCControllerMVCTest.java 196 + modified: src/test/java/fr/univ_lille/gitlab/classrooms/mvc/dashboard/DashboardControllerMVCTest.java 197 + modified: src/test/java/fr/univ_lille/gitlab/classrooms/quiz/QuizAnswerControllerMVCTest.java 198 + modified: src/test/java/fr/univ_lille/gitlab/classrooms/quiz/QuizEditionControllerMVCTest.java 199 + modified: src/test/java/fr/univ_lille/gitlab/classrooms/quiz/QuizServiceImplTest.java 200 + 201 + no changes added to commit (use "git add" and/or "git commit -a") 202 + ``` 203 + 204 + * the `pom.xml` (which was expected first) 205 + * the properties configuration files (some properties were renamed) 206 + * the test files (mainly for the deprecation of `@MockBean` and `@SpyBean`) 207 + 208 + A `git diff` allows to check all that: 209 + 210 + ```shell 211 + git diff pom.xml 212 + 213 + @@ -6,7 +6,7 @@ 214 + <parent> 215 + <groupId>org.springframework.boot</groupId> 216 + <artifactId>spring-boot-starter-parent</artifactId> 217 + - <version>3.5.6</version> 218 + + <version>4.0.0</version> 219 + <relativePath/> <!-- lookup parent from repository --> 220 + </parent> 221 + 222 + @@ -44,7 +44,7 @@ 223 + - <jacoco-maven-plugin.version>0.8.13</jacoco-maven-plugin.version> 224 + + <jacoco-maven-plugin.version>0.8.14</jacoco-maven-plugin.version> 225 + 226 + @@ -57,7 +57,7 @@ 227 + <dependency> 228 + <groupId>org.springframework.boot</groupId> 229 + - <artifactId>spring-boot-starter-web</artifactId> 230 + + <artifactId>spring-boot-starter-webmvc</artifactId> 231 + </dependency> 232 + 233 + @@ -67,12 +67,12 @@ 234 + <dependency> 235 + <groupId>org.springframework.boot</groupId> 236 + - <artifactId>spring-boot-starter-oauth2-client</artifactId> 237 + + <artifactId>spring-boot-starter-security-oauth2-client</artifactId> 238 + </dependency> 239 + 240 + <dependency> 241 + <groupId>org.springframework.boot</groupId> 242 + - <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> 243 + + <artifactId>spring-boot-starter-security-oauth2-resource-server</artifactId> 244 + </dependency> 245 + 246 + @@ -91,8 +91,8 @@ 247 + <dependency> 248 + - <groupId>org.flywaydb</groupId> 249 + - <artifactId>flyway-core</artifactId> 250 + + <groupId>org.springframework.boot</groupId> 251 + + <artifactId>spring-boot-starter-flyway</artifactId> 252 + </dependency> 253 + 254 + @@ -127,22 +127,32 @@ 255 + + <dependency> 256 + + <groupId>org.springframework.boot</groupId> 257 + + <artifactId>spring-boot-starter-webmvc-test</artifactId> 258 + + <scope>test</scope> 259 + + </dependency> 260 + + 261 + + <dependency> 262 + + <groupId>org.springframework.boot</groupId> 263 + + <artifactId>spring-boot-starter-data-jpa-test</artifactId> 264 + + <scope>test</scope> 265 + + </dependency> 266 + 267 + @@ -136,7 +136,7 @@ 268 + 269 + <dependency> 270 + <groupId>org.testcontainers</groupId> 271 + - <artifactId>postgresql</artifactId> 272 + + <artifactId>testcontainers-postgresql</artifactId> 273 + <scope>test</scope> 274 + </dependency> 275 + 276 + <dependency> 277 + - <groupId>org.springframework.security</groupId> 278 + - <artifactId>spring-security-test</artifactId> 279 + + <groupId>org.springframework.boot</groupId> 280 + + <artifactId>spring-boot-starter-security-test</artifactId> 281 + <scope>test</scope> 282 + </dependency> 283 + ``` 284 + 285 + At the `pom.xml` level, everything went well, all the expected modifications have been applied. 286 + 287 + The new modular architecture of Spring Boot 4 was correctly handled. 288 + 289 + The test code was also cleaned of the old deprecated `@MockBean`: 290 + 291 + ```text 292 + @@ -37,10 +37,10 @@ class ClassroomControllerMVCTest { 293 + @Autowired 294 + private MockMvc mockMvc; 295 + 296 + - @MockBean 297 + + @MockitoBean 298 + private ClassroomService classroomService; 299 + 300 + - @MockBean 301 + + @MockitoBean 302 + private Gitlab gitlab; 303 + ``` 304 + 305 + and some properties (which were in comments) were correctly renamed. 306 + 307 + ```text 308 + # generate full creation sql script if needed 309 + -spring.jpa.properties.javax.persistence.schema-generation.create-source=metadata 310 + -spring.jpa.properties.javax.persistence.schema-generation.scripts.action=update 311 + -spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=update.sql 312 + +spring.jpa.properties.jakarta.persistence.schema-generation.create-source=metadata 313 + +spring.jpa.properties.jakarta.persistence.schema-generation.scripts.action=update 314 + +spring.jpa.properties.jakarta.persistence.schema-generation.scripts.create-target=update.sql 315 + ``` 316 + 317 + ## The adjustments I had to do manually 318 + 319 + After this execution, my code doesn't compile. 320 + 321 + An import in my Spring Security configuration is not resolved 322 + 323 + ```java 324 + import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest; 325 + ``` 326 + 327 + because this class was moved to another package: 328 + 329 + ```java 330 + import org.springframework.boot.security.autoconfigure.actuate.web.servlet.EndpointRequest; 331 + ``` 332 + 333 + Once these small adjustments were made, I ran my unit tests. 334 + 335 + This time, I got an error message related to Spring Security at startup: 336 + 337 + ```text 338 + Caused by: java.lang.IllegalArgumentException: pattern must start with a / 339 + ``` 340 + 341 + 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. 342 + 343 + Once these last adjustments were made, the tests pass correctly ๐ŸŽ‰: 344 + 345 + ![Screenshot of my unit tests passing!](tests.png) 346 + 347 + ## Conclusion 348 + 349 + It took me about 1 hour to migrate my project from Spring Boot 3.5 to Spring Boot 4.0. 350 + 351 + OpenRewrite clearly made the work easier, it modified all my dependencies, and migrated deprecated annotations (which would have been tedious). 352 + 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). 353 + 354 + 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. 355 + 356 + Anyway, 1 hour of work to migrate a project of about 3,000 lines of code, I think it's quite efficient.
content/posts/2025-12-05-openrewrite-spring-boot-4/v2/index.md content/posts/2025-12-05-openrewrite-spring-boot-4/v2/index.fr.md