1. Overview

OpenRewrite is a refactoring ecosystem for Java and other source code. Sometimes, we need to upgrade dependencies to the latest versions, apply security patches, eliminate the use of a deprecated API, migrate from one technology to another (e.g., JUnit asserts to AssertJ), etc. We can use the OpenRewrite library to address these challenges. In this tutorial, we’ll discuss the basics of the OpenRewrite project and show some examples of how it can be used in practice. In every case, we’ll use the Spring PetClinic application.

2. OpenRewrite Basics

Here are some of the common types of upgrades we can perform with OpenRewrite:

  • Language Upgrades: migrating from an older version of a language to a newer version, such as Java 8 to Java 11 or beyond.
  • Framework Upgrades: adapting to newer versions of frameworks like Spring Boot, Hibernate, etc.
  • Dependency Migration: upgrading from one library version to another when there are breaking changes.
  • Security Patching: replacing vulnerable methods or libraries with secure alternatives.
  • Custom Transformations: any transformation specific to our business logic or infrastructure.

2.1. OpenRewrite Recipes

The main feature of OpenRewrite is that it will automatically refactor source code by applying recipes to the project. OpenRewrite comes with a variety of built-in recipes for common transformations, and the community often contributes recipes for widespread tasks. Each recipe can perform specific refactoring tasks. These recipes are written in Java code and included in the build process using the OpenRewrite Maven or Gradle plugin. In this tutorial, we’ll focus on the Maven plugin.

2.2. Maven Dependency

Let’s start by importing the rewrite-maven-plugin plugin in the section of the pom.xml:

<plugin>
    <groupId>org.openrewrite.maven</groupId>
    <artifactId>rewrite-maven-plugin</artifactId>
    <version>5.8.1</version>
    <configuration>
        <activeRecipes>
        // Define the recipes 
        </activeRecipes>
    </configuration>
    <dependencies>
        // Dependencies for recipes
    </dependencies>
</plugin>

The tags specify which OpenRewrite recipes should be active when the plugin runs. Also, we can define any additional dependencies that the plugin might need. This can be particularly useful if certain recipes require external libraries or specific versions of libraries.

2.3. Checkout Spring PetClinic

Let’s check out the relevant branch of Spring PetClinic locally:

git clone https://github.com/spring-projects/spring-petclinic.git;
cd spring-petclinic;
git switch -c 2.5.x 1.5.x;

Here, the Java and Spring Boot versions are 8 and 1.5, respectively. In the next sections, we’ll go through the most widely used recipes.

3. Upgrading Spring Boot

Upgrading a Spring Boot project can be a complex task, depending on the changes between the version we’re currently using and the version we want to upgrade to. However, using OpenRewrite can help us navigate the process smoothly. Let’s upgrade the PetClinic project from Spring Boot 1.5 to 2.7.

3.1. Maven Dependency

Let’s add the UpgradeSpringBoot_2_7 recipe to the section in the rewrite-maven-plugin plugin:

<activeRecipes>
    <recipe>org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_7</recipe>
</activeRecipes>

Also, we need to declare the rewrite-spring dependency to the section of our plugin:

<dependency>
    <groupId>org.openrewrite.recipe</groupId>
    <artifactId>rewrite-spring</artifactId>
    <version>5.0.11</version>
</dependency>

Note that to upgrade to Spring Boot 3, we need to use the UpgradeSpringBoot_3_0 recipe.

3.2. Run Spring Boot Upgrade Recipe

Now, we’re ready to execute the migration by running this command:

mvn rewrite:run

Below, we can see the results of Maven:

[INFO] --- rewrite-maven-plugin:5.8.1:run (default-cli) @ spring-petclinic ---
[INFO] Using active recipe(s) [org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_7]
[INFO] Using active styles(s) []
[INFO] Validating active recipes...
[INFO] Project [petclinic] Resolving Poms...
[INFO] Project [petclinic] Parsing source files

Here, we see that Maven lists active recipes and applies the pom and source file changes. After running the recipe, OpenRewrite provides a list of changes. We can inspect the results with git diff:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
-   <version>1.5.4.RELEASE</version>
+   <version>2.7.14</version>
</parent>

As we can see here, the spring-boot-starter-parent version has been upgraded to 2.7.14. In addition, the @Autowired has been removed from some of our Java classes during the upgrade process:

-import org.springframework.beans.factory.annotation.Autowired;
class VetController {

    private final VetRepository vets;

-   @Autowired
    public VetController(VetRepository clinicService) {
        this.vets = clinicService;
}

Note that in Spring Boot 2.x and above, if a class has a single constructor, it will automatically be used for autowiring without needing the @Autowired annotation. When we upgrade to a newer version of Spring Boot, many of the managed dependencies, including JUnit, may get upgraded as well. Spring Boot 2.x defaults to using JUnit 5, whereas Spring Boot 1.5 was aligned with JUnit 4. This means there’s an expected change in how tests are structured and run. Depending on our needs, we can just upgrade JUnit without upgrading Spring Boot. In the next section, we’ll see how to migrate from JUnit 4 to JUnit 5.

4. Upgrading to JUnit5

JUnit is the de facto standard for unit testing in Java applications. OpenRewrite supports migrating from JUnit 4 to its successor JUnit 5.

4.1. Maven Dependency

Let’s activate the JUnit5BestPractices recipe in our pom.xml:

<activeRecipes>
    <recipe>org.openrewrite.java.testing.junit5.JUnit5BestPractices</recipe>
</activeRecipes>

Also, we need to declare the rewrite-testing-frameworks dependency to the section of our plugin:

<dependency>
    <groupId>org.openrewrite.recipe</groupId>
    <artifactId>rewrite-testing-frameworks</artifactId>
    <version>2.0.12</version>
</dependency>

4.2. Run JUnit Upgrade Recipe

Now, we execute the migration by running the mvn rewrite:run command. This command is completed in about a minute and migrates all tests to JUnit 5, as well as updating the pom.xml:

-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.junit.jupiter.MockitoExtension;


-    @Before
-    public void setup() {
+    @BeforeEach
+    void setup() {
         this.petTypeFormatter = new PetTypeFormatter(pets);
     }

-    @Test(expected = ParseException.class)
-    public void shouldThrowParseException() throws ParseException {
-        Mockito.when(this.pets.findPetTypes()).thenReturn(makePetTypes());
-        petTypeFormatter.parse("Fish", Locale.ENGLISH);
+    @Test
+    void shouldThrowParseException() throws ParseException {
+        assertThrows(ParseException.class, () -> {
+            Mockito.when(this.pets.findPetTypes()).thenReturn(makePetTypes());
+            petTypeFormatter.parse("Fish", Locale.ENGLISH);
+        });
     }

OpenRewrite updates not just the imports and @Test annotations, but also method visibility, applies MockitoExtension, and adopts assertThrows(). In general, OpenRewrite can help JUnit migration with tasks such as:

  • Updating annotations, e.g., changing @Before to @BeforeEach or @After to @AfterEach.
  • Modifying assertion method calls to align with JUnit 5’s syntax.
  • Removing or replacing JUnit 4 specific features with their JUnit 5 counterparts.
  • Updating import statements from JUnit 4 packages to JUnit 5 packages.

5. Upgrading to Java 11

Upgrading an older source code to Java 11 can be a challenging and time-consuming issue. In this section, we’ll use OpenRewrite to perform an automated migration from Java 8 to Java 11.

5.1. Maven Dependency

To upgrade Java, we need a different dependency and recipe. Let’s add the Java8toJava11 recipe to the section:

<activeRecipes>
    <recipe>org.openrewrite.java.migrate.Java8toJava11</recipe>
</activeRecipes>

Also, we need to declare the rewrite-migrate-java dependency to our plugin:

<dependency>
    <groupId>org.openrewrite.recipe</groupId>
    <artifactId>rewrite-migrate-java</artifactId>
    <version>2.1.1</version>
</dependency>

Note that to upgrade to Java 17, we need to use the UpgradeToJava17 recipe.

5.2. Run Java Upgrade Recipe

To upgrade the Java, we can apply the mvn rewrite:run command. After that, we’ll see output like:

[INFO] Using active recipe(s) [org.openrewrite.java.migrate.Java8toJava11]
[INFO] Running recipe(s)...
[WARNING] Changes have been made to pom.xml by:
[WARNING]     org.openrewrite.java.migrate.Java8toJava11
[WARNING]         org.openrewrite.java.migrate.javax.AddJaxbDependencies
[WARNING]             org.openrewrite.java.dependencies.AddDependency: {groupId=jakarta.xml.bind, artifactId=jakarta.xml.bind-api, version=2.3.x, onlyIfUsing=javax.xml.bind..*, acceptTransitive=true}
[WARNING]                 org.openrewrite.maven.AddDependency: {groupId=jakarta.xml.bind, artifactId=jakarta.xml.bind-api, version=2.3.x, onlyIfUsing=javax.xml.bind..*, acceptTransitive=true}
[WARNING]             org.openrewrite.java.migrate.javax.AddJaxbRuntime: {runtime=glassfish}
[WARNING]                 org.openrewrite.java.migrate.javax.AddJaxbRuntime$AddJaxbRuntimeMaven
[WARNING]         org.openrewrite.java.migrate.cobertura.RemoveCoberturaMavenPlugin
[WARNING]             org.openrewrite.maven.RemovePlugin: {groupId=org.codehaus.mojo, artifactId=cobertura-maven-plugin}
[WARNING]         org.openrewrite.java.migrate.wro4j.UpgradeWro4jMavenPluginVersion
[WARNING]             org.openrewrite.maven.UpgradePluginVersion: {groupId=ro.isdc.wro4j, artifactId=wro4j-maven-plugin, newVersion=1.10.1}
[WARNING]         org.openrewrite.java.migrate.JavaVersion11
[WARNING]             org.openrewrite.java.migrate.UpgradeJavaVersion: {version=11}

The Java8toJava11 recipe produces the following main relevant changes:

-    <java.version>1.8</java.version>
+    <java.version>11</java.version>

-    <wro4j.version>1.8.0</wro4j.version>
+    <wro4j.version>1.10.1</wro4j.version>

+    <dependency>
+      <groupId>jakarta.xml.bind</groupId>
+      <artifactId>jakarta.xml.bind-api</artifactId>
+      <version>2.3.3</version>
+    </dependency>

+    <dependency>
+      <groupId>org.glassfish.jaxb</groupId>
+      <artifactId>jaxb-runtime</artifactId>
+      <version>2.3.8</version>
+      <scope>provided</scope>
+    </dependency>

Here, we see that the java.version Maven property is changed to 11, which sets maven.compiler.source and maven.compiler.target. This unlocks Java 11 language features such as var. Along the way, wro4j is updated to their latest version as well. Wro4j is a popular open-source project for optimizing and managing web resources in Java applications, such as JavaScript and CSS files. In addition, when migrating from Java 8 to Java 11, one of the major changes is the removal of the javax.xml.bind module from the JDK. Many applications that rely on this module for JAXB functionality need to add third-party dependencies to retain this functionality. One such replacement is the jakarta.xml.bind-api dependency.

6. Conclusion

In this tutorial, we have provided a step-by-step guide on leveraging the OpenRewrite project to seamlessly upgrade versions of Spring Boot, JUnit, and Java within the Spring PetClinic codebase. By following this tutorial, we can gain insights into the intricacies of the upgrade process, ensuring smoother transitions and better code compatibility for these core components.