skip to content

Why does an annotation need RUNTIME retention to be readable via reflection, and what happens if it doesn't have it?

level: juniorimportance: must knowfreq 70%

answer

  1. SOURCE / CLASS / RUNTIME
  2. CLASS is the default
  3. reflection sees only RUNTIME
  4. getAnnotation returns null silently
  5. frameworks always use @Retention(RUNTIME)

basics

~10 s

Reflection can only see annotations marked to survive into the running program. If an annotation isn't kept at runtime, reflection methods like getAnnotation return nothing, even though the annotation was in the source.

solid answer

~40 s

Every annotation has a retention policy set with @Retention(RetentionPolicy.X). SOURCE means the compiler discards it (e.g. @Override). CLASS (the default) keeps it in the .class file but the JVM does not load it, so reflection can't see it. RUNTIME keeps it in the class file AND makes the JVM load it into metadata, so methods such as getAnnotation, getAnnotations, and isAnnotationPresent can read it. If you write a custom annotation and rely on reflection (validation, serialization, dependency injection), you must annotate it with @Retention(RetentionPolicy.RUNTIME); otherwise isAnnotationPresent returns false and getAnnotation returns null at run time, with no error at compile time. This silent failure is a common gotcha.

code

java · 18 lines
java
import java.lang.annotation.*;
import java.lang.reflect.Method;

@Retention(RetentionPolicy.RUNTIME)   // remove this line -> defaults to CLASS
@Target(ElementType.METHOD)
@interface Audited {}

class Service {
    @Audited void save() {}
}

public class Demo {
    public static void main(String[] args) throws Exception {
        Method m = Service.class.getDeclaredMethod("save");
        System.out.println(m.isAnnotationPresent(Audited.class));
        // true with @Retention(RUNTIME); false (silently) without it
    }
}

go deeper

for a junior

Knows reflection can read annotations and that the annotation must be 'kept at runtime' (RUNTIME) to be visible.

for a middle

Names all three RetentionPolicy values, knows CLASS is the default, and recalls the silent-null failure when RUNTIME is missing.

for a senior

Explains that reflection reads JVM-loaded class metadata, distinguishes @Retention from @Target, and can debug a 'framework ignores my annotation' issue back to a missing RUNTIME retention.

for a principal

Reasons about retention trade-offs (startup metadata cost, annotation processors vs reflection vs bytecode reading), and designs annotation contracts for libraries used across module boundaries.

## What is an annotation? An **annotation** in Java is a piece of metadata you attach to code (classes, methods, fields, parameters) with the `@` syntax, e.g. `@Override`, `@Deprecated`, or a custom one like `@JsonProperty`. Metadata means 'data about the code' — it does not run by itself; some other program reads it and acts on it. ## What is reflection? **Reflection** is Java's ability, at run time, to inspect and manipulate classes, methods, and fields that it discovers while the program is running, rather than at compile time. Through `java.lang.reflect` and `java.lang.Class` you can ask 'what methods does this class have?', 'what is the type of this field?', and 'what annotations are on this method?'. ## The retention policy When you declare a custom annotation, you can control **how long it survives** using a meta-annotation (an annotation on an annotation) called `@Retention`, which takes one `RetentionPolicy` value: - `SOURCE` — the annotation exists only in the source code; the compiler throws it away. Used by tools that run at compile time (lint, `@Override`, Lombok). It never reaches the `.class` file. - `CLASS` — **this is the default if you don't specify @Retention.** The annotation is written into the compiled `.class` file (the bytecode), but when the JVM loads the class it does **not** keep the annotation in the in-memory metadata. So bytecode tools can read it, but reflection cannot. - `RUNTIME` — the annotation is written into the `.class` file AND the JVM loads it into the runtime class metadata, making it visible to reflection. ## Why reflection needs RUNTIME Reflection reads the **in-memory metadata** that the JVM builds when it loads a class. Only `RUNTIME`-retained annotations are placed into that metadata. So the reflection accessor methods on `AnnotatedElement` (the interface implemented by `Class`, `Method`, `Field`, etc.) — `getAnnotation(Class)`, `getAnnotations()`, `getDeclaredAnnotations()`, `isAnnotationPresent(Class)` — can only ever return `SOURCE` (never present), `CLASS` (in the file but not loaded → invisible), or `RUNTIME` (visible) annotations. In practice that means: **only RUNTIME annotations are reflectively visible.** ## The silent-failure trap If you forget `@Retention(RetentionPolicy.RUNTIME)` on a custom annotation that a framework reads via reflection, the code still **compiles** and the annotation still **appears in source**, but at run time `isAnnotationPresent(...)` returns `false` and `getAnnotation(...)` returns `null`. Nothing throws — the feature just silently does nothing. This is why almost every annotation used by Spring, Jackson, JUnit, Hibernate validation, etc. is declared `@Retention(RUNTIME)`. ## Declaring it ```java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MyMarker {} ``` Note `@Target` (another meta-annotation) restricts *where* the annotation may be placed; it is separate from retention but usually appears alongside it.

  • What is the default RetentionPolicy if you omit @Retention?
    CLASS — the annotation is in the .class file but is not loaded into runtime metadata, so reflection cannot read it.
  • Name a built-in annotation with SOURCE retention.
    @Override (and @SuppressWarnings). They are only meaningful to the compiler and are discarded after compilation.

saying these in an interview costs you the question

  • Thinking all annotations are visible to reflection by default
  • Believing the default retention is RUNTIME (it is CLASS)
  • Assuming a missing retention causes a compile error (it fails silently at run time)
  • Confusing @Retention (how long it lives) with @Target (where it can be placed)

context