Compile time processing using annotation processor

This article is an intro to Java source-level annotation processor and provides examples of using this technique for generating additional source files during compilation. This example demonstrates how to do compile time checking of an annotated element.

The annotation

The @Setter annotation is a marker can be applied to methods. The annotation will be discarded during compilation not be available afterwards.

package annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface Setter {
}

The annotation processor

The SetterProcessor class is used by the compiler to process the annotations. It checks, if the methods annotated with the @Setter annotation are public, non-static methods with a name starting with set and having a uppercase letter as 4th letter. If one of these conditions isn’t met, a error is written to the Messager. The compiler writes this to stderr, but other tools could use this information differently. E.g. the NetBeans IDE allows the user specify annotation processors that are used to display error messages in the editor.

package annotation.processor;
import annotation.Setter;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
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.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;

  @SupportedAnnotationTypes({"annotation.Setter"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class SetterProcessor extends AbstractProcessor {

     private Messager messager;
     @Override
     public boolean process(Set annotations, RoundEnvironment roundEnv) {
          // get elements annotated with the @Setter annotation
          Set annotatedElements = roundEnv.getElementsAnnotatedWith(Setter.class);

          for (Element element : annotatedElements) {
             if (element.getKind() == ElementKind.METHOD) {
                    // only handle methods as targets
                    checkMethod((ExecutableElement) element);
              }
           }
           // don't claim annotations to allow other processors to process them
           return false;
    }

    private void checkMethod(ExecutableElement method) {
        // check for valid name
        String name = method.getSimpleName().toString();
        if (!name.startsWith("set")) {
               printError(method, "setter name must start with \"set\"");
        } else if (name.length() == 3) {
               printError(method, "the method name must contain more than just \"set\"");
        } else if (Character.isLowerCase(name.charAt(3))) {
               if (method.getParameters().size() != 1) {
                     printError(method, "character following \"set\" must be upper case");
               }
        }

       // check, if setter is public
       if (!method.getModifiers().contains(Modifier.PUBLIC)) {
printError(method, "setter must be public");
       }
 
       // check, if method is static
       if (method.getModifiers().contains(Modifier.STATIC)) {
             printError(method, "setter must not be static");
        }
   }

   private void printError(Element element, String message) {
         messager.printMessage(Diagnostic.Kind.ERROR, message, element);
   }
   @Override
   public void init(ProcessingEnvironment processingEnvironment)        {
       super.init(processingEnvironment);

       // get messager for printing errors
       messager = processingEnvironment.getMessager();
   }     
}

Packaging

To be applied by the compiler, the annotation processor needs to be made available to the SPI (see ServiceLoader).

To do this a text file META INF/services/javax.annotation.processing.Processor needs to be added to the jar file containing the annotation processor and the annotation in addition to the other files. The file needs to include the fully qualified name of the annotation processor, i.e. it should look like this

annotation.processor.SetterProcessor

We’ll assume the jar file is called AnnotationProcessor.jar below.

Example annotated class

The following class is example class in the default package with the annotations being applied to the correct elements according to the retention policy. However only the annotation processor only considers the second method a valid annotation target.

import annotation.Setter;
public class AnnotationProcessorTest {

       @Setter
       private void setValue(String value) {}

       @Setter
       public void setString(String value) {}

       @Setter
       public static void main(String[] args) {}
}

Using the annotation processor with javac

If the annotation processor is discovered using the SPI, it is automatically used to process annotated elements. E.g. compiling the AnnotationProcessorTest class using

javac -cp AnnotationProcessor.jar AnnotationProcessorTest.java

yields the following output

AnnotationProcessorTest.java:6: error: setter must be public
private void setValue(String value) {}
^
AnnotationProcessorTest.java:12: error: setter name must start with "set"
public static void main(String[] args) {}
^
2 errors

instead of compiling normally. No .class file is created.

This could be prevented by specifying the -proc:none option for javac. You could also forgo the usual compilation by specifying -proc:only instead.

IDE integration
Netbeans

Annotation processors can be used in the NetBeans editor. To do this the annotation processor needs to be specified in the project settings:

  1. go to Project Properties > Build > Compiling
  2. add check marks for Enable Annotation Processing and Enable Annotation Processing in Editor
  3. click Add next to the annotation processor list
  4. in the popup that appears enter the fully qualified class name of the annotation processor and click Ok.

Result:

Annotations

Repeating Annotations

Until Java 8, two instances of the same annotation could not be applied to a single element. The standard workaround was to use a container annotation holding an array of some other annotation:

// Author.java
@Retention(RetentionPolicy.RUNTIME)
public @interface Author {
String value();
}
// Authors.java
@Retention(RetentionPolicy.RUNTIME)
public @interface Authors {
Author[] value();
}
// Test.java
@Authors({
@Author("Mary"),
@Author("Sam")
})
public class Test {
public static void main(String[] args) {
Author[] authors = Test.class.getAnnotation(Authors.class).value();
for (Author author : authors) {
System.out.println(author.value());
// Output:
// Mary
// Sam
}
}
}

Java 8 provides a cleaner, more transparent way of using container annotations, using the @Repeatable annotation. First we add this to the Author class:

@Repeatable(Authors.class)

This tells Java to treat multiple @Author annotations as though they were surrounded by the @Authors container. We can also use Class.getAnnotationsByType() to access the @Author array by its own class, instead of through its container:

@Author("Mary")
@Author("Sam")
public class Test {
public static void main(String[] args) {
Author[] authors = Test.class.getAnnotationsByType(Author.class);
for (Author author : authors) {
System.out.println(author.value());
// Output:
// Mary
// Sam
}
}
}

Inherited Annotations

By default class annotations do not apply to types extending them. This can be changed by adding the @Inherited annotation to the annotation definition

Example

Consider the following 2 Annotations:

@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface InheritedAnnotationType {
}
and
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface UninheritedAnnotationType {
}
If three classes are annotated like this:
@UninheritedAnnotationType
class A {
}
@InheritedAnnotationType
class B extends A {
}
class C extends B {
}

running this code

System.out.println(new A().getClass().getAnnotation(InheritedAnnotationType.class));
System.out.println(new B().getClass().getAnnotation(InheritedAnnotationType.class));
System.out.println(new C().getClass().getAnnotation(InheritedAnnotationType.class));
System.out.println("_____________________");
System.out.println(new A().getClass().getAnnotation(UninheritedAnnotationType.class));
System.out.println(new B().getClass().getAnnotation(UninheritedAnnotationType.class));
System.out.println(new C().getClass().getAnnotation(UninheritedAnnotationType.class));

will print a result similar to this (depending on the packages of the annotation):

null
@InheritedAnnotationType()
@InheritedAnnotationType()
@UninheritedAnnotationType()
null
null

Note that annotations can only be inherited from classes, not interfaces.

Getting Annotation values at run-time

You can fetch the current properties of the Annotation by using Reflection to fetch the Method or Field or Class which has an Annotation applied to it, and then fetching the desired properties.

@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation {
     String key() default "foo";
     String value() default "bar";
}

class AnnotationExample {
      // Put the Annotation on the method, but leave the defaults
     @MyAnnotation
     public void testDefaults() throws Exception {
           // Using reflection, get the public method "testDefaults", which is this method with no args
          Method method = AnnotationExample.class.getMethod("testDefaults", null);

          // Fetch the Annotation that is of type MyAnnotation from the    Method
          MyAnnotation annotation =   (MyAnnotation)method.getAnnotation(MyAnnotation.class);

         // Print out the settings of the Annotation
print(annotation);
    }

    //Put the Annotation on the method, but override the settings
    @MyAnnotation(key="baz", value="buzz")
    public void testValues() throws Exception {
        // Using reflection, get the public method "testValues", which is this method with no args
        Method method = AnnotationExample.class.getMethod("testValues", null);

       // Fetch the Annotation that is of type MyAnnotation from the Method
      MyAnnotation annotation = (MyAnnotation)method.getAnnotation(MyAnnotation.class);

      // Print out the settings of the Annotation
print(annotation);
     }
     public void print(MyAnnotation annotation) {
           // Fetch the MyAnnotation 'key' & 'value' properties, and print them out
           System.out.println(annotation.key() + " = " + annotation.value());
    }

    public static void main(String[] args) {
           AnnotationExample example = new AnnotationExample();
           try {
                example.testDefaults();
                example.testValues();
          } catch( Exception e ) {
            // Shouldn't throw any Exceptions
            System.err.println("Exception [" + e.getClass().getName() + "] - " + e.getMessage());
e.printStackTrace(System.err);
           }
     }
}

The output will be

foo = bar
baz = buzz
Annotations for ‘this’ and receiver parameters

When Java annotations were first introduced there was no provision for annotating the target of an instance method or the hidden constructor parameter for an inner classes constructor. This was remedied in Java 8 with addition of receiver parameter declarations; see JLS 8.4.1.

The receiver parameter is an optional syntactic device for an instance method or an inner class’s constructor. For an instance method, the receiver parameter represents the object for which the method is invoked. For an inner class’s constructor, the receiver parameter represents the immediately enclosing instance of the newly constructed object. Either way, the receiver parameter exists solely to allow the type of the represented object to be denoted in source code, so that the type may be annotated. The receiver parameter is not a formal parameter; more precisely, it is not a declaration of any kind of variable (§4.12.3), it is never bound to any value passed as an argument in a method invocation expression or qualified class instance creation expression, and it has no effect whatsoever at run time.

The following example illustrates the syntax for both kinds of receiver parameter:

public class Outer {
     public class Inner {
          public Inner (Outer this) {
              // …
          }
      public void doIt(Inner this) {
           // …
          }
      }
}

The sole purpose of receiver parameters is to allow you to add annotations. For example, you might have a custom annotation @IsOpen whose purpose is to assert that a Closeable object has not been closed when a method is
called. For example:

public class MyResource extends Closeable {
    public void update(@IsOpen MyResource this, int value) {
    // …
    }
     public void close() {
           // …
     }
}

At one level, the @IsOpen annotation on this could simply serve as documentation. However, we could potentially do more. For example:

  • An annotation processor could insert a runtime check that this is not in closed state when update is called.
  • A code checker could perform a static code analysis to find cases where this could be closed when update is called.
Add multiple annotation values

An Annotation parameter can accept multiple values if it is defined as an array. For example the standard annotation @SuppressWarnings is defined like this:

public @interface SuppressWarnings {
String[] value();
}

The value parameter is an array of Strings. You can set multiple values by using a notation similar to Array initializers:

@SuppressWarnings({"unused"})
@SuppressWarnings({"unused", "javadoc"})

If you only need to set a single value, the brackets can be omitted:

@SuppressWarnings("unused")

LEAVE A REPLY

Please enter your comment!
Please enter your name here