Java Annotation Processors – Generating Code
This article is part 3 of the series „Java Annotation Processing“
Read the first part now.
In the third and final blog post of this series, we will demonstrate how you can generate source code with the help of an annotation processor, while in the intro part we have learned how to write, register and use a simple Annotation Processor and in the second part we created configurations.
Generating source code
In our example, we wish to generate an additional JsonWriter
class for each class annotated with a @JsonObject
annotation. The generated JsonWriter
classes should create Json annotations for all Getter
methods of the annotated class. This makes it possible to serialize the annotated classes into Json format. In concrete terms, the class Person
@JsonObject
public class Person {
private String username;
private String email;
public Person(String username, String email) {
this.username = username;
this.email = email;
}
// getter
}
Should automatically create a PersonJsonWriter
class, which looks like this:
public final class PersonJsonWriter {
public static String toJson(Person object) {
StringBuilder builder = new StringBuilder("{");
builder.append("\"class\": \"");
builder.append(object.getClass())
builder.append("\",");
builder.append("\"username\": \"");
builder.append(object.getUsername());
builder.append("\",");
builder.append("\"email\": \"");
builder.append(object.getEmail());
builder.append("\"");
return builder.append("}").toString();
}
}
It would also be possible to add a toJson
method to the Person
class, but that would involve a much longer article, as we would have to parse the original class to do this.
Finding annotated classes
First, we must find all classes annotated with the JsonObject
annotation. This involves the same process as used in the first two sections, so we can skip the code listing this time. Subsequently, we must generate a Scope
object for each class found, which we will later feed into a template engine.
public final class Scope {
private String packageName;
private String sourceClassName;
private List<Field> fields = new ArrayList<>();
Scope(String packageName, String sourceClassName) {
this.packageName = packageName;
this.sourceClassName = sourceClassName;
}
void addGetter(String getter) {
String fieldName = getter.substring(3);
char firstChar = fieldName.charAt(0);
fieldName = Character.toLowerCase(firstChar) + fieldName.substring(1);
fields.add(new Field(fieldName, getter));
}
// getter
public static class Field {
private String name;
private String getter;
private Field(String name, String getter) {
this.name = name;
this.getter = getter;
}
// getter
}
}
For the Scope
object, we need the name of the annotated class and its package. In order to obtain the name of the package, we must first ensure that our annotated element is a TypeElement
:
if (element instanceof TypeElement) {
}
If this is the case, we can query the TypeElement
for its superordinate element, which we can then ask for its name:
private String getPackageName(TypeElement classElement) {
return ((PackageElement) classElement.getEnclosingElement()).getQualifiedName().toString();
}
Now, we only need the names of all getter methods for our Scope object. To do this, we can use the ElementUtils
of the ProcessingEnvironment
:
processingEnv.getElementUtils().getAllMembers(typeElement)
The getAllMembers
method returns a list of all member elements in our class. From this list, we now only have to filter out all METHOD
type elements whose names start with “get.” The Java Collection’s Stream API, introduced with Java 8, is very well suited for this.
processingEnv.getElementUtils().getAllMembers(typeElement)
.stream()
.filter(el -> el.getKind() == ElementKind.METHOD)
.map(el -> el.getSimpleName().toString())
.filter(name -> name.startsWith("get"))
.collect(Collectors.toList());
Line-by-line, the listing defines the following:
- Find all member elements
- Convert the list into a stream
- Remove all elements that are not of the method type
- Extract the name of the element
- Remove all names not starting with “get”
- Create another list from the stream
Now, we have collected all of the information we need to create the JsonWriter
.
Writing the JsonWriter
To write the JsonWriter
, the Filer from the ProcessingEnvironment
can be used again:
Filer filer = processingEnv.getFiler();
JavaFileObject fileObject = filer.createSourceFile(scope.getTargetClassNameWithPackage(), element);
The desired class name and the annotated element must be passed to the createSourceFile
method in order to obtain a JavaFileObject
. With this JavaFileObject
, you can then open a Writer:
Writer writer = fileObject.openWriter();
This Writer
then writes a Java file to the package folder in the classpath (with Maven, classes generated by annotation processors are stored under target/generated-sources/annotations
).
We could now write the source code directly using the Writer
, but it’s easy to lose track due to the escaping of inverted commas.
Another option for generating the source code from the Scope
object is JavaPoet. JavaPoet offers a Java Builder API to generate Java files. Explaining how to use JavaPoet falls outside the scope of this article, so we will make do with a simple template engine for our example.
We will use the Java Implementation of the Mustache template engine. Mustache templates have a very simple structure, and the syntax is easy to learn.
In order to understand our example, it is enough to know that using * with the expression {{sourceClassName}}
accesses the getSourceClassName
getter method of the Scope object, that using * with the {{#fields}}
…{{/fields}}
command iterates over the collection of the field variables of the Scope object, and that * {{^last}}
…{{/last}}
checks whether or not the field is the last element in the collection.
package {{packageName}};
public final class {{targetClassName}} {
public static String toJson({{sourceClassName}} object) {
StringBuilder builder = new StringBuilder("{");
{{#fields}}
builder.append("\"{{value.name}}\": \"");
builder.append(object.{{value.getter}}());
builder.append("\"{{^last}},{{/last}}");
{{/fields}}
return builder.append("}").toString();
}
}
With the following code, the Mustache template is read from the classpath, executed with the Scope
object, and written to the writer of the JavaFileObject:
MustacheFactory factory = new DefaultMustacheFactory();
Template template = factory.compile("com/cloudogu/blog/jsonwriter.mustache");
template.execute(writer, scope);
Now, we have everything we need to generate the PersonJsonWriter
. To do this, we compile the Person class annotated with @Json
in the classpath, using our annotation processor. Then, we should be able to find the PersonJsonWriter
class in the target/classes
directory. We can use the class as follows:
Person person = new Person("tricia", "tricia.mcmillian@hitchhicker.com");
String json = PersonJsonWriter.toJson(person);
System.out.println(json);
The listing shown above should provide the following Json string:
{
"class": "class com.cloudogu.blog.Person",
"username": "tricia",
"email": "tricia.mcmillian@hitchhicker.com"
}
Open source examples
Key examples of annotation processors that generate source code include the following:
- Hibernate Metamodel Generator generates a metamodel from JPA entities, in order to use the JPA Criteria API in a typesafe manner.
- QueryDSL offers a concept for formulating queries for Java entities in SQL-like languages. Here, annotation processors are used to generate the API for querying the entities.
- With a host of annotations, Project Lombok promises automatic generation of boilerplate code for Java classes, e.g. getter, setter,
hashCode
orequals
methods.
Visit our community platform to share your ideas with us, download resources and access our trainings.
Join us nowConclusion
The examples shown in our blog post series here clearly demonstrate that annotation processors can be a very powerful tool. They can generate source code and configuration files, as if by magic. However, with great power comes great responsibility. When only considering the source code of a project, the generated files will be missing. These only show up after compilation, and even then, you can’t tell at first glance where the generated files have come from or how they were generated. It is therefore recommended to make a note in the project documentation explaining that the files are generated during compilation. In addition, the comments on the generated files should include a reference to the annotation processor used to generated them. For Java source code, there is also a specific annotation for generated classes (@Generated), which can be used to provide information on the classes’ origins. All of the examples and source code featured in this article are available at GitHub under the MIT license.
This article is part 3 of the series „Java Annotation Processing“.
Read all articles now: