Start Learning
Javaneer
Back to stage
Stage 4·Advanced Java

Annotations & Reflection

Built-in and custom annotations, the reflection API, and how frameworks use them.

18 min readAdvanced
On this page

Ever wondered how Spring wires your app together, or how JUnit finds your test methods? The magic is annotations and reflection - the tools that let code inspect and act on other code. Understanding them demystifies every major Java framework.

Annotations: metadata on code

An annotation is a label you attach to code - a class, method, or field - that carries information for tools, frameworks, or the compiler. You've already used some:

@Override          // tells the compiler: this replaces a parent method
@Deprecated        // marks something as outdated
@FunctionalInterface  // enforces a single abstract method
Annotations are like sticky labels on boxes

Imagine warehouse boxes with labels: "Fragile", "This Side Up", "Priority". The boxes work fine without them, but the labels tell the handlers how to treat each one. Annotations are labels on your code that tell frameworks and tools how to handle it - without changing what the code itself does.

Defining your own annotation

You can create custom annotations. They can hold values:

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)   // keep it available at runtime
@Target(ElementType.METHOD)           // only allowed on methods
@interface Test {
    String description() default "";
}
  • @Retention controls how long the annotation survives: SOURCE (compiler only), CLASS, or RUNTIME (available while the program runs - needed for frameworks).
  • @Target restricts where it can be applied (method, field, class…).
class MathTests {
    @Test(description = "addition works")
    void testAdd() { /* ... */ }
}

Reflection: code inspecting code

Reflection lets a program examine and manipulate classes, methods, and fields at runtime - even ones it didn't know about at compile time.

Class<?> clazz = MathTests.class;

for (Method method : clazz.getDeclaredMethods()) {
    if (method.isAnnotationPresent(Test.class)) {
        System.out.println("Found test: " + method.getName());
    }
}
Reflection is like an X-ray for objects

Normally you interact with an object through its public methods - you see its surface. Reflection is an X-ray: it lets you peer inside at runtime, discover every field and method, read their annotations, and even invoke them by name. It sees what's normally hidden.

Building a mini test runner

Put them together and you've built the core of a testing framework - the same idea JUnit uses:

Object instance = clazz.getDeclaredConstructor().newInstance();

for (Method method : clazz.getDeclaredMethods()) {
    if (method.isAnnotationPresent(Test.class)) {
        try {
            method.invoke(instance);          // run the test method
            System.out.println("PASS: " + method.getName());
        } catch (Exception e) {
            System.out.println("FAIL: " + method.getName());
        }
    }
}

This is genuinely how frameworks work: annotations mark intent, reflection discovers and acts on them.

How frameworks use this

FrameworkAnnotationWhat reflection does
Spring@AutowiredFinds and injects dependencies
JUnit@TestDiscovers and runs test methods
JPA/Hibernate@EntityMaps classes to database tables
Jackson@JsonPropertyMaps fields to JSON keys

Powerful, but use sparingly

Reflection is slower than direct calls and bypasses compile-time safety (mistakes surface at runtime). It's essential for frameworks, but in everyday application code you should rarely need it. Reach for normal method calls first.

Quick check

What RetentionPolicy must a custom annotation have for a framework to read it while the program runs?

Key takeaways

  • Annotations attach metadata to code for compilers, tools, and frameworks - without changing behavior.
  • Custom annotations use @Retention (lifespan) and @Target (where allowed); RUNTIME is needed for frameworks.
  • Reflection inspects and manipulates classes, methods, and fields at runtime.
  • Together they power frameworks: annotations mark intent, reflection discovers and acts on it.
  • Reflection is powerful but slow and unsafe - use it sparingly in ordinary application code.

Next, we go even deeper - inside the JVM itself: class loading, memory, the JIT, and garbage collection.