featured image Java Annotation Processors - Generating Code

October 01, 2018 / by Sebastian Sdorra / In Software Craftsmanship

Java Annotation Processors - Generating Code

+++ If you want to read the post in German, you can download the original article, published in JAVA PRO 02/2018. +++

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 or equals methods.

Conclusion

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’ rigins.
All of the examples and source code featured in this article are available at GitHub under the MIT license.


Sebastian Sdorra
Sebastian Sdorra

- Software Development -

As expert for OpenSource and DevOps, Automatation is his passion – no matter if it is IT-infrastructure, software deployments or even coffee makers. Sebastian works as head developer on Cloudogu Ecosystem and the SCM-Manager and an expert with the mission to make other developers life easier.