Annotation Processing Tool裏讀取Annotation屬性值爲Class的值

Java, Trains, Space, Weather and pretty much everything elsehtml

Getting Class values from Annotations in an AnnotationProcessor

Post navigation

In annotation processors one common use case is to read the values contained within an Annotation. In most cases this works fine, however there are some caveats when it comes to accessing values within Annotations that are either Class or a Class array. It’s even worse if you are trying to access an annotation on a method within a super type of the class you are processing. Here I’ll go through how to access those Class values and how to get those values when they are in a super type – specifically when a method has been overridden.java

 

First why?linux

 

Well inside retepTools we have an AnnotationProcessor that checks to ensure that certain annotations are used correctly, specifically the @ReadLock and @WriteLock annotations. It’s invalid for those annotations to be used together on the same method. It’s also invalid for a method to be annotated with one and then overridden in another class but annotated with the other, so the processor checks the overridden method and generates an error if the rules have been violated.web

 

Now this caused be a big headache in reading the overridden annotations, because although the annotations were present, the values (and specific the Class ones) were null. Although @ReadLock and @WriteLock do not use values, another related annotation @Contract does, so here’s how I finally solved the problem.sql

 

The usual way of reading annotation valuesubuntu

 

In most cases, when you have an javax.lang.model.element.Element you can use the getAnnotation() method to return your annotation and then you have access to the values contained within it. For example say we have an annotation called Action which holds a single String value:windows

 

@Documented
@Retention( RetentionPolicy.RUNTIME )api

@Target(ElementType.METHOD)tomcat

public @interface Action

{

    String value();

}

 

We annotate a class with that annotation against the run method:

 

public class A {

    @Action( 「do something」 )

    public void run() {

    }

}

Now within your AnnotationProcessor, you can obviously get the value easily by calling getAnnotation( Action.class ) and if it returns an instance call the value() method:

 

@SupportedAnnotationTypes( 「*」 )

@SupportedSourceVersion( SourceVersion.RELEASE_6 )

@ProhibitAnnotationProcessing

public class Processor

        extends AbstractProcessor

{

 

    @Override

    public boolean process( final Set<? extends TypeElement> annotations,

                            final RoundEnvironment env )

    {

        if( !env.processingOver() )

        {

            for( Element e : env.getRootElements() )

            {

                TypeElement te = findEnclosingTypeElement( e );

                System.out.printf( 「\nScanning Type %s\n\n」,

                                   te.getQualifiedName() );

 

                for( ExecutableElement ee : ElementFilter.methodsIn(

                        te.getEnclosedElements() ) )

                {

 

                    Action action = ee.getAnnotation( Action.class );

                    

                    System.out.printf(

                            「%s Action value = %s\n」,

                            ee.getSimpleName(),

                            action == null ? null : action.value() );

                }

            }

        }

 

        return false;

    }

 

    public static TypeElement findEnclosingTypeElement( Element e )

    {

        while( e != null && !(e instanceof TypeElement) )

        {

            e = e.getEnclosingElement();

        }

        return TypeElement.class.cast( e );

    }

}

This generates the following output when run:

 

————————————————————————

Building scratch

   task-segment: [clean, install]

————————————————————————

[clean:clean]

Deleting directory /Users/peter/dev/retep/scratch/target

[compiler:compile {execution: compileAnnotations}]

Compiling 2 source files to /Users/peter/dev/retep/scratch/target/classes

[resources:resources]

Using default encoding to copy filtered resources.

[compiler:compile]

Compiling 2 source files to /Users/peter/dev/retep/scratch/target/classes

 

Scanning Type scratch.A

 

run Action value = do something

 

Reading annotation values from an overridden method

 

Now this is fine, but what happens if you are looking at an annotation thats in an overridden class?

 

Say we have class B which extends A and overrides run():

 

public class B extends A {

    @Override

    public void run() {

    }

}

 

Now when we run the processor, for each ExecutableElement we’ll first look for an annotation and then if not found we’ll look for an overridden method and check there. 

 

    @Override

    public boolean process( final Set<? extends TypeElement> annotations,

                            final RoundEnvironment env )

    {

        if( !env.processingOver() )

        {

            for( Element e : env.getRootElements() )

            {

                TypeElement te = findEnclosingTypeElement( e );

 

                System.out.printf( 「\nScanning Type %s\n\n」,

                                   te.getQualifiedName() );

 

                for( ExecutableElement ee : ElementFilter.methodsIn(

                        te.getEnclosedElements() ) )

                {

 

                    Action action = ee.getAnnotation( Action.class );

 

                    if( action == null )

                    {

                        // Look for the overridden method

                        ExecutableElement oe = getExecutableElement( te,

                                                                     ee.getSimpleName() );

 

                        if( oe != null )

                        {

                            action = oe.getAnnotation( Action.class );

                        }

                    }

 

                    System.out.printf(

                            「%s Action value = %s\n」,

                            ee.getSimpleName(),

                            action == null ? null : action.value() );

                }

            }

        }

 

        return false;

    }

 

    public ExecutableElement getExecutableElement( final TypeElement typeElement,

                                                   final Name name )

    {

        TypeElement te = typeElement;

        do

        {

            te = (TypeElement) processingEnv.getTypeUtils().asElement(

                    te.getSuperclass() );

            if( te != null )

            {

                for( ExecutableElement ee : ElementFilter.methodsIn(

                        te.getEnclosedElements() ) )

                {

                    if( name.equals( ee.getSimpleName() ) && ee.getParameters().isEmpty() )

                    {

                        return ee;

                    }

                }

            }

        } while( te != null );

        return null;

    }

 

Now when we run we get the annotated value on A.run() when we are processing B.run():

 

[compiler:compile]

Compiling 2 source files to /Users/peter/dev/retep/scratch/target/classes

 

Scanning Type scratch.A

 

run Action value = do something

 

Scanning Type scratch.B

 

run Action value = do something

 

The problem with Class

 

Now this is fine, but what happens if the annotation’s value is Class instead of String? Well the problem here is that Javac does not load classes in the normal manner. In fact it doesn’t at all for classes that are in the source – it’s all contained within a model.

 

Now say our Action annotation had value defined as Class instead of String. In that case when we call action.value() it would fail:

 

[compiler:compile]

Compiling 2 source files to /Users/peter/dev/retep/scratch/target/classes

 

Scanning Type scratch.A

 

javax.lang.model.type.MirroredTypeException: Attempt to access Class object for TypeMirror java.lang.Runnable

 

So we have to find another way to get the value, and there are two available to us. The first is not to use getAnnotation() but getAnnotationMirrors(), and the second is a neat trick with MirroredTypeException.

Solution 1 use getAnnotationMirrors()

This solution is a little long winded but is the most reliable. When getAnnotationMirrors() is used, it returns a set of AnnotationMirror instances, one for each annotation on that Element, so the first step is to locate the correct AnnotationMirror for the annotation you require. The next step is to extract the AnnotationValue’s from that mirror which represents the values stored in the annotation. This is held in a map keyed by an ExecutableElement.

 

Why ExecutableElement? Well it’s because the annotation values are actually defined as methods – hence why in our Action value is defined as value().

So in the next example we run through the AnnotationMirrors on a method until we find the one for our annotation then run through until we find the required value.

Once we have the AnnotationValue we simply print it to System.out but normally you would use the getValue() method which returns the value as an Object. If the value is an array it returns a java.util.List containing the values. Oh and if the values are of type Class then it returns a TypeMirror or a List of TypeMirrors.

 

Element actionElement = processingEnv.getElementUtils().getTypeElement(

        Action.class.getName() );

TypeMirror actionType = actionElement.asType();

 

for( ExecutableElement ee : ElementFilter.methodsIn(

        te.getEnclosedElements() ) )

{

    ExecutableElement oe = ee;

    AnnotationValue action = null;

    while( action == null && oe != null )

    {

        for( AnnotationMirror am : oe.getAnnotationMirrors() )

        {

            if( am.getAnnotationType().equals( actionType ) )

            {

                for( Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : am.getElementValues().entrySet() )

                {

                    if( 「value」.equals(

                            entry.getKey().getSimpleName().toString() ) )

                    {

                        action = entry.getValue();

                        break;

                    }

                }

            }

        }

 

        // Look for the overridden method

        oe = getExecutableElement(

                findEnclosingTypeElement( oe ),

                ee.getSimpleName() );

    }

 

    System.out.printf(

            「%s Action value = %s\n」,

            ee.getSimpleName(),

            action == null ? null : action );

}

 

Now at first that appears to work, and in most use cases it does – if we have a method thats overridden then we get the annotation values from the overridden method.

 

However, although its not obvious, if the super type is not part of the same CompilationUnit – i.e. it’s in a third party jar or from a previous call to javac then it will not find anything outside of that CompilationUnit.

 

The trouble with TypeMirrors

 

Now the cause on why the solution above fails isn’t obvious. The problem here is actually down to the TypeMirror’s. In the above example we get a TypeMirror for the annotation called actionType then search the AnnotationMirror set of each element using that TypeMirror.

 

Now TypeMirror acts in a similar way to how Class works at runtime. At run time Class is equal if it’s in the same ClassLoader, so here TypeMirror is equal if it’s in the same CompilationUnit. So the example above fails because they are different instances.


So the solution here is not to use TypeMirror.equals() but to convert the TypeMirror into a String representing the fully qualified class name and use equals() on that String. Now, no matter what  source the super type comes from, it will always match.

 

Here’s the new version:

final String actionName = Contract.class.getName();

 

for( ExecutableElement ee : ElementFilter.methodsIn(

        te.getEnclosedElements() ) )

{

    ExecutableElement oe = ee;

    AnnotationValue action = null;

    while( action == null && oe != null )

    {

        for( AnnotationMirror am : oe.getAnnotationMirrors() )

        {

            if( actionName.equals(

                    am.getAnnotationType().toString() ) )

            {

                for( Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : am.getElementValues().entrySet() )

                {

                    if( 「value」.equals(

                            entry.getKey().getSimpleName().toString() ) )

                    {

                        action = entry.getValue();

                        break;

                    }

                }

            }

        }

 

        // Look for the overridden method

        oe = getExecutableElement(

                findEnclosingTypeElement( oe ),

                ee.getSimpleName() );

    }

 

    System.out.printf(

            「%s Action value = %s\n」,

            ee.getSimpleName(),

            action == null ? null : action );

}

 

Now that one works. The lesson here is to use the String version of TypeElement when searching as two TypeElement’s representing the same class are not always equal.

 

Solution 2 – Single Class values

 

Now if your value contains just one Class (i.e. not Class[] ) then there’s a much simpler solution. This one isn’t that obvious, but I found that someone had a similar problem in the sun forums[1]. There the trick is to actually use getAnnotation() and catch the MirroredTypeException. Surprisingly the exception then provides the TypeMirror of the required class:

 

for( ExecutableElement ee : ElementFilter.methodsIn(

        te.getEnclosedElements() ) )

{

 

    Action action = ee.getAnnotation( Action.class );

 

    if( action == null )

    {

        // Look for the overridden method

        ExecutableElement oe = getExecutableElement( te,

                                                     ee.getSimpleName() );

 

        if( oe != null )

        {

            action = oe.getAnnotation( Action.class );

        }

    }

 

    TypeMirror value = null;

    if( action != null )

    {

        try

        {

            action.value();

        }

        catch( MirroredTypeException mte )

        {

            value = mte.getTypeMirror();

        }

    }

 

    System.out.printf(

            「%s Action value = %s\n」,

            ee.getSimpleName(),

            value );

}

 

Notice getTypeMirror() method call? Here’s the output of the above loop:

 

[compiler:compile]

Compiling 2 source files to /Users/peter/dev/retep/scratch/target/classes

 

Scanning Type scratch.A

 

run Action value = java.lang.Runnable

 

Scanning Type scratch.B

 

run Action value = java.lang.Runnable

 

This trick works fine for individual classes, but sadly it does not work for Class[] arrays. According to the javadocs it should work for Class[] as it should then throw the MirroredTypesException instead and that exception contains an alternate method that returns a List of TypeMirrors.

 

However it doesn’t – it simply throws MirroredTypeException for the first element in the array. I think it’s where it’s running through the array to populate it and the first element is then throwing MirroredTypeException before it gets chance to throw MirroredTypesException.

 

Conclusion

 

Well, hopefully this article will save someone hours of trouble when they hit the same problem. I’ve spent about 20 hours of time searching the net and dabbling for these solutions – and at least these solutions use just the documented public APIs.

 

References

  1. http://forums.sun.com/thread.jspa?threadID=791053
Advertisements
 

Post navigation

12 Comments

  1. I bow down humbly in the presence of such grtaenses.

  2. hiddenone

    Nice post, but this works only for retrieving type metamodel (TypeMirror) from annotation elements whose value is of type Class.

    Is it actually possible to load the Class somehow?

    1. The problem is that when the annotation processor runs it runs from within Javac and as such you cannot load the class as all you have is the source classpath.

      During compilation the classes are not actually loaded in the normal sense, only the meta data is loaded, so if you try to load those classes normally and then use reflection then it will fail.

  3. Hi Peter, your post is great, thank you!

    I’ve more than one annotation for my classes. One annotation registers a Swing Action in a registry and two other annotations put this action in different GUI components (like menu, toolbar …).

    To register a special action to a component I prefer the class value. It’s more type save, but I’ve a problem to get the AnnotationMirror in nested Annotations.

    here an example:

    @Action (id=」my-super-action」, icon=」org/mycomp/icon.png」)
    @ActionReferences ({
    @ActionReference (component=MyPopupMenu.class, position = 100),
    @ActionReference (component=MyToolbar.class, position = 10),
    })
    public class MySuperAction implements ActionListener {
    }

    with

    @Retention(RetentionPolicy.SOURCE)
    @Target(ElementType.TYPE)
    public @interface Action {…}

    @Retention(RetentionPolicy.SOURCE)
    @Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
    public @interface ActionReference {…}

    @Retention(RetentionPolicy.SOURCE)
    @Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PACKAGE})
    public @interface ActionReferences {
    ActionReference[] value();
    }

    With you sample code it’s easy to evaluate a class value in the @Action annotation. But if I’m in the process method to parse the Action annotation, I’m unable to go into the ActionReference:

    public final boolean process(Set annotations, RoundEnvironment env) {

    for (Element e : env.getElementsAnnotatedWith(Action.class)) {

    TypeElement clazz = (TypeElement) e;
    Action action = clazz.getAnnotation(Action.class);
    String teName = elements.getBinaryName(clazz).toString();
    // some useful things…

    ActionReference aref = e.getAnnotation(ActionReference.class);
    if (aref != null) {
    processReferences(e, teName, aref, category);
    }
    ActionReferences refs = e.getAnnotation(ActionReferences.class);
    if (refs != null) {
    for (ActionReference actionReference : refs.value()) {
    processReferences(e, teName, actionReference, category);
    }
    }
    }

    In processReference it’s no problem to call the annotation properties. But I’ve no idea how to get the AnnotationValues

    This does not work:

    private void processReferences(
    Element e, String teName, ActionReference ref, String category)
    {
    String clazz = null;

    AnnotationValue componentValue = null;
    for (AnnotationMirror am : e.getAnnotationMirrors()) {
    for (Map.Entry entry : am.getElementValues().entrySet()) {
    // Looking for the class value from component
    if (「component」.equals(entry.getKey().getSimpleName().toString())) {
    componentValue = entry.getValue();
    break;
    }
    }
    clazz = componentValue .getValue().toString();

    I’ve only the base Element provided by the getElementsAnnotatedWith method.

    Do you have an idea? 

    br, josh.

    1. Looking at the javadoc for AnnotationValue:

      A value is of one of the following types:
      * a wrapper class (such as Integer) for a primitive type
      * String
      * TypeMirror
      * VariableElement (representing an enum constant)
      * AnnotationMirror
      * List (representing the elements, in declared order, if the value is an array)

      So in theory going by your code, the 「value」 entry inside ActionReferences should be a List and then each one of those an AnnotationMirror for each ActionReference.

      I’ll have a play to see if that’s the case.

  4. Awesome.
    Many thanks 

  5. yash

    Hi Peter,

    Thanks for the awesome article. I have been searching a lot to access the class values that are stored in the annotation.

    Here in your example 「Action」 annotation has the Class variable stored in value() method right. I am using your second solution and trying to get a hold of Class variable stored in value().

    I am able to get a TypeMirror object, But unable to extract the class object which is stored in it. It would be great if you can help me understand or guide me about how to do it.

    Thanks

  6. CGen

    Hello!

    Look at the interface 「Types」. Utility methods for operating on types.
    http://docs.oracle.com/javase/7/docs/api/javax/lang/model/util/Types.html

    「if( am.getAnnotationType().equals( actionType ) )」 – this is not right.

    You must use the 「boolean isSameType(TypeMirror t1,
    TypeMirror t2)」 method .

    Thank you, good luck!

    1. CGen

      And more. http://docs.oracle.com/javase/7/docs/api/javax/lang/model/type/TypeMirror.html
      「Types should be compared using the utility methods in Types. There is no guarantee that any particular type will always be represented by the same object.」

  7. To whomever who wrote this, Thanks a lot! (I had solved my own question, but you just make me feel quiet by showing me that I chose the right solution)

Leave a Reply

Previous articles

相關文章
相關標籤/搜索