Monday, March 21, 2016

Repeating Annotation Effect - How to get the correct annotation at runtime

Since Annotation was introduced in Java 5, Java had also provided the Reflection interface, java.lang.reflect.AnnotatedElement which represents an annotated element of the program at runtime. It allows us to get the program element's annotations reflectively. An annotation might not be directly present on a program element. Hence, getter methods in AnnotatedElement help to detect and return the annotation(s) with certain kind of presence.

Moreover, the kind of annotation presence is further expanded when Java 8 introduced Repeating Annotation. And of course, new methods were added onto AnnotatedElement to make sure the caller is able to retrieve the annotation with new kind of presence.

This post describes:
  • Different kind of annotation presence, prior to and since Java 8.
  • Which AnnotatedElement method deals with which kind of annotation presence.
The following annotation types and classes are used for explanation in this post.

Given annotation types below.
@Retention(RetentionPolicy.RUNTIME) // Annotation available at runtime
@Target(ElementType.TYPE) // Target to Class/Interface element only
@Inherited // Inheritable. In this case, annotation is inherited to Subclass
public @interface Alert {
    String name() default "";
}

@Retention(RetentionPolicy.RUNTIME) // Annotation available at runtime
@Target(ElementType.TYPE) // Target to Class/Interface element only
@Inherited // Inheritable. In this case, annotation is inherited to Subclass
public @interface Role {
    String name() default "";
}

Apply annotation onto the Access classes below.
@Role(name = "superadmin")
@Alert(name = "superadmin") 
abstract class Access { // Superclass
}

@Role(name = "staff") // Overrided superclass @Role annotation
class ReadOnlyAccess extends Access { // Subclass
}

Now we will use the methods in AnnotatedElement to retrieve the @Role and @Alert annotation at runtime.

Prior to Java 8

Prior to Java 8, there are no precise terms and descriptions about the annotation presences. However, they are obvious and  not too complicated to be understood. Basically, an annotation could have 2 kinds of presences on a program element.
  • Directly present
  • Present

Directly Present

An annotation is explicitly specified on an element and this annotation is visible or available at runtime. To get the annotation that direct present on an element, we use getDeclaredAnnotation(Class) method.

Class c = ReadOnlyAccess.class;
Role roleDirectPresent = (Role) c.getDeclaredAnnotation(Role.class); // roleDirectPresent = @Role(staff)
Alert alertDirectPresent = (Alert) c.getDeclaredAnnotation(Alert.class); // alertDirectPresent = null
Annotation[] allDirectlyPresent = c.getDeclaredAnnotations(); // allDirectPresent = {@Role(staff)}

We are able to get the @Role(staff) because it is directly specified on the ReadOnlyAccess class element. On the hand, although @Alert is specified on Access class element, and it is inherited to ReadOnlyAccess, but we never able to get it using getDeclaredAnnotation(Class) because it is not direct present on ReadOnlyAccess class element.

Present

An annotation is either directly present on an element or inherited from the element's parent and this annotation is visible or available at runtime. To get the annotation that present on an element, we use getAnnotation(Class) method.

Class c = ReadOnlyAccess.class;
Role rolePresent = (Role) c.getAnnotation(Role.class); // rolePresent = @Role(staff)
Alert alertPresent = (Alert) c.getAnnotation(Alert.class); // aleartPresent = @Alert(superadmin) - inherited
Annotation[] allPresent = c.getAnnotations(); // allPresent = {@Role(staff), @Alert(superadmin)}

By using getAnnotation(Class) method, we not only able to get the annotation @Role(staff) that is directly present on ReadOnlyAccess, we also able to get the annotation @Alert(superadmin) that is inherited from Access class element.

Note: Getting an inherited annotation does not work for all annotated element. For example, it does not works for METHOD element because the getAnnotation(Class) method in java.lang.reflect.Method still getting the annotation that directly present on the method element.

Since Java 8

Started from Java 8, an annotation type supports repeatable. We are now able to specify zero to many annotations with the same annotation type. Now, let enhance our annotation type to be repeatable.

Repeating Annotation

We first create the containing annotation types.
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Alerts {
    Alert[] value(); // to contain an array of Alert annotations
}

@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Roles {
    Role[] value(); // to contain an array of Role annotations
}

Then specify the @Repeatable annotation onto the annotation types respectively.
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(Alerts.class) // Use @Alerts as container annotation
public @interface Alert {
    String name() default "";
}

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(Roles.class) // Use @Roles as container annotation
public @interface Role {
    String name() default "";
}

A few important notes to take:
  • The containing annotation type must have a method named as "value" and return an array of specific annotation. If not, it will be treated as normal annotation type.
  • The characteristic (such as inheritable, runtime visible, target) of the containing annotation type must consistent with the annotation type that it would like to contain. Failing to do so will cause compilation error at annotation type that takes it as annotation container.

Backward Compatibility

Making an annotation type to support repeatable will not break backward compatibility. In the case where the Access and ReadOnlyAccess class remain the same,
@Role(name = "superadmin")
@Alert(name = "superadmin") 
abstract class Access { // Superclass
}

@Role(name = "staff") 
class ReadOnlyAccess extends Access { // Subclass
}

Despite the @Alert and @Role is now repeatable, but we are still able to get the single annotation using the methods below..
Role roleDirectPresent = (Role) c.getDeclaredAnnotation(Role.class); // roleDirectPresent = @Role(staff)       
Role rolePresent = (Role) c.getAnnotation(Role.class); // rolePresent = @Role(staff)

Note: The @Role(staff) annotation that is specified on the ReadOnlyAccess class element will still override the @Role(superadmin) annotation of it superclass Access. They are never accumulated to become repeating annotation

New Kind of Annotation Presence

Due to the existence of the container annotation, new kinds of annotation presence are introduced. What does the methods in AnnotatedElement can do also become more complicated.
  • Directly Present
  • Indirectly Present
  • Present
  • Associated

Let's specify the repeating annotation in our classes. I will then use the AnnotationElement methods to get the annotation and explain the kind of presence accordingly. You may need to come back to here again when you go through the next sections because the example codes in next section are all talking about getting annotation we have specified here.
@Role(name = "superadmin")
@Alert(name = "superadmin") 
@Alert(name = "superuser") // Repeated @Alert
abstract class Access {
}

@Role(name = "staff")
@Role(name = "guest") // Repeated @Role
class ReadOnlyAccess extends Access {
}

Directly Present

An annotation A is directly present on an element E if E has a RuntimeVisibleAnnotations or RuntimeVisibleParameterAnnotations or RuntimeVisibleTypeAnnotations attribute, and the attribute contains A. - AnnotatedElement Oracle Doc

Well, it basically maintains the contract prior to Java 8. However, things work differently due to the existence of repeating annotation. Look at the ReadOnlyAccess class, now we have the repeating annotation, in this case, two @Role annotations are specified on it. What we see is two individual @Role annotations but in fact, they are contained in a container annotation @Roles. Decompile the ReadOnlyAccess class and you will find that, Java Compiler had turned them into a container annotation as below.

@Roles(
    value = {
        @Role(name = "staff"),
        @Role(name = "guest")
    }
)
class ReadOnlyAccess extends Access {
}

That's mean, what directly present on the ReadOnlyAccess class element is container annotation @Roles.

Note: We can specify the repeating annotations by using the containing annotation type. After all, it is also a valid annotation type.

Let's try to get the annotation that directly present on ReadOnlyAccess class element. Again, we use the getDeclaredAnnotation(Class) to do that. Have a look on the variable(s) returned from each method call below, they should help you to understand the idea behind.

Class c = ReadOnlyAccess.class;

// roleDirectPresent = null 
// @Role annotation no longer direct present. It is an attribute in container annotation @Roles.
Role roleDirectPresent = (Role) c.getDeclaredAnnotation(Role.class);

// rolesDirectPresent = @Roles({@Role(staff), @Role(guest)})
// @Roles is the container annotation that direct present on the class element.
Roles rolesDirectPresent = (Roles) c.getDeclaredAnnotation(Roles.class);

// alertDirectPresent = null
// @Alert not explicitly present at all.
Alert alertDirectPresent = (Alert) c.getDeclaredAnnotation(Alert.class);

// allDirectPresent = {@Roles({@Role(staff), @Role(guest)})}
Annotation[] allDirectPresent = c.getDeclaredAnnotations();

// allRolesDirectPresent = @Roles({@Role(staff), @Role(guest)})
// New method in AnnotatedElement since Java 8. Get container annotation that is directly present on the class element
Roles[] allRolesDirectPresent = (Roles[]) c.getDeclaredAnnotationsByType(Roles.class);

The getDeclaredAnnotationsByType(Class) is a new method in AnnotatedElement which was introduce since Java 8. It can be used to get annotation that is directly present on an element. It seems working equivalent to getDeclaredAnnotation(Roles.class), but it actually can do more. See the next Indirectly Present section.

Indirectly Present

An annotation A is indirectly present on an element E if E has a RuntimeVisibleAnnotations or RuntimeVisibleParameterAnnotations or RuntimeVisibleTypeAnnotations attribute, and A 's type is repeatable, and the attribute contains exactly one annotation whose value element contains A and whose type is the containing annotation type of A 's type. - AnnotatedElement Oracle Doc

This is one of the new kinds of annotation presence introduced since Java 8. By using our example code for the explanation, the annotation @Role is indirectly present on a ReadOnlyAccess class element because it is runtime visible, and it resides in the @Roles container annotation which is specified on ReadOnlyAccess.

At first glance on the ReadAccessOnly, it may hard to understand why @Role annotations are not directly present on ReadAccessOnly. They did directly specified on the ReadAccessOnly class element. The reason is what I have described in previous Directly Present section. Whenever more than one repeating annotation specified on an element, Java will use container annotation to host the repeating annotations. Therefore, @Role annotations are indirectly present on ReadAccessOnly class element via container annotation @Roles

The getDeclaredAnnotationByType(Class) method can be used to get the annotations from the container annotation.

// allRoleDirectPresent = {@Role(staff), @Role(guest)}
Role[] allRoleDirectPresent = (Role[]) c.getDeclaredAnnotationsByType(Role.class);

This saves our effort from getting the container annotation first, then look through its value attribute to get the annotations.

Present

An annotation A is present on an element E if either:
  • A is directly present on E; or
  • No annotation of A 's type is directly present on E, and E is a class, and A 's type is inheritable, and A is present on the superclass of E.
AnnotatedElement Oracle Doc

Again, it basically maintains the contract prior to Java 8. The AnnotatedElement methods which able to get annotations that present on an element now also able to get the container annotation which directly present on the element or inherited from the element's parent.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// rolePresent = @Role(superadmin)
// An annotation inherited from parent class element.
Role rolePresent = (Role) c.getAnnotation(Role.class);

// rolesPresent = @Roles({@Role(staff), @Role(guest)})
// Container annotation is directly present on class element
Roles rolesPresent = (Roles) c.getAnnotation(Roles.class);

// alertPresent = null
// Not specify in either superclass or subclass.
Alert alertPresent = (Alert) c.getAnnotation(Alert.class);

// alertsPresent = @Alerts({@Alert(superadmin), @Alert(superuser)})
// Container annotation is inherited from parent class element.
Alerts alertsPresent = (Alerts) c.getAnnotation(Alerts.class);

// allPresent = {@Role(staff), @Alerts({Alert(superadmin), @Alert(superuser)})}
Annotation[] allPresent = c.getAnnotations();

The interesting part is at line #3 and #7. The @Role annotation specified on superclass Access is not overridden by the @Roles container annotation specified on subclass ReadOnlyAccess. Well, they are two different annotations in fact. And of course, they will not be accumulated into a container annotation.

Associated

An annotation A is associated with an element E if either:
  • A is directly or indirectly present on E; or
  • No annotation of A 's type is directly or indirectly present on E, and E is a class, and A's type is inheritable, and A is associated with the superclass of E.

Another new kind of annotation presence introduced since Java 8. It covers the broadest annotation presence where an annotation is directly or indirectly present on an element or inherited from the element's parent.

The only method in AnnotatedElement that able to return annotation that associated to an element is getAnnotationsByType(Class).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// roleAssociated = {@Role(staff), @Role(guest)}
// Because @Role is repeatable, hence look through the container annotation and return it value attributes.
Role[] roleAssociated = (Role[]) c.getAnnotationsByType(Role.class);

// rolesAssociated = {@Roles({@Role(staff), @Role(guest)})}
// @Roles container annotation is directly present on class element.
Roles[] rolesAssociated = (Roles[]) c.getAnnotationsByType(Roles.class);

// alertAssociated = {@Alert(superadmin), @Alert(superuser)}
// Because @Alert is repeatable, hence look through the inherited container annotation and return it value attributes.
Alert[] alertAssociated = (Alert[]) c.getAnnotationsByType(Alert.class);

// alertsAssociated = {@Alerts({@Alert(superadmin), @Alert(superuser)})}
// @Alerts container annotation is inhertied from parent class element.
Alerts[] alertsAssociated = (Alerts[]) c.getAnnotationsByType(Alerts.class);

The highlights are at line #3 and #7.

#3 - Because @Role is repeatable, so getAnnotationByType(Role.class) looks through the container annotation @Roles and return all @Role annotations that the container contains. If the container is empty, then it will return the inherited @Role annotation (if any).

#7 - getAnnotationByType(Alert.class) is able to return all @Alert annotation inhertied by the ReadOnlyAccess.

Summary

Creating a repeating annotation type is straightforward, but getting the correct annotation could be a bit complicated. By knowing different kinds of annotation presence, it will help us to get the annotation that we want.

Overview of kind of presence detected by different AnnotatedElement methods - AnnotatedElement Oracle Doc
Kind of Presence
MethodDirectly PresentIndirectly PresentPresentAssociated
TgetAnnotation(Class<T>) X
Annotation[]getAnnotations() X
T[]getAnnotationsByType(Class<T>) X
TgetDeclaredAnnotation(Class<T>) X
Annotation[]getDeclaredAnnotations() X
T[]getDeclaredAnnotationsByType(Class<T>) XX

Besides runtime processing, Java also introduced the javax.lang.model.AnnotatedConstruct interface which represents the annotated source code element and allows us to get the annotation during annotation processing at compile time. The annotation presence terms and concepts are same as the AnnotatedElement. If you are interested in annotation processing, you can learn more from Annotation Processing during Compile Time.

References:
https://docs.oracle.com/javase/7/docs/api/java/lang/reflect/AnnotatedElement.html
https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/AnnotatedElement.html
https://docs.oracle.com/javase/tutorial/java/annotations/repeating.html
https://docs.oracle.com/javase/8/docs/api/javax/lang/model/AnnotatedConstruct.html

No comments: