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.
isAnnotationPresent(Class<? extends Annotation>)
: allows you to check is a given annotation is present on this element. The class you need to pass as an argument must be an annotation class. If you don't, you will get a compiler error.getAnnotations()
andgetDeclaredAnnotations()
: gives you an array with the annotations present on this element. This array can be empty if there is no such annotation. ThegetDeclaredAnnotations()
gives you the annotations declared on this element only, without the inherited annotations.getAnnotation(Class)
andgetDeclaredAnnotation(Class)
: returns an annotation for the specific type passed as an argument. The methodgetDeclaredAnnotation(Class)
only returns the annotation declared on this element, without the inherited annotations.getAnnotationsByType(Class
has been added in Java SE 8 to support repeating annotations. This method returns an array of the annotations associated with this element. If it finds a repeating annotation, it opens it to return the repeating element.
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