This simple java library aims add providing some convenience when implementing javac compiler extensions like annotation processors or compiler plugins.

Writing Annotation Processors

There are some helpful resources available concerning how to write an annotation processor. For a more in depth understanding on how annotation processors work, please study these. Here only a rough overview is given on how to implement a java annotation processor:

@SupportedAnnotationTypes({"org.example.MyAnnotationClass", "org.example.MyOtherAnnotation"}) (2)
public class MyProcessor extends AbstractProcessor { (1)


  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { (3)

        for (TypeElement annotationElement : annotations) { (4)

            Set<? extends Element> annotatedElements =
                roundEnvironment.getElementsAnnotatedWith(annotationElement); (5)

            for (Element element : annotatedElements) { (6)

                ... (7)
            }
        }
  }
}
1 The processor extends from javax.annotation.processing.AbstractProcessor.
2 It specifies the list of annotation it registers for using the annotation javax.annotation.processing.SupportedAnnotationTypes
3 It implements the method process, which will get a set of annotations that were found as first parameter.
4 Typically, the processor now iterates over the annotations that were found.
5 Then, it obtains the source code elements that were annotated with one given annotation.
6 After that, it iterates over the elements found…​
7 …​ to execute some custom code

Using Javac Extension Utilities

Using this utility this can be simplified by deriving from io.github.miracelwhipp.javac.extension.annotation.processor.ReflectiveAnnotationProcessor. In such a processor, one simply needs to create element handler methods which will be called for annotated elements automatically. An element handler method is a method

  • annotated with io.github.miracelwhipp.javac.extension.annotation.processor.ElementHandler

  • that has 2 arguments

    • first, an annotation instance of the annotation that is handled

    • second a subtype of javax.lang.model.element.Element (or Element itself) where this annotation is expected

The return value of this method will be ignored, so it is good practice to have none. All element handlers will be called once for every annotation instance and element that it is registered to.

Example:

Let’s write an annotation processor that gives a warning, whenever he encounters a given annotation. Assume the following annotation:

package io.github.miracelwhipp.annotation.processor.test;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD}) (1)
public @interface Warning {

    String value(); (2)
}
1 The annotation is applicable to methods and fields, those are the kind of elements we want to warn about.
2 The annotation has a string member. It is the value of the warning we want to give, when an element that is annotated with it is encountered.

Using element handlers we can now implement an annotation processor

package io.github.miracelwhipp.annotation.processor.test;

import io.github.miracelwhipp.javac.extension.annotation.processor.ElementHandler;
import io.github.miracelwhipp.javac.extension.annotation.processor.ReflectiveAnnotationProcessor;

import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.VariableElement;
import javax.tools.Diagnostic;

public class WarningAnnotationProcessor extends ReflectiveAnnotationProcessor { (1)

    @ElementHandler (2)
    public void warnMethod(Warning annotation, ExecutableElement element) {

        processingEnv.getMessager().printMessage(Diagnostic.Kind.MANDATORY_WARNING, annotation.value(), element);
    }
    @ElementHandler (3)
    public void warnField(Warning annotation, VariableElement element) {

        (4)
        processingEnv.getMessager().printMessage(Diagnostic.Kind.MANDATORY_WARNING, annotation.value(), element);
    }
}
1 The annotation processor extends io.github.miracelwhipp.javac.extension.annotation.processor.ReflectiveAnnotationProcessor which in turn derives from AbstractProcessor. Thus, the functionality AbstractProcessor provides is still available.
2 The processor defines a method warnMethod that is annotated with io.github.miracelwhipp.javac.extension.annotation.processor.ElementHandler. Since its first parameter is of the type Warning it will be called for elements annotated with Warning. Since its second parameter is of the type ExecutableElement it will only be called for those elements (i.e. methods, constructor, initializer blocks etc.), however, since the annotation can only be applied to methods and fields, this method will only be called for elements that represent methods.
3 A second element handler is defined, that will be called for VariableElements (i.e. variables, fields, parameters etc.). Since the annotation is only applicable to methods and fields, the method will only be called for fields.
4 Both methods use the processingEnv provided by AbstractProcessor to get a Messager object which can be used to print a warning. The message to print is simply obtained from the annotation itself.
Note that the processor in this example could be implemented in a simpler way, only defining one element handler that gets an Element as second parameter.

Completion Handlers

A commons use case for annotation processors is to analyze annotated elements while the compiler runs and compute or returns something dependent of this analysis. In order to do this, the annotation processor needs to do something at the end of the compile process. This can be done by annotating a member method that does not expect any parameters with io.github.miracelwhipp.javac.extension.annotation.processor.CompletionHandler. This method will be called after all when the compilation finishes.

Example:

Let’s extend the warning processor so that the warnings will be collected and written to a file after the compilation:

package io.github.miracelwhipp.annotation.processor.test;

import io.github.miracelwhipp.javac.extension.annotation.processor.ElementHandler;
import io.github.miracelwhipp.javac.extension.annotation.processor.ReflectiveAnnotationProcessor;
import io.github.miracelwhipp.javac.extension.annotation.processor.CompletionHandler;

import javax.lang.model.element.Element;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import java.io.Writer;
import java.io.IOException;

public class WarningAnnotationProcessor extends ReflectiveAnnotationProcessor {


    private StringBuilder builder = new StringBuilder(); (1)

    @ElementHandler
    public void warnMethod(Warning annotation, Element element) { (2)

        builder.append(annotation.value()).append("\n"); (3)
    }

    @CompletionHandler
    public void finish() { (4)

        try {

            FileObject resource = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", "warnings.txt");

            try (Writer writer = resource.openWriter()) {

                writer.append(builder.toString());
            }

        } catch (IOException e) {

            throw new RuntimeException(e);
        }
    }
}
1 The class specifies a string builder, to which all the warnings will be written.
2 As noted above, we can implement the annotation parsing part in one single method.
3 When a warning annotation is encountered its value will simply be added to the string builder (followed by a new line).
4 A single non argument void method is marked as completion handler. In it the processing environments filer is used to create a new file in the classes directory with the name warnings.txt, the content of the string builder is written to it

Configuration Parameters

Annotation processor may receive configuration parameters that are given to the compiler in the form -A<parameter-name>=<value>. To obtain a configuration parameter an instance field of the annotation processor can be marked with io.github.miracelwhipp.javac.extension.annotation.processor.Parameter. The annotation specifies the name of the parameter and optionally a default value. The type of such a field must be string or a primitive type.

Example:

Let’s extend the warning processor further, so that the file to write to can be configured.

package io.github.miracelwhipp.annotation.processor.test;

import io.github.miracelwhipp.javac.extension.annotation.processor.ElementHandler;
import io.github.miracelwhipp.javac.extension.annotation.processor.ReflectiveAnnotationProcessor;
import io.github.miracelwhipp.javac.extension.annotation.processor.CompletionHandler;
import io.github.miracelwhipp.javac.extension.configuration.Parameter;

import javax.lang.model.element.Element;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import java.io.Writer;
import java.io.IOException;

public class WarningAnnotationProcessor extends ReflectiveAnnotationProcessor {

    @Parameter(name = "target.file", defaultValue = "warnings.txt") (1)
    private String targetFile;

    private StringBuilder builder = new StringBuilder();

    @ElementHandler
    public void warnMethod(Warning annotation, Element element) {

        builder.append(annotation.value()).append("\n");
    }

    @CompletionHandler
    public void finish() {

        try {

            FileObject resource = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", targetFile); (2)

            try (Writer writer = resource.openWriter()) {

                writer.append(builder.toString());
            }

        } catch (IOException e) {

            throw new RuntimeException(e);
        }
    }
}
1 A member variable targetFile of type string is defined. It is annotated as parameter. Its default value is still "warnings.txt" so if the parameter is not configured the behaviour will still be the same.
2 The warnings are written to a file with the name configured.

Writing Compiler Plugins

Compiler plugins typically add a list of task listeners to the current java task. Using this utility task listeners can simply be registered using the annotation io.github.miracelwhipp.javac.extension.compiler.plugin.JavaCompilerTaskListener in compiler plugins derived from io.github.miracelwhipp.javac.extension.compiler.plugin.ReflectiveCompilerPlugin.

@JavaCompilerTaskListener(MyTaskListener.class)
@JavaCompilerTaskListener(MyOtherTaskListener.class)
public class MyCompilerPlugin extends ReflectiveCompilerPlugin {

}

The plugins name can be specified with the annotation io.github.miracelwhipp.javac.extension.compiler.plugin.PluginName:

@PluginName("my-plugin")
@JavaCompilerTaskListener(MyTaskListener.class)
@JavaCompilerTaskListener(MyOtherTaskListener.class)
public class MyCompilerPlugin extends ReflectiveCompilerPlugin {

}

A task listener can be implemented deriving from io.github.miracelwhipp.javac.extension.compiler.plugin.ReflectiveJavaCompilerTaskListener. I order to listen to a task event simply declare a void method accepting one parameter of the type TaskEvent, annotated either with io.github.miracelwhipp.javac.extension.compiler.plugin.Before or io.github.miracelwhipp.javac.extension.compiler.plugin.After. Both annotation specify a TaskEvent.Kind determining the compile step before or after which the method will be called.

public class MyTaskListener extends ReflectiveJavaCompilerTaskListener {

    @Before(TaskEvent.Kind.PARSE) (1)
    public void beforeParsing(TaskEvent event) {

        // ...
    }

    @After(TaskEvent.Kind.PARSE) (2)
    public void afterAnalyze(TaskEvent event) {

        // ...
    }

}
1 the method beforeParsing will be called before the compiler parses a source file
2 the method afterAnalyze will be called after the compiler analyzed a source file