Suppose you are working on an older Java project. Your team has decided that it wants to introduce Lombok to get rid of all the getter and setter methods in the code base to make the code more readable. Instead of manually introducing the Lombok annotations and removing the getter/setter methods, you want to leverage OpenRewrite to automate this task. Since there are no officially supported Lombok recipes available, you’ll have to write your own recipe.
Planning the structure of the recipe
Before writing the recipe, you should first think carefully about all the constraints that
your recipe should adhere to. Otherwise you run the risk of changing too much code
or forgetting certain use cases. For the sake of simplicity, let’s focus on replacing
getter methods with the Lombok
since replacing setter methods should work almost identically. For the getter methods, these constraints should be as follows:
- It should add the
@Getterannotation to a variable if and only if this variable is a field and has a corresponding getter method in the same class.
- If a field
fhas a boolean type, the name of the getter method should be equal to
isF(), otherwise it should be equal to
- The return type of the getter method should match the type of the field.
- In the case of multiple field declarations on a single line, the annotation should only be added if
the class contains a getter method for each field in this declaration.
For example if the class has the declaration
int a, b;it should also have the getter methods
To avoid creating recipes that perform too much complex logic, it is helpful to split the recipe into multiple building blocks. Each building block should be a recipe in itself and can be linked together to form a larger recipe. The advantage of this approach is that you have many small recipes that are easy to test, and that you can often use existing recipes to perform small tasks. In our case, it would be helpful to divide the recipe into the following three parts:
- Add the Maven dependency for Lombok to the project’s
- Place each variable declaration in its own statement and on its own line. This prevents the extra complications with multiple variable declarations on a single line.
- Add the Lombok
@Getterannotation to all fields that have a corresponding getter method. After adding the annotation, it should additionaly remove these getter methods from the class.
For the first two steps you can use pre-existing recipes from Openrewrite. The
AddDependency recipe can
add the Lombok dependency to the project if it does already exist. In addition, the
can simplify the multiple variable declarations. The final recipe has to be implemented by yourself.
As described in the previous blog post, yu can use the declarative approach to combine these
recipes into a single recipe. The
rewrite.yml file for the complete recipe should then look something like this:
Creating the project
To create this OpenRewrite recipe, you must first create a separate Java project. Recipes should be implemented in their own projects, so that they can be referenced as a dependency in the OpenRewrite Maven/Gradle plugin.
Since we want to modify the Java code and also update the Maven
pom.xml of our original codebase,
the dependencies section in our build file (in this case
build.gradle.kts) of the recipe-project
should look like this:
Defining an acceptance test
Before writing the recipes, it is very helpful to define one or more acceptance tests to make sure that they do what they are supposed to be doing. Fortunately, OpenRewrite provides a testing framework that allows you to easily test your recipes. All you have to do is pass it a snippet of source code and the expected code snippet and the library does the rest!
Now think of a class definition that covers all the constraints that need to be considered by the recipe. A good example of such a class would be:
After running the recipe, the class should be transformed like this:
Recipe-tests must implement the
org.openrewrite.test.RewriteTest interface. The
specific recipe to be tested
can be specified by overriding the
defaults(RecipeSpec spec) method.
spec parameter, you can either specify a
rewrite.yml file or instantiate the java class for the recipe.
Within the test method, you can run openrewrite by executing the
rewriteRun method, which accepts different implementations of
SourceSpec depending on the type of recipe you want to test. In this case we want to rewrite Java code, so we will use the
method. The acceptance test for our recipe will therefore look like this:
Writing the recipe for adding the Lombok @Getter annotation
OpenRewrite Recipes are defined as Java classes that extend the abstract base class
Let us first start with writing the recipe for adding the
@Getter annotation to fields having a getter method:
The core of the recipe logic lies in the implementation of the abstract base class
TreeVisitor, which is in most cases is
an implementation of the abstract
JavaIsoVisitor class (see the documentation for more details on this topic). The TreeVisitor has
methods for all possible visitors that can be defined for the LST of the source code file. When implementing the class it is
therefore important to know on which LST elements the visitor should act upon. In our case, we want to add the Lombok
annotation to a field of a class. Therefore it makes sense to implement the visitor method for variable declarations. The
structure of this implementation can be written as follows:
Now let us see how we can implement the individual methods. First, we
need to see how we can tell if a variable declaration
is actually a field declaration inside a class. A key aspect of a field is that it should be declared in a class and not inside
a method. Therefore, we can recognise a field in the LST if the parent node of a variable declaration is a class declaration. This can
be done by using the
getCursor() method. The cursor points to the currently visited element in the LST and provides methods to navigate to other elements in the LST.
To find out if the variable declaration already has the
@Getter annotation, we can directly use one of the available methods of
In order to find the getter methods for all the variables in the variable declaration, it would be very difficult to use our current
visitor - we would have to manually move the cursor through the LST and can not take advantage of the predefined visitor methods in
TreeVisitor. Therefore it is best to use a second visitor to find the methods we want. Fortunately, OpenRewrite provides many
“search” visitors for finding specific elements in an LST. In our case,
we can take advantage of the
FindMethods visitor. This visitor takes a method pattern as a search parameter and returns a set of
all matching method declarations in the provided LST (which in our case should be the LST of the enclosing class). The method pattern is defined as an AspectJ-like expression (see the documentation for more details). For example, if the name of the variable is
x, the method expression should be
"* isX()" if it is a boolean and
"* getX()" in all other cases (for simplicity, we ignore the return type here).
Finally, we are at the point where we can write the code to add the
@Getter annotation to the variable declaration!
To generate new code for the current LST, it is not advisable to write the LST-elements by hand and add them to the respective
objects. Instead, you should use the
JavaTemplate class, which can generate the LST-elements by parsing a snippet of code.
To parse the snippet correctly,
JavaTemplate should know the correct implementations of all the classes used in its definition.
JavaTemplate only knows the classes provided by the Java runtime. To add new classes to its scope, you can use its
javaParser(...) method to add the dependencies, for example by specifying the classpath location or by providing a stub implementation. Stubs are needed when the classes are not available on the runtime classpath. This is often the case when writing recipes for framework migrations, where only the older framework version is on the classpath. When defining stubs, they only need to have the bare minimum amount of declarations for the
JavaParser to work
(just enough to determine the necessary LST elements).
In our case, the
JavaTemplate should use a stub for the Lombok annotation,
as we do not have Lombok on our classpath.
addGetterAnnotation, which adds to annotation to the variable declaration,
then looks like this:
Removing the relevant getter methods from the class
As the final step in our Lombok refactoring recipe, we should extend our visitor
to remove all the getter methods for fields that have the
The perfect candidate for this callback method is the
method. The ClassDeclaration knows all the statements declared in the class,
such as variable and method declarations.
The steps that we should take to to remove the relevant getter methods are as follows:
- First, we should find all the fields that have the
- For each of these fields, we should find the corresponding getter methods
- Finally, we need to modify the ClassDeclaration, so that these getter methods are no longer included in its list of statements.
The code for the
visitClassDeclaration(...) will then look like this:
So why does this work at all? At first glance, it is not obvious why the code in
visitVariableDeclaration(...) that adds the
@Getter annotation, is executed before
the code in
visitClassDeclaration(...). So you might think that not much
should be happening here, asthe Lombok annotation could be added after the class declaration is visited!
The key point here, is the call to
super.visitClassDeclaration(...) at the beginning
of the method. This basically causes the visitor to traverse the subtree contained
in the class declaration and return the potentially modified subtree. Since variable declarations
are contained in the subtree of the class declaration, the result of this method call is a
modified LST with Lombok annotations on its fields. This in turn allows us to operate
on the class declaration in the way we need to.
By writing refactoring recipes yourself, you can really exploit the full potential of OpenRewrite to maintain your software projects. However, as you have seen above, it can be quite a daunting task to implement them by yourself. It is therefore best to rely as much as possible on using recipes provided by OpenWrite itself, or on collections of recipes written by framework authors. Fortunately, more and more frameworks are providing recipes for their dependency upgrades, so finding the right ones should be less of a problem in the future!
In this blog post, we have limited ourselves to the basic mechanics of writing recipes. There are many more (advanced) concepts in OpenRewrite that you can explore, such as
- Writing recipes over multiple source file types, e.g. to evaluate conditions in a property file before modifying a piece of Java code.
- Writing non-isomorphic Java visitors, for example, when the visitor method for a class declaration should return a different type of LST element.
- Sharing data between visitor methods by using markers or cursor messages.
- Defining a style for your source code so that the OpenRewrite recipes will format their results according to the rules in the style definition.
- And much more …
More details on these topics can be found in the documentation or in the source code of OpenRewrite on Github.
The code for the refactoring recipe created in this post can be found here on my Github account