Reading Annotations - Dev.java

Annotations are widely used with the Reflection API. Many frameworks use them extensively and with great success. This is the case for the object relational mapping frameworks, the dependency injection frameworks, some validation frameworks, some security frameworks. Basically all Java EE, Jakarta EE, and all the similar frameworks define their sets of annotations that you can add on classes and class members to trigger behavior. This section shows you how all this is working under the hood, and how you can use annotations at runtime.

Discovering Annotations on Elements

The Annotations page explains what are annotations, and how you can use them.

The following classes from the Reflection API implement the interface AnnotatedElement that gives you access to the annotations such an element can carry: Field, Method, Constructor, and Class. Remember that the class Class models classes, abstract classes, interfaces, enumerations, records, and arrays.

This AnnotatedElement gives you the methods you need to discover the annotations added on the corresponding element, as well as the instance of this annotation, so that you can call its methods.

Suppose that you have the following enumeration, and the two annotations @Bean and @Serialized in your application.

As you can see, both can be applied on types, and are made available at runtime. The @Serialized annotation defines an attribute, format , that can take three values: BINARY, XML, and JSON.

Now, suppose that you have a Person class, declared in this way.

The Reflection API gives you several methods to discover these annotations.

Let us see these methods in action.

First, you can check if the Person class has any annotations, and discover them.

Running the previous code prints the following.

isBean = true
annotation = @org.devjava.Serialized(format=JSON)
annotation = @org.devjava.Bean()

Note that what you get in return are instances of the annotation classes you define, on which you can call the methods they declare. Let us now run this code.

Running the previous code prints the following.

format = JSON

Let us create another, repeating annotation.

And modify the Person class, to add a field to it, with this repeating annotation.

From the class file perspective, there is actually only one annotation on the name field: @Validators. You can then get this instance, and get the enclosed @Validator annotations by calling the value() method. This is what the following example is doing.

Running the previous example prints the following.

# annotations = 1
validator = @org.devjava.Validator(NON_NULL)
validator = @org.devjava.Validator(NON_EMPTY)

So you can have access to repeating annotations using the getAnnotations() method, but it's a little tedious.

This is where the getAnnotationsByType(Class) can make your code simpler. Calling this method with the Validator class as an argument gives you the annotations you are looking for.

Running the previous example gives you the following.

# annotations = 2
annotation = @org.devjava.Validator(NON_NULL)
annotation = @org.devjava.Validator(NON_EMPTY)

Getting Inherited Annotations

Annotations declared on types (classes, abstract classes, and interfaces) can be inherited by their subtypes. It does not make sense to do that for enumerations and records, as these two are final classes, and thus cannot be extended.

The methods getDeclaredAnnotations() and getDeclaredAnnotation(Class) have a strict behavior. They return only the annotation declared on the type you are calling these methods on. On the other hand, getAnnotations() and getAnnotation(Class) may return inherited annotations. You can see that on the following example.

Suppose you have the following Bean annotation, that can be inherited.

And the following two classes. Note that the Person class is annotated with @Bean, and not the User class that extends it.

You can examine the available annotations on the User class with the following code.

Running the previous code prints the following.

# declared annotations = 0
# annotations = 1
annotation = @org.devjava.Bean()

Getting Annotations on Types

Annotated types are annotations declared on types, that can be used anywhere in your code. You can see example of such use of types on this page.

The Reflection API gives you access to some of these annotations using a specific interface: AnnotatedType.

Suppose that you have the following annotation. Note that this annotation can be used where the types are used, but not on the declaration of these types.

You can then use this annotation in this way. Note that this annotation is declared on the extending class, and on an implemented interface.

The Reflection API gives you access to this annotation in this way.

Running the previous example gives you the following.

annotation on the super class = @org.devjava.NonNull()
annotation on the implemented interface = @org.devjava.NonNull()

Let us consider the following example, where you can discover an annotation on an exception. This exception is not a RuntimeException, so you need to handle it explicitly. To overcome this, you decide to create an annotation to tell your client code if it can rethrow this exception as a runtime exception.

You can then use this class with the following client code.

Note that in this example the call to the getConstructor() method throws a NoSuchMethodException exception, that you need to catch or rethrow.

Running the previous code prints the following. As you can see, the exception is rethrown as a runtime exception.

Exception in thread "main" java.lang.RuntimeException: org.devjava.EmptyStringException: name is empty
    at org.devjava.Main.main(ScrapMain.java:32)
Caused by: org.devjava.EmptyStringException: name is empty
    at org.devjava.Message.<init>(ScrapMain.java:18)
    at org.devjava.Main.main(ScrapMain.java:45)