Cloudogu Logo

Hello, we are Cloudogu!

Experts in Software Lifecycle Management and process auto­mation, supporter of open source soft­ware and developer of the Cloudogu EcoSystem.

featured image Java Annotation Processors – Generating Code
10/01/2018 in Technology

Java Annotation Processors – Generating Code


Sebastian Sdorra
Sebastian Sdorra

Software Developer


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

Visit our community platform to share your ideas with us, download resources and access our trainings.

Join us now
Cloudogu Platform Logo

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

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

This article is part 3 of the series „Java Annotation Processing“.
Read all articles now: