🪞 Reflection, Annotations & Modules · Advanced

Custom annotations in Java

@interface, elements, @Retention, @Target, @Inherited, @Repeatable.

🧩 The mysteryYou write @Timed on a method, but your metrics library never notices it. The annotation is right there in the source. Where did it go?

Declaring your own

An annotation type is declared with **@interface. Its elements look like methods and can have defaults**.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Timed {
    String value() default "";
    int warnMs() default 500;
}

What elements may be

Element types are limited to **primitives, String, Class, enums, other annotations, and arrays of these**. No wrapper types like Integer, no List, and no null defaults.

⚠️ The trap

Integer is not allowed

Wrapper types aren't on the list, so this is a compile error. Use the primitive instead: int port() default 8080;

@interface Config {
    Integer port() default 8080; // error
}

Retention and target

**@Retention says how long it's kept: SOURCE (javac only), CLASS (the default: stored in the .class file, but invisible to reflection) or RUNTIME (readable via reflection). @Target** says where it may go: METHOD, TYPE, FIELD and so on.

🔮 Predict it

Count what reflection sees

What does this print?

@Retention(RetentionPolicy.SOURCE)
@interface Note { }
@interface Plain { }
@Retention(RetentionPolicy.RUNTIME)
@interface Kept { }
@Note @Plain @Kept class Box { }
void main() {
    int n = Box.class.getAnnotations().length;
    System.out.println(n);
}
  1. 3
  2. 1
  3. 0
Show the answer

Only **@Kept** has RUNTIME retention. @Note is SOURCE (gone after javac). @Plain has no @Retention, so it defaults to CLASS: stored in the file, but invisible to reflection.

@Inherited and @Repeatable

**@Inherited: a class annotation on a superclass is also found when you ask the subclass. @Repeatable: the annotation may appear several times on one element, but it needs a container** annotation with an array value(). Read repeats with getAnnotationsByType.

@Repeatable(Roles.class)
@interface Role { String value(); }
@interface Roles { Role[] value(); }
 
@Role("admin") @Role("dev")
class Ops { }
💼 In the real world

Silent failures

Custom annotations power @Timed metrics, validation like @NotBlank and @Transactional. The classic bug: forgetting @Retention(RUNTIME). Nothing fails; the framework simply sees nothing, and your metrics stay empty.

Key takeaways

  1. Retention: SOURCE, CLASS (the default) or RUNTIME
  2. Only RUNTIME annotations are visible to reflection
  3. @Inherited works for class annotations from superclasses
  4. @Repeatable needs a container annotation
🤯 Did you know?

If an annotation's only element is named value, you can leave out the name: @Role("admin") instead of @Role(value = "admin").

Practice questions

What does this print?

@interface Plain { }
@Retention(RetentionPolicy.RUNTIME)
@interface Kept { }
 
@Plain @Kept class Box { }
void main() {
    var anns = Box.class.getAnnotations();
    System.out.println(anns.length);
}
  1. 2
  2. 1
  3. 0
Check your answer

1. @Kept is RUNTIME, so reflection sees it. @Plain has no @Retention, so it defaults to CLASS: stored in the .class file but invisible at runtime.

What does this print?

@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface Team { String value(); }
 
@Team("core") class Base { }
class Sub extends Base { }
void main() {
    var t = Sub.class.getAnnotation(Team.class);
    System.out.println(t.value());
}
  1. core
  2. Throws NullPointerException
  3. Compile error
  4. Sub
Check your answer

core. Team is @Inherited, so asking Sub for it finds the annotation on its superclass Base.

An annotation is just a sticky note. Who actually reads it, and when? Next: runtime scanning vs compile-time processors.