Java Reflection: Reading Annotations
Read annotation metadata at runtime in Java with reflection — getAnnotation, getAnnotations.
Java Reflection: Reading Annotations
Part 16 covered declaring annotations; this chapter is about reading them back at runtime through reflection. An annotation with @Retention(RUNTIME) becomes queryable on the Class, Method, Field, Constructor, and Parameter it's attached to. Reading annotations is how JUnit finds @Test, how Spring finds @Autowired, and how validation frameworks find @NotNull. This chapter gathers the full reading API in one place, including the @Inherited and @Repeatable wrinkles.
The four reading methods
Every annotatable element (Class, Method, Field, Constructor, Parameter) implements AnnotatedElement, which defines the same four methods everywhere:
AnnotatedElement el = SomeClass.class; // or a Method, Field, etc.
el.isAnnotationPresent(Audited.class); // boolean — quick check
el.getAnnotation(Audited.class); // the annotation instance, or null
el.getAnnotations(); // ALL annotations (declared + inherited)
el.getDeclaredAnnotations(); // only those declared directly hereBecause the API is uniform, the code to read an annotation off a method is identical to reading one off a class — you just hold a different AnnotatedElement. Retrieved annotation values are JVM-generated proxies; calling a.value() is a real method call returning the element value baked in at compile time.
Retention is a prerequisite
This bears repeating because it's the number-one bug: only RUNTIME-retained annotations are visible to reflection.
@Retention(RetentionPolicy.RUNTIME) // <-- required for reflection
@interface Audited { String value(); }The default retention is CLASS, which keeps the annotation in the .class file but discards it before runtime. SOURCE retention drops it even earlier. If getAnnotation returns null on an annotation you can plainly see in the source, the missing @Retention(RUNTIME) is almost always why.
getAnnotations vs getDeclaredAnnotations and @Inherited
The difference between these two is @Inherited. By default annotations are not inherited by subclasses. But if an annotation type is itself meta-annotated @Inherited, then a subclass inherits a class-level annotation from its superclass:
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@interface Component { }
@Component class Base { }
class Derived extends Base { } // Derived has no @Component in source
Derived.class.getAnnotation(Component.class) // → present! (inherited)
Derived.class.getDeclaredAnnotation(Component.class) // → null (not declared here)So getAnnotations() includes inherited annotations; getDeclaredAnnotations() reports only what's physically written on that element. Two important limits: @Inherited works only for class annotations (not methods or fields), and only along the superclass chain (not interfaces).
Repeatable annotations
Since Java 8, an annotation marked @Repeatable can appear multiple times on one element. Under the hood the compiler bundles repeats into a container annotation, so plain getAnnotation won't see them — you use getAnnotationsByType, which transparently unpacks the container:
@Repeatable(Roles.class)
@Retention(RetentionPolicy.RUNTIME)
@interface Role { String value(); }
@Retention(RetentionPolicy.RUNTIME)
@interface Roles { Role[] value(); } // the container
@Role("admin") @Role("user") class Account { }
Account.class.getAnnotationsByType(Role.class); // → [Role(admin), Role(user)]
Account.class.getAnnotation(Role.class); // → null! (it's wrapped in Roles)Use getAnnotationsByType(Role.class) for repeatable annotations; it returns an array and handles both the single and repeated cases.
Reading parameter and other targets
Parameters get their own annotations via the two-dimensional Method.getParameterAnnotations() (an array per parameter), or the cleaner Parameter API:
for (Parameter p : method.getParameters()) {
if (p.isAnnotationPresent(NotNull.class)) { /* validate */ }
}The same AnnotatedElement methods work on Field, Constructor, Package, and even on annotations themselves (to read meta-annotations like @Retention).
A worked example: a mini validation scanner
The program declares runtime annotations, marks @Inherited and @Repeatable cases, and then a generic scanner walks a class's annotations, its methods' annotations, and its method parameters' annotations — the skeleton of a validation or routing framework.
What to take from the run:
getAnnotation(Service.class)returned a live proxy whosevalue()gave back"users"— the value written in source. Reading an annotation is just calling its element methods; the framework reacts to those values (here, treating"users"as a route prefix). The annotation carries data, the scanner supplies behaviour.AdminControllerreported@Servicepresent but not declared:isAnnotationPresentreturnedtrue(inherited fromUserController) whilegetDeclaredAnnotationreturnednull. That gap is entirely due to the@Inheritedmeta-annotation, and it works only because@Servicetargets a class — methods and fields never inherit annotations this way.list.getAnnotation(Role.class)returnednulleven though two@Roleannotations are right there in the source. Repeatable annotations are wrapped in aRolescontainer by the compiler, so the single-value getter misses them;getAnnotationsByType(Role.class)unpacked the container and returned both roles. Always usegetAnnotationsByTypefor repeatable annotations.- Parameter annotations were reachable per-parameter: the
tenantparameter reported@NotNullpresent andpagedid not. This per-parameter granularity is what bean-validation and request-binding frameworks use to validate or inject individual arguments. getDeclaredAnnotations()onlistcounted two annotations —@Endpointand the syntheticRolescontainer — confirming that the two@Roles collapsed into one container at the class-file level. Any annotation lacking@Retention(RUNTIME)would not have appeared in that count at all.
Practice
A framework marks methods with a repeatable annotation '@Role' (a method can have several). On a method annotated '@Role('admin') @Role('editor')', calling 'method.getAnnotation(Role.class)' returns null. Why, and what should be called instead?