Previous in the Series
Current Tutorial
Reading Annotations

Previous in the Series: Working with Records

Next in the Series: Creating an Interceptor with Annotations

Reading Annotations

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 you can use annotations at runtime.

 

Discovering Annotations on Elements

The page Annotations 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.

enum SerializedFormat {
    BINARY, XML, JSON
}

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@interface Bean {}

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@interface Serialized {
    SerializedFormat format() default SerializedFormat.JSON;
}

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.

@Serialized @Bean
public class Person {
}

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 annotation, and discover them.

Class<?> c = Person.class;

boolean isBean = c.isAnnotationPresent(Bean.class);
System.out.println("isBean = " + isBean);

Annotation[] annotations = c.getAnnotations();
for (Annotation annotation : annotations) {
    System.out.println("annotation = " + annotation);
}

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.

Class<?> c = Person.class;

Serialized annotation = c.getAnnotation(Serialized.class);
SerializedFormat format = annotation.format();
System.out.println("format = " + format);

Running the previous code prints the following.

format = JSON

Let us create another, repeating annotation.

enum ValidationRules {
    NON_NULL, NON_EMPTY, NON_ZERO
}

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface Validators {
    Validator[] value();
}

@Target(ElementType.FIELD)
@Repeatable(Validators.class)
@interface Validator {
    ValidationRules value();
}

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

public class Person {
    
    @Validator(ValidationRules.NON_NULL)
    @Validator(ValidationRules.NON_EMPTY)
    private String name;
}

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.

Class<?> c = Person.class;
Field nameField = c.getDeclaredField("name");

Annotation[] annotations = nameField.getAnnotations();
System.out.println("# annotations = " + annotations.length);

Validators validators = (Validators)annotations[0];
for (Validator validator : validators.value()) {
    System.out.println("validator = " + validator);
}

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.

Class<?> c = Person.class;
Field nameField = c.getDeclaredField("name");

Annotation[] annotations = nameField.getAnnotationsByType(Validator.class);
System.out.println("# annotations = " + annotations.length);

for (Annotation annotation : annotations) {
    System.out.println("annotation = " + annotation);
}

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.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface Bean {}

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

@Bean
class Person {}

class User extends Person {}

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

Class<?> c = Employee.class;

Annotation[] declaredAnnotations = c.getDeclaredAnnotations();
System.out.println("# declared annotations = " + declaredAnnotations.length);

Annotation[] annotations = c.getAnnotations();
System.out.println("# annotations = " + annotations.length);
for (Annotation annotation : annotations) {
    System.out.println("annotation = " + annotation);
}

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 through a specific interface: AnnotatedType.

Suppose that you have the following annotation. Note that this annotation can be used where the types are used, not the types themselves.

@Target(ElementType.TYPE_USE)
@Retention(RetentionPolicy.RUNTIME)
@interface NonNull {}

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

public class Person {}

public class User
        extends @NonNull Person
        implements @NonNull Serializable {
}

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

Class<?> c = Employee.class;

AnnotatedType superClass = c.getAnnotatedSuperclass();
Annotation[] superClassAnnotations = superClass.getAnnotations();
for (Annotation annotation : superClassAnnotations) {
    System.out.println("annotation on the super class = " + annotation);
}

AnnotatedType[] interfaces = c.getAnnotatedInterfaces();
for (AnnotatedType implementedInterface : interfaces) {
    Annotation[] interfaceAnnotations = implementedInterface.getAnnotations();
    for (Annotation interfaceAnnotation : interfaceAnnotations) {
        System.out.println("annotation on the implemented interface = " + interfaceAnnotation);
    }
}

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.

class EmptyStringException extends Exception {
    public EmptyStringException(String message) {
        super(message);
    }
}

@Target(ElementType.TYPE_USE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReThrowAsRuntimeException {}

public class Message {
    private final String name;
    
    public Message(String name) throws @ReThrowAsRuntimeException EmptyStringException {
        Objects.requireNonNull(name);
        if (name.isEmpty()) {
            throw new EmptyStringException("name is empty");
        }
        this.name = name;
    }
    
    public String name() {
        return name;
    }
}

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

try {
    
    Message message = new Message("");
    
} catch (EmptyStringException e) {
    
    Class<?> c = Message.class;
    Constructor<?> constructor = c.getConstructor(String.class);
    
    AnnotatedType[] annotatedExceptionTypes = constructor.getAnnotatedExceptionTypes();
    for (AnnotatedType annotatedExceptionType : annotatedExceptionTypes) {
        boolean annotationPresent =
                annotatedExceptionType.isAnnotationPresent(ReThrowAsRuntimeException.class);
        throw new RuntimeException(e);
    }
    System.out.println(e.getMessage());
    e.printStackTrace();
}

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)

Last update: July 25, 2024


Previous in the Series
Current Tutorial
Reading Annotations

Previous in the Series: Working with Records

Next in the Series: Creating an Interceptor with Annotations