Friday, December 25, 2015

Annotation processing during compilation time: Getting Class annotation value

An annotation type could have the following annotation value type:
  • Primitive type
  • String
  • Class
  • Enum
  • Another Annotation
  • An array of any of the above
While this post shows the different ways of getting annotation value during annotation processing at compile time, however, the spotlight is on getting the Class value. In all the following annotation processing code example, they are doing the same thing which to getting all the Tag annotation values specified in Application class.

Annotation type:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Tag {
    
    enum Kind {
        PRICE, STANDARD
    }
    
    String stringValue() default "";
    int intValue() default 0;
    Kind enumValue() default Kind.PRICE;
    Priority annotationTypeValue() default @Priority(0);
    Class classValue() default Void.class;
    Class[] classesValue() default {};
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Priority {
    int value();
}

Application class:

@Tag(
    stringValue = "A String Value",
    intValue = 100, 
    enumValue = Kind.STANDARD, 
    annotationTypeValue = @Priority(3), 
    classValue = Application.class, 
    classesValue = {Application.class}
)
public class Application {
    
    private int id;
    
    public void setId(int id) {
        this.id = id;
    }
}

From the Annotation object

This is the simplest and direct way of getting annotation values. The following code example is getting annotation values other than Class.

@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes("com.hauchee.annotation.Tag")
public class TagProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        
        for (Element typeElement : roundEnv.getElementsAnnotatedWith(Tag.class)) {
            System.out.println("From the Annotation object:");
            
            // Get the annotation object from the type element
            Tag tagAnnotation = typeElement.getAnnotation(Tag.class);
            System.out.printf(">> stringValue: %s\n",
                    tagAnnotation.stringValue());
            System.out.printf(">> intValue: %s\n",
                    tagAnnotation.intValue());
            System.out.printf(">> enumValue: %s\n",
                    tagAnnotation.enumValue().toString());
            System.out.printf(">> annotationTypeValue: %s\n",
                    tagAnnotation.annotationTypeValue().toString());
        }
        return true;
    }
}

When we compile Application project where the TagProcessor above is executed, the compilation is successful and we get the following result.

From the Annotation object:
>> stringValue: A String Value
>> intValue: 100
>> enumValue: STANDARD
>> annotationTypeValue: @com.hauchee.annotation.Priority(value=3)
The result is within expectation without suprise. Now, lets try getting Class value.

1
2
System.out.printf(">> classValue: %s\n", tagAnnotation.classValue().getName());
System.out.printf(">> classesValue: %s\n", tagAnnotation.classesValue()[0].getName());

Well, surprisingly the Application project compilation failed with the following exception.

Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.3:compile (default-compile) on project Application: Fatal error compiling: javax.lang.model.type.MirroredTypeException: Attempt to access Class object for TypeMirror com.hauchee.application.Application

The MirroredTypeException is thrown at line 1. If line 1 is removed, trying the getting Class array value at line 2 will also hit exception but the different one which is MirroredTypesException. What is wrong with them? We can get the answer from Element.getAnnotation JavaDoc.

The annotation returned by this method could contain an element whose value is of type Class. This value cannot be returned directly: information necessary to locate and load a class (such as the class loader to use) is not available, and the class might not be loadable at all. Attempting to read a Class object by invoking the relevant method on the returned annotation will result in a MirroredTypeException, from which the corresponding TypeMirror may be extracted. Similarly, attempting to read a Class[]-valued element will result in a MirroredTypesException.

Note: This method is unlike others in this and related interfaces. It operates on runtime reflective information — representations of annotation types currently loaded into the VM — rather than on the representations defined by and used throughout these interfaces. Consequently, calling methods on the returned annotation object can throw many of the exceptions that can be thrown when calling methods on an annotation object returned by core reflection. This method is intended for callers that are written to operate on a known, fixed set of annotation types.

Getting a Class value from annotation object directly is not a good move based on the explanation above. In fact, we should get DeclaredType (a subclass of TypeMirror) which represents the Class type or Interface type in annotation processing. The good news is we can get the type mirror corresponding to the type being accessed from MirroredTypesException directly.

try {
    System.out.printf(">> classValue: %s\n", // never print
            tagAnnotation.classValue().getName());
} catch (MirroredTypeException e) {
    TypeMirror typeMirror = e.getTypeMirror();
    System.out.printf(">> classValue(MirroredTypeException): %s\n",
            typeMirror.toString());
}
try {
    System.out.printf(">> classesValue: %s\n", // never print
            tagAnnotation.classesValue()[0].getName());
} catch (MirroredTypesException e) {
    List<? extends TypeMirror> typeMirrors = e.getTypeMirrors();
    System.out.printf(">> classValue(MirroredTypesException): %s\n",
            typeMirrors.get(0).toString());
}

And the result:
>> classValue(MirroredTypeException): com.hauchee.application.Application
>> classValue(MirroredTypesException): com.hauchee.application.Application
Although it works but this is taking the short-cut and not a good coding practise. Moreover, we need to take the risk if the reflection in annotation object is not working due to any reason that we don't know.

From the Annotation element

What we actually process in annotation processor is the Java source. The Java source is modelled into elements. Take the Application class as an example, the corresponding elements are as below:

package com.hauchee.application; // PackageElement

import com.hauchee.annotation.Priority;
import com.hauchee.annotation.Tag;
import com.hauchee.annotation.Tag.Kind;

@Tag( // AnnotationMirror
    // ExecutableElement:AnnotationValue pairs
    stringValue = "A String Value",
    intValue = 100, 
    enumValue = Kind.STANDARD, 
    annotationTypeValue = @Priority(3), 
    classValue = Application.class, 
    classesValue = {Application.class}
)
public class Application { // TypeElement
    
    private int id; // VariableElement
    
    public void setId(int id) { // ExecutableElement
        this.id = id;
    }
}

The element that we are interested in is the AnnotationMirror. Its name easily lead us to think that it is a subclass of TypeMirror. Apparently it is not. While the TypeMirror is about the Type family, the AnnotationMirror is from the Element family.

The annotation element key such as stringValue is a ExecutableElement. This is true indeed. It is a method in annotation type and executable from an annotation object you have seen in the previous approach.

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

    for (Element typeElement : roundEnv.getElementsAnnotatedWith(Tag.class)) {
        System.out.println("From the Annotation element:");

        // Get the annotation element from the type element
        List<? extends AnnotationMirror> annotationMirrors = typeElement.getAnnotationMirrors();
        for (AnnotationMirror annotationMirror : annotationMirrors) {
            
            // Get the ExecutableElement:AnnotationValue pairs from the annotation element
            Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues
                    = annotationMirror.getElementValues();
            for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry
                    : elementValues.entrySet()) {
                String key = entry.getKey().getSimpleName().toString();
                Object value = entry.getValue().getValue();
                switch (key) {
                    case "intValue":
                        int intVal = (Integer) value;
                        System.out.printf(">> intValue: %d\n", intVal);
                        break;
                    case "stringValue":
                        String strVal = (String) value;
                        System.out.printf(">> stringValue: %s\n", strVal);
                        break;
                    case "enumValue":
                        VariableElement enumVal = ((VariableElement) value);
                        System.out.printf(">> enumValue: %s\n", enumVal.getSimpleName());
                        break;
                    case "annotationTypeValue":
                        AnnotationMirror anoTypeVal = (AnnotationMirror) value;
                        System.out.printf(">> annotationTypeValue: %s\n", anoTypeVal.toString());
                        break;
                    case "classValue":
                        TypeMirror typeMirror = (TypeMirror) value;
                        System.out.printf(">> classValue: %s\n", typeMirror.toString());
                        break;
                    case "classesValue":
                        List<? extends AnnotationValue> typeMirrors
                            = (List<? extends AnnotationValue>) value;
                        System.out.printf(">> classesValue: %s\n",
                            ((TypeMirror) typeMirrors.get(0).getValue()).toString());
                        break;
                }
            }
        }
    }
    return true;
}

Result:
From the Annotation element:
>> stringValue: A String Value
>> intValue: 100
>> enumValue: STANDARD
>> annotationTypeValue: @com.hauchee.annotation.Priority(3)
>> classValue: com.hauchee.application.Application
>> classValue: com.hauchee.application.Application
This approach is more reliable compared to the previous one. However, the returned object of the AnnotationValue is an Object. Therefore, as you can see, there are a lot of class castings before we can get the real value we want. Wrong casting will be troublesome.

From the Annotation Value Visitor

The AnnotationValue can accept visitor to process its value object. This means that the annotation value processing can be offloaded from annotation processor. This design makes the codes in a neat way.

@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes("com.hauchee.annotation.Tag")
public class TagProcessor extends AbstractProcessor {

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

        // The annotation value visitor that is able to assess all annotation value
        MyValueVisitor valueVisitor = new MyValueVisitor();
        
        for (Element typeElement : roundEnv.getElementsAnnotatedWith(Tag.class)) {
            System.out.println("From the Annotation Value Visitor:");
    
            // Get the annotation element from the type element
            List<? extends AnnotationMirror> annotationMirrors = typeElement.getAnnotationMirrors();
            for (AnnotationMirror annotationMirror : annotationMirrors) {
                Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues
                        = annotationMirror.getElementValues();
                for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry
                        : elementValues.entrySet()) {
                    entry.getValue().accept(valueVisitor, null);
                }
            }
        }
        return true;
    }
}

Result:
From the Annotation Value Visitor:
>> stringValue: A String Value
>> intValue: 100
>> enumValue: STANDARD
>> annotationTypeValue: @com.hauchee.annotation.Priority(3)
>> classValue: com.hauchee.application.Application
>> classValue: com.hauchee.application.Application

Below is the annotation value visitor to print all the Tag annotation values. As you can see, no casting is required which eliminate the risk of wrong casting during processing time.

public class MyValueVisitor extends SimpleAnnotationValueVisitor7<Void, Void> {

    @Override
    public Void visitInt(int i, Void p) {
        System.out.printf(">> intValue: %d\n", i);
        return p;
    }

    @Override
    public Void visitString(String s, Void p) {
        System.out.printf(">> stringValue: %s\n", s);
        return p;
    }

    @Override
    public Void visitEnumConstant(VariableElement c, Void p) {
        System.out.printf(">> enumValue: %s\n", c.getSimpleName());
        return p;
    }

    @Override
    public Void visitAnnotation(AnnotationMirror a, Void p) {
        System.out.printf(">> annotationTypeValue: %s\n", a.toString());
        return p;
    }

    @Override
    public Void visitType(TypeMirror t, Void p) {
        System.out.printf(">> classValue: %s\n", t.toString());
        return p;
    }

    @Override
    public Void visitArray(List<? extends AnnotationValue> vals, Void p) {
        for (AnnotationValue val : vals) {
            val.accept(this, p);
        }
        return p;
    }
}

Another important point is that do not implement the AnnotationValueVisitor interface directly. This is because new methods could be added to accommodate new, currently unknown, language structures added to future versions of the Java™ programming language. In order to avoid of source incompatible in the future version, visitor class should extends the appropriate abstract visitor class that implements this interface. The MyValueVisitor class above extends SimpleAnnotationValueVisitor7. The number "7" at the back indicating it complies with RELEASE_7 source version which is same as the SupportedSourceVersion defined in the TagProcessor.


References:
http://hannesdorfmann.com/annotation-processing/annotationprocessing101/
http://blog.retep.org/2009/02/13/getting-class-values-from-annotations-in-an-annotationprocessor/
http://stackoverflow.com/questions/17660469/get-field-class-in-annotations-processor
http://stackoverflow.com/questions/1458535/which-types-can-be-used-for-java-annotation-members
https://docs.oracle.com/javase/7/docs/api/javax/lang/model/type/MirroredTypeException.html
https://docs.oracle.com/javase/7/docs/api/javax/lang/model/element/Element.html
https://docs.oracle.com/javase/7/docs/api/javax/lang/model/element/AnnotationValueVisitor.html
https://docs.oracle.com/javase/7/docs/api/javax/lang/model/util/SimpleAnnotationValueVisitor7.html

No comments: