OpenRewrite allows us to do major refactorings on our source code using (prewritten) recipes. It works by making changes to the Lossless Semantic Trees representing our source code and printing the modifications back to the source code/diffs which we can then compare and commit if we deem them ok.
-
fixes: autoformatting, unused imports, applying new conventions using a recipe, …
-
migrations: log4j ⇒ slf4j, java 8 ⇒ 11 ⇒ 17, JUnit 4 ⇒ 5, …
-
static analysis fixes: resolve common issues reported by SAST tools, code cleanup, …
-
utility: generate a CycloneDx bill of materials, update GitHub actions, …
OpenRewrite makes changes to the Lossless Semantic Tree representation of your code using visitors. Visitors are basically event handlers, which deal with what to do, and when to do it that get triggered as OpenRewrite goes through the LST translation of our codebase. These visitors can in turn be gathered into recipes.
OpenRewrite can be run using the Maven/Gradle build plugin tools or directly from a java main
method if a build tool plugin isn’t possible (see for reference)
Both for Maven and Gradle we can run the migrations either by modifying our build files or by running a shell command or init script respectively.
If we add the plugin to our pom.xml file
<plugin>
<groupId>org.openrewrite.maven</groupId>
<artifactId>rewrite-maven-plugin</artifactId>
<version>5.12.0</version>
</plugin>
For Gradle, we need to be certain that mavenCentral()
is present in our repositories section, then we need to add the following to our build file:
plugins {
id 'org.openrewrite.rewrite' version '6.5.3'
}
repositories {
// needed to resolve recipe artifacts
mavenCentral()
}
rewrite {
// here we'll place the recipes we wish to use
}
Note: With Gradle. you can either add each dependency with the version number specified or add rewrite-recipe-bom
as a bill of materials dependency `rewrite(platform("org.openrewrite.recipe:rewrite-recipe-bom:2.5.0")) `
After which we can try ./mvnw rewrite:discover
or ./gradlew rewriteDiscover
to discover which recipes we can run from OpenRewrite using this setup. (we can add other sources/write our own).
Some OpenRewrite recipes require configuration, but we’ll start easy with a standard OpenRewrite which doesn’t need any setup.
For example, if you have a project with a lot of unused imports you can use the org.openrewrite.java.RemoveUnusedImports
recipe which is part of the core library.
a) run mvn -U org.openrewrite.maven:rewrite-maven-plugin:run -Drewrite.activeRecipes=org.openrewrite.java.RemoveUnusedImports
b) add <recipe>org.openrewrite.java.RemoveUnusedImports</recipe>
to the <activeRecipes>
in your pom file and perform ./mvnw rewrite:run
Add activeRecipe("org.openrewrite.java.RemoveUnusedImports")
and perform ./gradlew rewriteRun
If we were to run this one on the current project, and then execute a git diff
we’d see:
diff --git a/src/main/java/dev/simonverhoeven/openrewritedemo/OpenrewritedemoApplication.java b/src/main/java/dev/simonverhoeven/openrewritedemo/OpenrewritedemoApplication.java
index d97b878..8e85aaf 100644
--- a/src/main/java/dev/simonverhoeven/openrewritedemo/OpenrewritedemoApplication.java
+++ b/src/main/java/dev/simonverhoeven/openrewritedemo/OpenrewritedemoApplication.java
@@ -3,8 +3,6 @@ package dev.simonverhoeven.openrewritedemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
-import java.math.BigDecimal;
-
@SpringBootApplication
public class OpenrewritedemoApplication {
Some recipes require a configuration. Let’s start with an easy one. For example, your organisation changes its name, and suddenly you need to rewrite your package names.
For this, we can use the org.openrewrite.java.ChangePackage
recipe.
To set it up we’ll need to create a rewrite.yml
in which we’ll define a recipe name, optionally a display name, and the recipe list with the parameters.
In this case, we’ll be renaming dev.simonverhoeven.openrewritedemo.oldorgname
to dev.simonverhoeven.openrewritedemo.neworgname
---
type: specs.openrewrite.org/v1beta/recipe
name: dev.simonverhoeven.sampleRecipe
displayName: A simple recipe
recipeList:
- org.openrewrite.java.ChangePackage:
oldPackageName: dev.simonverhoeven.openrewritedemo.oldorgname
newPackageName: dev.simonverhoeven.openrewritedemo.neworgname
recursive: null
Then we’ll add dev.simonverhoeven.sampleRecipe
to our active recipes.
When we then run the rewrite we’ll see that our oldorgname
has been renamed to neworgname
and that the package statement in our Sample
file has also been adapted.
As of Rewrite version 8.9.0 we can once again write Preconditions
for our recipes like in Rewrite 7.
Those conditions allow us to introduce some conditionality to our recipes, such as for example only applying a recipe in case of certain Java versions using org.openrewrite.java.search.HasJavaVersion
. In case we define multiple conditions then a file must meet them all before any changes are applied.
Note
|
Precondition recipes can make changes to determine whether the condition is met, but are not included in the final result. |
For example:
---
type: specs.openrewrite.org/v1beta/recipe
name: dev.simonverhoeven.preconditionExample
preconditions:
- org.openrewrite.java.search.HasJavaVersion:
version: 8.X
recipeList:
- org.openrewrite.text.FindAndReplace:
find: somethingold
replace: somethingnew
filePattern: '**/application.properties'
Our FindAndReplace
recipe will only be applied in case the HasJavaVersion
recipe condition is met.
It is possible to use OpenRewrite without the build tool plugins, the hardest part is determining the applicable classpath for each set of files. A brief overview of the approach is documented at running rewrite without build tool plugins on the OpenRewrite website.
For now, we’ve used 2 quite basic recipes, which had relatively limited impact. Now let’s take a leap forward to Java 17 & Spring Boot 3.1.
Now taking a look at our project, we stumble upon an issue. We’re still using Hamcrest
, which is no longer actively being supported, and we’ve encountered some challenges with using it. So a migration to a different framework such as AssertJ
seems apt.
OpenRewrite has a lot of individual recipes for this, but we can also use org.openrewrite.recipe:rewrite-testing-frameworks:2.1.0
⇒ org.openrewrite.java.testing.hamcrest.MigrateHamcrestToAssertJ
which has no required input.
So we can just add this one to our pom.xml or build.gradle
, or execute it directly from the mvn command line.
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-testing-frameworks:RELEASE \
-Drewrite.activeRecipes=org.openrewrite.java.testing.hamcrest.MigrateHamcrestToAssertJ
After running this command you can see that this recipe has managed to fully replace all usages of Hamcrest. So if desired one can remove the library.
And we'd love to finally start using `spring-boot-starter-test`.
Now we’d like to take the sensible approach and make certain that all of our tests run properly using this library. Now here’s where we stumble upon a small hiccup. For some reason, our project’s using JUnit 4, not 5 and since Spring Boot 2.2 the backward compatibility with Spring JUnit 4 has been dropped.
As documented the upgrade to JUnit 5 entails a couple of steps for which there are recipes
-
@Ignore
⇒@Disabled
:org.openrewrite.java.testing.junit5.IgnoreToDisabled
-
org.junit.Assert
⇒org.junit.jupiter.api.Assertions
:org.openrewrite.java.test.junit5.AssertToAssertions
-
org.junit.Test
⇒org.junit.jupiter.api.Test
:org.openrewrite.java.test.junit5.UpdateTestAnnotation
-
@Junit 4’s
@Rule ExpectedException ⇒ JUnit 5’s `Assertions.assertThrows()
:org.openrewrite.java.testing.junit5.ExpectedExceptionToAssertThrows
-
…
And that’s the premise behind OpenRewrite, large migrations in small steps.
One of the recipes we can use for this is org.openrewrite.java.testing.junit5.JUnit4to5Migration for which we’ll need a dependency on org.openrewrite.recipe:rewrite-testing-frameworks:2.1.0
.
When we execute this recipe we’ll get
[WARNING] Changes have been made to pom.xml by:
[WARNING] org.openrewrite.java.testing.junit5.JUnit4to5Migration
[WARNING] org.openrewrite.java.dependencies.RemoveDependency: {groupId=junit, artifactId=junit}
[WARNING] org.openrewrite.maven.RemoveDependency: {groupId=junit, artifactId=junit}
[WARNING] org.openrewrite.java.dependencies.AddDependency: {groupId=org.junit.jupiter, artifactId=junit-jupiter, version=5.x, onlyIfUsing=org.junit.jupiter.api.Test, acceptTransitive=true}
[WARNING] org.openrewrite.maven.AddDependency: {groupId=org.junit.jupiter, artifactId=junit-jupiter, version=5.x, onlyIfUsing=org.junit.jupiter.api.Test, acceptTransitive=true}
[WARNING] Changes have been made to src\test\java\dev\simonverhoeven\openrewritedemo\JunitTest.java by:
[WARNING] org.openrewrite.java.testing.junit5.JUnit4to5Migration
[WARNING] org.openrewrite.java.testing.junit5.IgnoreToDisabled
[WARNING] org.openrewrite.java.ChangeType: {oldFullyQualifiedTypeName=org.junit.Ignore, newFullyQualifiedTypeName=org.junit.jupiter.api.Disabled}
[WARNING] org.openrewrite.java.testing.junit5.AssertToAssertions
[WARNING] org.openrewrite.java.testing.junit5.CategoryToTag
[WARNING] org.openrewrite.java.testing.junit5.TemporaryFolderToTempDir
[WARNING] org.openrewrite.java.testing.junit5.UpdateBeforeAfterAnnotations
[WARNING] org.openrewrite.java.testing.junit5.UpdateTestAnnotation
[WARNING] org.openrewrite.java.testing.junit5.ExpectedExceptionToAssertThrows
If we then run a git diff
to see the changes that were made we’ll notice that our pom.xml
has been upgraded, our imports are now from the jupiter
hierarchy, @Ignore
⇒ @Disabled
, Assert.
⇒ Assertions.
, …
note: there are multiple recipes that can be used from this. For example, there’s also org.openrewrite.java.spring.boot2.SpringBoot2JUnit4to5Migration
which is a superset of the JUnit 4 to 5 & Mockito 1 to 3 recipes.
Now we can run those tests, and everything looks fine and dandy.
Let’s take the next step, and try a migration to Java 17 and spring boot.
In our pom.xml:
<plugin>
<groupId>org.openrewrite.maven</groupId>
<artifactId>rewrite-maven-plugin</artifactId>
<version>5.12.0</version>
<configuration>
<activeRecipes>
<recipe>org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_1</recipe>
</activeRecipes>
</configuration>
<dependencies>
<dependency>
<groupId>org.openrewrite.recipe</groupId>
<artifactId>rewrite-spring</artifactId>
<version>5.1.1</version>
</dependency>
</dependencies>
</plugin>
or build.gradle:
plugins {
id("org.openrewrite.rewrite") version("6.5.3")
}
rewrite {
activeRecipe("org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_1")
}
repositories {
mavenCentral()
}
dependencies {
rewrite("org.openrewrite.recipe:rewrite-spring:5.1.1")
}
After running ./mvnw rewrite:run
or ./gradlew rewriteRun
we can use git diff
to take a look at the results.
And we can see a lot of interesting changes:
-
our outdated spring properties have been migrated
-
our Java version has been upgraded from java 8 to 17 (the new spring boot 3 baseline), including improvements such as:
-
using the BigDecimal RoundingMode enum rather than an int
-
!emptyOptional.isPresent();
⇒emptyOptional.isEmpty()
-
concatenated text has been replaced with a text block
-
updated String formatting
-
-
JUnit 4 ⇒ JUnit 5
-
…
We got all this thanks to the recipe list of UpgradeSpringBoot_3_1
It’s quite amazing to see what we can achieve with just this simple action.
One will quite likely encounter Guava in a lot of older projects, it offered us a lot of functionality that wasn’t part of the JDK. Over the years a lot of this functionality has become part of it though, and after all the effort we’ve done to upgrade our project we’d like to use the standard JDK as much as possible.
For example, in our SampleService we’ll see that a lot of things are being done using the Guava library.
OpenRewrite has a lot of individual recipes for this, but we can also use org.openrewrite.recipe:rewrite-migrate-java:2.3.0
⇒ org.openrewrite.java.migrate.guava.NoGuava
which has no required input.
So we can just add this one to our pom.xml or build.gradle
, or execute it directly from the mvn command line.
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-migrate-java:RELEASE \
-Drewrite.activeRecipes=org.openrewrite.java.migrate.guava.NoGuava
After running this command you can see that this recipe has managed to fully replace all usages of Guava. So if desired one can remove the library.
Now that we’ve done all this, we’re finally starting to reach our target. The next thing we’d like to tackle are the results we got from our upgraded Sonar instance. Whilst some of these will of course require human intervention, OpenRewrite offers a lot of (composite) recipes which will help us clean up the common issues which can be found at static analysis.
We can run a lot of recipes manually, such as org.openrewrite.staticanalysis.MissingOverrideAnnotation
, but our eye swiftly gets drawn to org.openrewrite.staticanalysis.CommonStaticAnalysis which is part of org.openrewrite.recipe:rewrite-static-analysis:1.1.0
and has no required input.
So we can just do
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-static-analysis:RELEASE \
-Drewrite.activeRecipes=org.openrewrite.staticanalysis.CommonStaticAnalysis
And we’ll notice that a lot of complaints such as:
-
missing serialVersionUID
-
inverted boolean checks
-
catch should do more than just rethrow
-
modifier order
-
missing braces
-
Strings not using .equals
-
unnecessary String#toString()
-
no double variable declaration
-
…
are resolved for us
In our console we’ll see:
[WARNING] org.openrewrite.staticanalysis.CommonStaticAnalysis
[WARNING] org.openrewrite.staticanalysis.BigDecimalRoundingConstantsToEnums
[WARNING] Changes have been made to src\main\java\dev\simonverhoeven\openrewritedemo\oldorgname\SampleController.java by:
[WARNING] org.openrewrite.staticanalysis.CommonStaticAnalysis
[WARNING] org.openrewrite.staticanalysis.AddSerialVersionUidToSerializable
[WARNING] org.openrewrite.staticanalysis.BooleanChecksNotInverted
[WARNING] org.openrewrite.staticanalysis.CaseInsensitiveComparisonsDoNotChangeCase
[WARNING] org.openrewrite.staticanalysis.DefaultComesLast
[WARNING] org.openrewrite.staticanalysis.EmptyBlock
[WARNING] org.openrewrite.staticanalysis.FinalizePrivateFields
[WARNING] org.openrewrite.staticanalysis.FinalClass
[WARNING] org.openrewrite.staticanalysis.ForLoopIncrementInUpdate
[WARNING] org.openrewrite.staticanalysis.ModifierOrder
[WARNING] org.openrewrite.staticanalysis.MultipleVariableDeclarations
[WARNING] org.openrewrite.staticanalysis.NoToStringOnStringType
[WARNING] org.openrewrite.staticanalysis.RemoveExtraSemicolons
[WARNING] org.openrewrite.staticanalysis.RenamePrivateFieldsToCamelCase
[WARNING] org.openrewrite.staticanalysis.UseDiamondOperator
[WARNING] org.openrewrite.staticanalysis.InlineVariable
And looking at SampleController will reveal a lot of changes
Now OpenRewrite goes beyond just rewriting one’s codebase. There are a lot of other convenient features:
There are quite a bit of recipes to help you manage your GitHub workflows.
For example, there’s setup-java which updates your setup-java action if needed (and is part of the upgrade to Spring Boot 3.1 recipe for example)
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-github-actions:RELEASE \
-Drewrite.activeRecipes=org.openrewrite.github.SetupJavaUpgradeJavaVersion
Or say if one wants to bulk update the used runners there’s the replacerunners recipe.
OpenRewrite offers a lot of recipes at cloud suitability to help you determine the cloud suitability of your project
One nice example is findunsuitablecode
Which will scan for items that may potentially cause issues such as:
-
usage of ehcache
-
usage of corba
-
hardcoded IP addresses
-
remote method invocation
-
unhandled term signals
-
…
Hopefully one will never need these, but there are recipes to scan for different types of secrets within your codes.
For example one can spot that in our SampleController we have:
private static final String ACCOUNT_KEY = "lJzRc1YdHaAA2KCNJJ1tkYwF/+mKK6Ygw0NGe170Xu592euJv2wYUtBlV8z+qnlcNQSnIYVTkLWntUO1F8j8rQ==";
After running:
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-java-security:RELEASE \
-Drewrite.activeRecipes=org.openrewrite.java.security.secrets.FindAzureSecrets
We’ll see that it has been transformed to:
private static final String ACCOUNT_KEY = /*~~(Azure access key)~~>*/"lJzRc1YdHaAA2KCNJJ1tkYwF/+mKK6Ygw0NGe170Xu592euJv2wYUtBlV8z+qnlcNQSnIYVTkLWntUO1F8j8rQ==";
Which makes it a lot easier for us to find these kind of issues.
You might be asked to provide a list of your (transitive) project dependencies, this can easily be achieved using the cyclonedx
goal.
OpenRewrite has so many more interesting recipes, and I’d invite you to take a gander at their recipe list.
A last one I wanted to point out which showcases a way in which OpenRewrite can help with the readability of your codebase is the formatsql one
Which automatically transforms this:
class Test {
String query = """
SELECT b.book_id, b.title, COUNT(r.review_id) AS num_reviews,AVG(r.rating) AS median_rating FROM books b
JOIN reads rd ON b.book_id = rd.book_id JOIN readers
re ON rd.reader_id = re.reader_id
JOIN reviews r ON b.book_id = r.book_id
GROUP BY b.book_id, b.title ORDER
BY num_reviews DESC;\
""";
}
to
class Test {
String query = """
SELECT
b.book_id,
b.title,
COUNT(r.review_id) AS num_reviews,
AVG(r.rating) AS median_rating
FROM
books b
JOIN reads rd ON b.book_id = rd.book_id
JOIN readers re ON rd.reader_id = re.reader_id
JOIN reviews r ON b.book_id = r.book_id
GROUP BY
b.book_id,
b.title
ORDER BY
num_reviews DESC;\
""";
}
-
Moderne - a platform to automate migrating, securing, and maintaining source code. It uses OpenRewrite recipes and offers certain extra features like data tables to view the changes that were made. It is free for open source projects.
-
Spring boot migrator - a CLI tool that offers recipes to migrate/upgrade an application to Spring boot and is compatible with & uses OpenRewrite
If you have a multi-module maven project you might run into errors when using the maven plugin, a workaround & more information is documented at using multi-module maven.