Back to Blog
Java

Reading Java Reflection Annotations at Runtime

java reflection annotations: Learn how to use Java reflection to read custom annotations at runtime, including retention policies, method and field inspection, and pra...

reflectionannotationsruntimemetadatacustom-annotations
Illustration of a magnifying glass inspecting annotation symbols on a Java class diagram, representing runtime reflection and metadata access.

When you need to read java reflection annotations at runtime, the first thing to understand is that not all annotations are visible to reflection. The visibility depends on the annotation's retention policy. Without the right retention, your code will compile but getAnnotation() will return null at runtime. This article walks through the exact steps to declare, inspect, and act on annotations using the reflection API, and covers the performance and correctness tradeoffs you'll encounter in real applications.

Declaring an Annotation That Survives to Runtime

Annotations are only available to reflection if their @Retention policy is RUNTIME. The default is CLASS, which means the annotation is recorded in the class file but not loaded into the JVM's runtime memory. To read an annotation via reflection, you must explicitly set the retention.

import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) public @interface Audit { String action() default "read"; boolean logResult() default false; }

The @Target meta-annotation restricts where the annotation can be applied. It's not strictly required for reflection, but it prevents accidental misuse. The @Retention(RetentionPolicy.RUNTIME) line is the key: it tells the JVM to keep the annotation metadata accessible during execution.

Accessing Class-Level Annotations

Once an annotation has runtime retention, you can retrieve it from a Class object using getAnnotation(). This method returns the annotation instance if present, or null if the annotation is not found.

Class<?> clazz = OrderService.class; Audit audit = clazz.getAnnotation(Audit.class); if (audit != null) { System.out.println("Class action: " + audit.action()); }

getAnnotation() works for any annotation type, including built-in ones like @Deprecated or @Override. For a complete view, getAnnotations() returns all annotations present on the element, and getDeclaredAnnotations() returns only those directly declared (ignoring inherited ones).

Inspecting Methods and Fields

Reflection gives you the same access to methods and fields. Use getDeclaredMethods() to avoid inherited methods that might carry annotations from superclasses, then call getAnnotation() on each Method object.

for (Method method : clazz.getDeclaredMethods()) { Audit audit = method.getAnnotation(Audit.class); if (audit != null) { System.out.println("Method " + method.getName() + " logs: " + audit.action()); } }

Fields follow the same pattern with Field.getAnnotation(). This is useful for dependency injection or validation frameworks that inspect object state. The key point is that reflection treats annotations as part of the element's metadata, so you can query them uniformly across classes, methods, fields, and parameters.

Using Annotation Values to Drive Behavior

Annotations become powerful when their values influence runtime behavior. Consider a simple audit framework that logs method calls based on the @Audit annotation.

public class AuditInterceptor { public static void invoke(Object target, Method method, Object... args) throws Exception { Audit audit = method.getAnnotation(Audit.class); if (audit != null) { System.out.println("Action: " + audit.action()); Object result = method.invoke(target, args); if (audit.logResult()) { System.out.println("Result: " + result); } return; } method.invoke(target, args); } }

This pattern is common in frameworks like Spring, where annotations like @Transactional or @Cacheable are read via reflection (or bytecode proxies) to trigger cross-cutting behavior. The annotation values act as configuration, and reflection is the mechanism that reads them.

Performance: Caching Reflection Results

Reflection calls are significantly slower than direct code because the JVM must perform type checks and access metadata dynamically. If you're inspecting annotations on every method invocation, the overhead can become noticeable. The standard solution is to cache the annotation lookups.

public class AnnotationCache { private final Map<Method, Audit> cache = new ConcurrentHashMap<>(); public Audit getAudit(Method method) { return cache.computeIfAbsent(method, m -> m.getAnnotation(Audit.class)); } }

Caching is especially important in high-throughput services. The first lookup may take microseconds, but subsequent lookups become a simple map access. For class-level annotations, you can cache on the Class object as well. Remember that reflection results are stable as long as the class is not dynamically modified, so caching is safe.

Common Pitfalls with Proxies and Inheritance

Two issues frequently break runtime annotation inspection. First, annotations are not inherited by default. If a subclass overrides a method that has an annotation in the superclass, the subclass method will not show that annotation unless the annotation itself is marked with @Inherited. This meta-annotation only affects class-level annotations, not methods or fields.

Second, when you use dynamic proxies (e.g., java.lang.reflect.Proxy), the proxy class may not expose the original method's annotations. The proxy's Method object is not the same as the target class's method. If you need annotations on proxied objects, you must extract the underlying target class and inspect its methods directly.

When to Prefer Annotation Processing Over Reflection

Reflection gives you runtime flexibility, but it introduces overhead and can obscure compile-time safety. If you only need to generate code or validate annotations at build time, consider using the annotation processing API (javax.annotation.processing). This runs during compilation, so it can catch errors early and avoid runtime reflection entirely. However, annotation processing cannot influence behavior that depends on dynamic runtime state. Use reflection when you need to react to annotations at runtime; use annotation processing when you can generate code or validate at compile time.

The decision comes down to whether the annotation's effect is static or dynamic. For a framework that must support arbitrary user classes without recompilation, reflection is the only option. For a library that controls its own annotations, compile-time processing often yields better performance and type safety.

java reflection annotations: Practical Usage and Code Exampl | RYUSLOG DEV