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 |
|---|---|
9.0 or greater |
|
version 6 or greater |
|
v1.12.4 or newer |
Defining an Annotation and Creating an Annotation Processor
In this exercise you will create a class library project.
-
Choose File > New Project and select the Java Class Library project type in the Java category. Click Next.
-
Type
AnnProcessoras 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.
-
Right-click the AnnProcessor project node in the Projects window and choose Properties.
-
In the Sources category, confirm that either JDK 6 or JDK 7 are specified as the source/binary format.
-
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.
-
Right-click the Source Packages node under the AnnProcessor project node and choose New > Java Package.
-
Type
annfor the Package Name and click Finish to create the new Java package. -
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.
Figure 1. The structure of the project for the annotation processor. -
Right-click the
annJava package and choose New > Java class. -
Type
Handleablefor the Class Name. Click Finish. -
Modify the new
Handleable.javafile 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
interfacekeyword must be preceded with anatsign (@). This annotation is calledHandleable.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
Handleableannotation. -
Right-click the
procJava package and choose New > Java class. -
Type
HandleableProcessorfor the Class Name. Click Finish. -
Modify the
HandleableProcessor.javaclass to add the following code. Save your changes.The value of @SupportedSourceVersionwill 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
AbstractProcessorclass from thejavax.annotation.processingpackage.AbstractProcessoris 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 parentAbstractProcessorclass. 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 theAbstractProcessorclass. Note that throughAbstractProcessor, you also access theProcessingEnvironmentinterface, 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
capitalizemethod 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); } } -
Build the project by right-clicking the
AnnProcessorproject 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.
-
Choose File > New Project and select the Java Application project type in the Java category. Click Next.
-
In the Name and Location page, type
Demoas the Project Name and specify the project location. -
Type
demo.Mainin the Create Main Class field. Click Finish.
Figure 2. Creating the Demo project in the New Project wizard. -
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.
-
Modify the
Main.javaclass 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
Mainthat extends theMainExtrasclass (MainExtrasshould be generated by the annotation processor during compilation) -
a private field named
stuffthat is annotated with the@Handleableannotation -
the
mainmethod that calls thehandleStuffmethod, which is declared in the automatically generatedMainExtrasclassIn this simple example, the
handleStuffmethod only prints out the current value. You can modify this method to perform other tasks.After you save the
Main.javacode you will see that the IDE reports multiple compilation errors. This is because the annotation processor has not been added yet to the project.
-
-
Right-click the
Demoproject node in the Projects window, choose Properties, then select the Libraries category in the Project Properties window. -
In the Compile tab, click Add Project and locate the
AnnProcessorproject.
Figure 3. Compile tab in Libraries category of the project’s Properties windowThe Compile tab corresponds to the
-classpathoption 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. -
Select the Compiling category in the Project Properties window and select the Enable Annotation Processing and Enable Annotation Processing in Editor checkboxes.
-
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.
Figure 4. Annotation Processor FQN dialog boxThe Compiling category in the Project Properties window should look like the following image.
Figure 5. Compiling category in the project’s Properties window -
Click OK in the Properties window.
In the Main.javafile you might still see compilation errors. This is because the IDE cannot yet find theMainExtras.javafile that declares thehandleStuffmethod. TheMainExtras.javafile 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 savedMain.java. -
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 Sourcesnode with thedemo/MainExtras.javafile.
Figure 6. Projects window with Generated SourcesIf you review the contents of the generated
MainExtras.javafile, you can see that the annotation processor generated theMainExtrasclass with thehandleStuffmethod. ThehandleStuffmethod is the one invoked from the annotatedMain.javafile.package demo; public abstract class MainExtras { protected MainExtras() {} /** Handle something. */ protected final void handleStuff(java.lang.String value) { System.out.println(value); } } -
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.
Figure 7. Projects window with Generated Sources
See Also
See the following resources for more information about annotations in Java applications:
-
The previous part of the annotations tutorial: Part I: Using Lombok for Custom Annotations).
-
Java SE Documentation - Annotations
-
Java SE Tutorial - Annotations
-
Joseph D. Darcy’s Weblog - useful tips from the JSR-269 specification lead