Migrating Legacy Spring (Java 8) to Spring Boot (Java 17) Using OpenRewrite Recipes
Modernizing a legacy Spring application can feel like moving from a manual transmission car to a self-driving Tesla. Sure, both get you to your destination, but one does it with way more automation, style, and fewer headaches. Enter OpenRewrite, the hero of this story, which helps automate your migration from Spring on Java 8 to Spring Boot on Java 17.
This blog explores declarative recipes (the ready-made solutions) and custom recipes (the tailored fixes), helping you master both sides of this modernization journey.
The Why and How of OpenRewrite Recipes
OpenRewrite recipes are pre-configured or customizable scripts that automate tedious refactoring tasks.
Declarative Recipes: Address common migration challenges with pre-built logic.
Custom Recipes: Handle project-specific quirks like XML-to-Java configuration.
Behind the scenes, OpenRewrite uses Lossless Semantic Trees (LSTs) to parse code. Unlike traditional parsers, LSTs preserve formatting (comments, whitespace), ensuring the output looks like you wrote it yourself—on a good day.
Declarative Recipes for Migration
Declarative recipes are like IKEA furniture: pre-designed but requiring some assembly. Here's a rundown of key recipes for your migration:
1. Upgrade Java Version (Java 8 to Java 17)
Java 17 introduces cool features like TextBlocks and Switch Expressions while deprecating older APIs. OpenRewrite handles the heavy lifting:
org.openrewrite.java.migrate.UseStringStrip
Replacestrim()with the Unicode-compliantstrip().Example:
String value = input.trim(); // Before
String value = input.strip(); // After
org.openrewrite.java.migrate.SwitchToTextBlocks
Converts concatenated strings into elegantTextBlocks:
String json = "{\n" + " \"key\": \"value\"\n" + "}"; // Before
String json = """
{
"key": "value"
}
"""; // After
org.openrewrite.java.migrate.UseCollectionFactoryMethods
Refactors old-school collection factories:
Collections.unmodifiableList(Arrays.asList("A", "B")); // Before
List.of("A", "B"); // After
2. Spring to Spring Boot Migration
Spring Boot minimizes boilerplate configurations. OpenRewrite declarative recipes streamline this shift:
org.openrewrite.spring.boot2.SpringBoot2Migration
Automates upgrades to Spring Boot 2.x conventions.org.openrewrite.spring.boot2.UpdatePropertiesToApplicationYaml
Convertsapplication.propertiesto structuredapplication.yml.org.openrewrite.spring.boot2.SpringBootSecurity
Refactors security configurations from XML to Java annotations:
<http>
<intercept-url pattern="/admin/**" access="hasRole('ADMIN')" />
</http>
Becomes
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN");
}
}
org.openrewrite.spring.boot2.SpringBootTestMigration
Updates legacy test configurations to@SpringBootTest.
Custom Recipes for Project-Specific Needs
Declarative recipes can’t cover every edge case. Here’s where custom recipes shine, addressing unique scenarios like XML bean definitions and property files.
1. Convert XML Bean Definitions to Java Configuration
Spring Boot favors Java-based configurations over XML. Automate this transformation:
Input: applicationContext.xml
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
<property name="url" value="jdbc:mysql://localhost:3306/mydb" />
<property name="username" value="root" />
<property name="password" value="password" />
</bean>
Custom Recipe Code
public class ConvertXmlToJavaConfig extends Recipe {
@Override
public String getDisplayName() {
return "Convert XML Bean Definitions to Java Configuration";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new XmlVisitor<ExecutionContext>() {
@Override
public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) {
if ("bean".equals(tag.getName())) {
String id = tag.getAttributeValue("id");
String className = tag.getAttributeValue("class");
StringBuilder javaConfig = new StringBuilder();
javaConfig.append(String.format("@Bean\npublic %s %s() {\n", className, id));
javaConfig.append(" return new ").append(className).append("();\n}");
return tag.withContent(Xml.Comment.build("Converted to Java config:\n" + javaConfig));
}
return super.visitTag(tag, ctx);
}
};
}
}
Output Java Configuration
@Bean
public org.apache.commons.dbcp2.BasicDataSource dataSource() {
return new org.apache.commons.dbcp2.BasicDataSource()
.setUrl("jdbc:mysql://localhost:3306/mydb")
.setUsername("root")
.setPassword("password");
}
2. Refactor application.properties to application.yml
Spring Boot prefers YAML for configurations. Automate the conversion of .properties to .yml.
Custom Recipe Code
public class ConvertPropertiesToYaml extends Recipe {
@Override
public String getDisplayName() {
return "Convert .properties to application.yml";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new PropertiesVisitor<ExecutionContext>() {
@Override
public Properties visitEntry(Properties.Entry entry, ExecutionContext ctx) {
String key = entry.getKey();
String value = entry.getValue();
String yamlFormatted = key.replace('.', ':') + ": " + value;
return entry.withValue("<!-- YAML Format -->\n" + yamlFormatted);
}
};
}
}Input
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=password
server.port=8080
Output
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
username: root
password: password
server:
port: 8080
3. Replace @Controller with @RestController
Legacy Spring controllers returning JSON can be refactored to use @RestController.
Custom Recipe Code
public class ConvertControllerToRestController extends Recipe {
@Override
public String getDisplayName() {
return "Convert @Controller to @RestController";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new JavaVisitor<ExecutionContext>() {
@Override
public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ctx) {
if ("Controller".equals(annotation.getSimpleName())) {
return annotation.withTemplate(
JavaTemplate.builder(this::getCursor, "@RestController").build(),
annotation.getCoordinates().replace()
);
}
return super.visitAnnotation(annotation, ctx);
}
};
}
}Before
@Controller
public class MyController {
@RequestMapping("/hello")
public String sayHello() {
return "Hello, World!";
}
}
After
@RestController
public class MyController {
@RequestMapping("/hello")
public String sayHello() {
return "Hello, World!";
}
}
4. Add Spring Boot Starter Dependencies
Spring Boot uses "starter" dependencies to simplify configurations. This custom recipe replaces individual dependencies with starters.
Custom Recipe Code
public class AddSpringBootStarters extends Recipe {
@Override
public String getDisplayName() {
return "Add Spring Boot Starter Dependencies";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new MavenVisitor() {
@Override
public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) {
if ("dependency".equals(tag.getName()) && tag.getChildValue("artifactId").isPresent()) {
String artifactId = tag.getChildValue("artifactId").get();
if (artifactId.contains("spring-context") || artifactId.contains("spring-web")) {
return tag.withTemplate(
JavaTemplate.builder(this::getCursor, "<dependency>\n" +
" <groupId>org.springframework.boot</groupId>\n" +
" <artifactId>spring-boot-starter</artifactId>\n" +
"</dependency>").build(),
tag.getCoordinates().replace()
);
}
}
return super.visitTag(tag, ctx);
}
};
}
}Input
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
Output
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
Migration Workflow
Run Declarative Recipes
Upgrade Java APIs (UseStringStrip,SwitchToTextBlocks), and apply Spring Boot recipes (SpringBoot2Migration,SpringBootTestMigration).Apply Custom Recipes
Handle project-specific needs like XML-to-Java configuration and property-to-YAML conversion.Test and Validate
Validate using updated test configurations and deploy in a staging environment.
Conclusion
With OpenRewrite, migrating a legacy Spring application to Spring Boot on Java 17 becomes a streamlined process. Declarative recipes handle the common challenges, while custom recipes help navigate project-specific quirks. Add Moderne to the mix, and you can scale this magic across multiple repositories!
So, grab your migration latte ☕ and let OpenRewrite do the heavy lifting.
Reference Section
Additional Dependencies for custom Recipes
Here’s a complete build.gradle file with all the necessary dependencies for creating custom OpenRewrite recipes. Each dependency is accompanied by a comment explaining its purpose.
plugins {
id 'java'
}
group = 'com.example'
version = '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
ext {
// Define the OpenRewrite version in one place for easier updates
openrewriteVersion = '8.0.0' // Replace with the latest version
}
dependencies {
// Core OpenRewrite library for all recipe processing
implementation "org.openrewrite:rewrite-core:$openrewriteVersion"
// Java-specific recipes, visitors, and utilities
implementation "org.openrewrite:rewrite-java:$openrewriteVersion"
// XML file support (e.g., refactoring applicationContext.xml)
implementation "org.openrewrite:rewrite-xml:$openrewriteVersion"
// Maven-specific recipes for managing pom.xml files
implementation "org.openrewrite:rewrite-maven:$openrewriteVersion"
// Properties file support (e.g., refactoring application.properties)
implementation "org.openrewrite:rewrite-properties:$openrewriteVersion"
// YAML file support (e.g., refactoring application.yml)
implementation "org.openrewrite:rewrite-yaml:$openrewriteVersion"
// Support for Gradle build file refactoring
implementation "org.openrewrite:rewrite-gradle:$openrewriteVersion"
// Testing utilities for writing unit tests for custom recipes
testImplementation "org.openrewrite:rewrite-test:$openrewriteVersion"
// Optional: Java runtime updates (e.g., migration from Java 8 to Java 17)
implementation "org.openrewrite.recipe:rewrite-java-17:$openrewriteVersion"
// Optional: Spring Boot-specific recipes
implementation "org.openrewrite.recipe:rewrite-spring:$openrewriteVersion"
}
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
options.release = 8 // Set this to your Java version
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17) // Set to your target Java version
}
}
Dependency Highlights
Core Dependency:
org.openrewrite:rewrite-core: The backbone of OpenRewrite.
Java-Specific Support:
org.openrewrite:rewrite-java: For Java code transformations.
File-Type-Specific Dependencies:
org.openrewrite:rewrite-xml: Handles XML file refactoring.org.openrewrite:rewrite-properties: Refactors.propertiesfiles.org.openrewrite:rewrite-yaml: Refactors.ymlfiles.
Build Tool Support:
org.openrewrite:rewrite-gradle: Handles Gradle build file refactoring.org.openrewrite:rewrite-maven: For Mavenpom.xmltransformations.
Java Migration Recipes:
org.openrewrite.recipe:rewrite-java-17: Includes recipes for migrating Java codebases to Java 17.
Spring Recipes:
org.openrewrite.recipe:rewrite-spring: Recipes tailored for migrating and refactoring Spring projects.
Testing:
org.openrewrite:rewrite-test: Enables writing and running unit tests for custom recipes.
Usage Tips
Use the toolchain configuration to target specific Java versions.
Adjust the
options.releaseto match the source compatibility of your project.Keep your dependencies updated to the latest version to leverage new features and improvements.
This configuration should cover all the dependencies you need to build and test custom recipes with OpenRewrite effectively! 🚀
