Annotations Part II: Using Custom Annotation Processors

This tutorial needs a review. You can edit it in GitHub following these contribution guidelines.

The previous part of the annotations tutorial (Part I: Using Lombok for Custom Annotations) showed how custom annotations work within NetBeans.

In this section of the tutorial, you will learn how to add a self-written custom annotation processor to a project in the IDE. This tutorial does not teach you how to write an annotation processor. It explains how to add it to a NetBeans IDE project.

The sample application used in this section was created by Jesse Glick and published as an FAQ entry for the previous IDE releases.

The annotation processor used as the example generates a parent class for the annotated class. The generated parent class also contains a method that is called from the annotated class. Follow the instructions below on how to create and add a custom annotation processor to an IDE’s project.

Requirements

To complete this tutorial, you need the following software and resources.

Software or Resource Version Required

NetBeans IDE

9.0 or greater

Java Development Kit (JDK)

version 6 or greater

lombok.jar

v1.12.4 or newer

Defining an Annotation and Creating an Annotation Processor

In this exercise you will create a class library project.

  1. Choose File > New Project and select the Java Class Library project type in the Java category. Click Next.

  2. Type AnnProcessor as the Project Name and specify a location for the project. Click Finish.

When you click Finish, the IDE creates the class library project and lists the project in the Projects window.

  1. Right-click the AnnProcessor project node in the Projects window and choose Properties.

  2. In the Sources category, confirm that either JDK 6 or JDK 7 are specified as the source/binary format.

  3. Select the Libraries tab and confirm that the Java platform is set to either JDK 1.6 or JDK 1.7. Click OK to close the Project Properties window.

In this exercise you will create two Java packages and one Java class in each of the packages.

  1. Right-click the Source Packages node under the AnnProcessor project node and choose New > Java Package.

  2. Type ann for the Package Name and click Finish to create the new Java package.

  3. Repeat the two previous steps to create a Java package named proc.

After you create the two Java packages, the structure of the project should be similar to the following image.

packages
Figure 1. The structure of the project for the annotation processor.
  1. Right-click the ann Java package and choose New > Java class.

  2. Type Handleable for the Class Name. Click Finish.

  3. Modify the new Handleable.java file to make the following changes. Save the file.

package ann;

public @interface Handleable {

}

This is how annotations are declared, and it is quite similar to an interface declaration. The difference is that the interface keyword must be preceded with an at sign (@). This annotation is called Handleable .

In annotation declarations, you can also specify additional parameters, for example, what types of elements can be annotated, e.g. classes or methods. You do this by adding @Target(value = {ElementType.TYPE}) for classes and @Target(value = {ElementType.METHOD}). So, the annotation declaration becomes annotated itself with meta-annotations.

You now need to add code for the annotation processor to process the Handleable annotation.

  1. Right-click the proc Java package and choose New > Java class.

  2. Type HandleableProcessor for the Class Name. Click Finish.

  3. Modify the HandleableProcessor.java class to add the following code. Save your changes.

The value of @SupportedSourceVersion will depend upon the version of the JDK that you are using and will be either (SourceVersion.RELEASE_7) or (SourceVersion.RELEASE_6) .
package proc;

import ann.Handleable;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;

@SupportedAnnotationTypes("ann.Handleable")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class HandleableProcessor extends AbstractProcessor {

    /** public for ServiceLoader */
    public HandleableProcessor() {
    }

    public boolean process(Set<? extends TypeElement> annotations,
            RoundEnvironment roundEnv) {
        for (Element e : roundEnv.getElementsAnnotatedWith(Handleable.class)) {
            if (e.getKind() != ElementKind.FIELD) {
                processingEnv.getMessager().printMessage(
                        Diagnostic.Kind.WARNING,
                        "Not a field", e);
                continue;
            }
            String name = capitalize(e.getSimpleName().toString());
            TypeElement clazz = (TypeElement) e.getEnclosingElement();
            try {
                JavaFileObject f = processingEnv.getFiler().
                        createSourceFile(clazz.getQualifiedName() + "Extras");
                processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,
                        "Creating " + f.toUri());
                Writer w = f.openWriter();
                try {
                    PrintWriter pw = new PrintWriter(w);
                    pw.println("package "
                            + clazz.getEnclosingElement().getSimpleName() + ";");
                    pw.println("public abstract class "
                            + clazz.getSimpleName() + "Extras {");
                    pw.println("    protected " + clazz.getSimpleName()
                            + "Extras() {}");
                    TypeMirror type = e.asType();
                    pw.println("    /** Handle something. */");
                    pw.println("    protected final void handle" + name
                            + "(" + type + " value) {");
                    pw.println("        System.out.println(value);");
                    pw.println("    }");
                    pw.println("}");
                    pw.flush();
                } finally {
                    w.close();
                }
            } catch (IOException x) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
                        x.toString());
            }
        }
        return true;
    }

    private static String capitalize(String name) {
        char[] c = name.toCharArray();
        c[0] = Character.toUpperCase(c[0]);
        return new String(c);
    }
}

Let’s take a closer look at the main parts that constitute the code for the annotation processor (note that for convenience, only parts of the code are provided).

At first, you specify the annotation types that the annotation processor supports (by using @SupportedAnnotationTypes ) and the version of the source files that are supported (by using @SupportedSourceVersion ), in this case the version is JDK 6:

@SupportedAnnotationTypes("ann.Handleable")
@SupportedSourceVersion(SourceVersion.RELEASE_6)

Then, you declare a public class for the processor that extends the AbstractProcessor class from the javax.annotation.processing package. AbstractProcessor is a standard superclass for concrete annotation processors that contains necessary methods for processing annotations.

public class HandleableProcessor extends AbstractProcessor {
...
}

You now need to provide a public constructor for the class.

public class HandleableProcessor extends AbstractProcessor {
  public HandleableProcessor() {
  }
...

}

Then, you call the process () method of the parent AbstractProcessor class. Through this method the annotations available for processing are provided. In addition, this method contains information about the round of processing.

public class HandleableProcessor extends AbstractProcessor {
   ...
   public boolean process(Set<? extends TypeElement> annotations,
            RoundEnvironment roundEnv) {
     ...
     }
}

The annotation processor’s logic is contained within the process() method of the AbstractProcessor class. Note that through AbstractProcessor , you also access the ProcessingEnvironment interface, which allows annotation processors to use several useful facilities, such as Filer (a filer handler that enables annotation processors to create new files) and Messager (a way for annotation processors to report errors).

public class HandleableProcessor extends AbstractProcessor {
   ...
     public boolean process(Set<? extends TypeElement> annotations,
            RoundEnvironment roundEnv) {//For each element annotated with the Handleable annotation
            for (Element e : roundEnv.getElementsAnnotatedWith(Handleable.class)) {

// Check if the type of the annotated element is not a field. If yes, return a warning.
if (e.getKind() != ElementKind.FIELD) {
processingEnv.getMessager().printMessage(
Diagnostic.Kind.WARNING,
"Not a field", e);
continue;
}
            //Define the following variables: name and clazz.
String name = capitalize(e.getSimpleName().toString());
TypeElement clazz = (TypeElement) e.getEnclosingElement();
//Generate a source file with a specified class name.
            try {
JavaFileObject f = processingEnv.getFiler().
createSourceFile(clazz.getQualifiedName() + "Extras");
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,
"Creating " + f.toUri());
Writer w = f.openWriter();
//Add the content to the newly generated file.
                    try {
PrintWriter pw = new PrintWriter(w);
pw.println("package "
+ clazz.getEnclosingElement().getSimpleName() + ";");
pw.println("public abstract class "
+ clazz.getSimpleName() + "Extras {");
pw.println("    protected " + clazz.getSimpleName()
+ "Extras() {}");
TypeMirror type = e.asType();
pw.println("    /** Handle something. */");
pw.println("    protected final void handle" + name
+ "(" + type + " value) {");
pw.println("        System.out.println(value);");
pw.println("    }");
pw.println("}");
pw.flush();
} finally {
w.close();
}
} catch (IOException x) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
x.toString());
}
}return true;
     }
...
}

The last block in this code declares the capitalize method that is used to capitalize the name of the annotated element.

public class HandleableProcessor extends AbstractProcessor {
   ...

  private static String capitalize(String name) {
    char[] c = name.toCharArray();
    c[0] = Character.toUpperCase(c[0]);
    return new String(c);
  }
}
  1. Build the project by right-clicking the AnnProcessor project and choosing Build.

Using the Annotation Processor in the IDE

In this section you will create a Java Application project in which the annotation processor will be used.

  1. Choose File > New Project and select the Java Application project type in the Java category. Click Next.

  2. In the Name and Location page, type Demo as the Project Name and specify the project location.

  3. Type demo.Main in the Create Main Class field. Click Finish.

demo project wizard
Figure 2. Creating the Demo project in the New Project wizard.
  1. Open the Project Properties window and confirm that either JDK 6 or JDK 7 are selected as the source/binary format in the Sources panel and that the Java platform is set to JDK 1.6 or JDK 1.7 in the Libraries panel.

  2. Modify the Main.java class to add the following code. Save your changes.

package demo;

import ann.Handleable;

public class Main extends MainExtras {

    @Handleable
    private String stuff;

    public static void main(String[] args) {
        new Main().handleStuff("hello");
    }
}

This code contains the following elements:

  • import statement for the custom annotation processor ann.Handleable

  • the public class Main that extends the MainExtras class ( MainExtras should be generated by the annotation processor during compilation)

  • a private field named stuff that is annotated with the @Handleable annotation

  • the main method that calls the handleStuff method, which is declared in the automatically generated MainExtras class

In this simple example, the handleStuff method only prints out the current value. You can modify this method to perform other tasks.

After you save the Main.java code you will see that the IDE reports multiple compilation errors. This is because the annotation processor has not been added yet to the project.

  1. Right-click the Demo project node in the Projects window, choose Properties, then select the Libraries category in the Project Properties window.

  2. In the Compile tab, click Add Project and locate the AnnProcessor project.

demo properties compile
Figure 3. Compile tab in Libraries category of the project’s Properties window

The Compile tab corresponds to the -classpath option of the Java compiler. Because the annotation processor is a single JAR file that contains both the annotation definition and the annotation processor, you should add it to the project’s classpath, which is the Compile tab.

  1. Select the Compiling category in the Project Properties window and select the Enable Annotation Processing and Enable Annotation Processing in Editor checkboxes.

  2. Specify the annotation processor to run by click the Add button next to the Annotation Processors text area and typing * proc.HandleableProcessor * in the Annotation Processor FQN field.

demo processor fqn
Figure 4. Annotation Processor FQN dialog box

The Compiling category in the Project Properties window should look like the following image.

demo properties compiling
Figure 5. Compiling category in the project’s Properties window
  1. Click OK in the Properties window.

In the Main.java file you might still see compilation errors. This is because the IDE cannot yet find the MainExtras.java file that declares the handleStuff method. The MainExtras.java file will be generated after you build the Demo project for the first time. If Compile On Save is enabled for you project, the IDE compiled the project when you saved Main.java .
  1. Right-click the Demo project and choose Build.

After you build the project, if you look at the project in the Projects window you can see a new Generated Sources node with the demo/MainExtras.java file.

demo generated sources
Figure 6. Projects window with Generated Sources

If you review the contents of the generated MainExtras.java file, you can see that the annotation processor generated the MainExtras class with the handleStuff method. The handleStuff method is the one invoked from the annotated Main.java file.

package demo;
public abstract class MainExtras {
    protected MainExtras() {}
    /** Handle something. */
    protected final void handleStuff(java.lang.String value) {
        System.out.println(value);
    }
}
  1. Right-click the Demo project and choose Run.

When you click Run you should see the following in the Output window. The Demo project compiles and prints the message.

demo run
Figure 7. Projects window with Generated Sources

See Also

See the following resources for more information about annotations in Java applications: