Stage 7: Tools and testing, lesson 6 of 7

Annotations and reflection

Advanced3 min read@since 16Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

An annotation attaches information to classes, methods, fields or parameters. Some are for the compiler (@Override, @FunctionalInterface, @Deprecated); most are read by frameworks at run time (@Entity, @RestController, @Test).

Defining your own:

  • @Retention(RUNTIME) keeps it available to reflection; the default, CLASS, doesn't.
  • @Target(FIELD) says where it may be used.
  • Elements act like settings: int visible() default 4;

Reflection (java.lang.reflect) inspects and uses classes at run time: list methods and fields, read annotations, create objects and call methods by name. Spring, Hibernate, Jackson and JUnit are built on it.

Reflection is powerful but slower, skips compile-time checks and can break encapsulation, so application code rarely needs it directly. Annotation processors such as Lombok and MapStruct run at compile time instead and generate code.

Example

Java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Masked {
    int visible() default 4;                        // show only the last N characters
}

public record Customer(String name, @Masked String phone, @Masked(visible = 2) String pan) {}

public static String describe(Object obj) throws IllegalAccessException {
    StringBuilder out = new StringBuilder(obj.getClass().getSimpleName()).append("[");
    for (Field f : obj.getClass().getDeclaredFields()) {
        f.setAccessible(true);
        String value = String.valueOf(f.get(obj));
        Masked m = f.getAnnotation(Masked.class);
        if (m != null && value.length() > m.visible()) {
            int hidden = value.length() - m.visible();
            value = "*".repeat(hidden) + value.substring(hidden);
        }
        out.append(f.getName()).append("=").append(value).append(" ");
    }
    return out.append("]").toString();
}

System.out.println(describe(new Customer("Asha", "9876543210", "ABCDE1234F")));
// Customer[name=Asha phone=******3210 pan=********4F ]

Common mistake

Forgetting @Retention(RUNTIME) on a custom annotation. getAnnotation returns null at run time and your framework code silently does nothing.

Under the hood

Reflection into JDK internals is blocked by strong encapsulation (Java 16+), and Java 26 warns when deep reflection changes final fields, part of making final truly final. Frameworks increasingly generate code at build time (Spring AOT, Micronaut, Quarkus) for faster startup and for GraalVM native images, where reflection must be declared in advance.

Check yourself

Which retention makes an annotation readable at run time?

How this connects

Part of Job-ready backend developer.

Was this lesson helpful?

Finished reading? Mark it complete to track your progress.