Frameworks like Spring, JUnit, Jackson and Hibernate are built on annotations and reflection; understanding them demystifies the magic.
An annotation attaches metadata to code. Some are for the compiler (@Override, @Deprecated, @SuppressWarnings, @FunctionalInterface); most in real projects are read by frameworks (@Test, @Entity, @GetMapping).
Declaring one uses @interface, with two meta-annotations that matter:
@Retention—SOURCE(discarded by the compiler),CLASS(in the class file, the default) orRUNTIME(readable via reflection — what frameworks need).@Target— where it may appear:TYPE,METHOD,FIELD,PARAMETER, and more.
Annotation elements look like methods and can have defaults: String value() default "";. An element named value can be set without a name: @Route("/home").
Reflection (java.lang.reflect) lets code inspect classes at runtime. Starting from a Class<?> object (obj.getClass(), Foo.class, Class.forName(name)), you can list getDeclaredMethods(), getDeclaredFields() and constructors, read annotations with getAnnotation(...), and call method.invoke(target, args) or constructor.newInstance(...). Exceptions thrown by the invoked method arrive wrapped in InvocationTargetException.
Costs and limits:
- It bypasses compile-time checks: typos in names fail at runtime.
- It is slower than direct calls (though the JIT and
MethodHandles narrow the gap). setAccessible(true)on private members is blocked across module boundaries unless the package is opened — the module system enforces strong encapsulation of JDK internals.
Use reflection to build frameworks and tooling, not in everyday business logic. Annotation processors offer a compile-time alternative that generates code (Lombok, MapStruct, Dagger).
Example
import java.lang.annotation.*;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;
public class MiniRouter {
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD)
@interface Route { String value(); }
public static class Handlers {
@Route("/home") public String home() { return "Welcome"; }
@Route("/about") public String about() { return "About us"; }
public String helper() { return "not routed"; }
}
public static void main(String[] args) throws Exception {
var target = new Handlers();
Method[] methods = Handlers.class.getDeclaredMethods();
Arrays.sort(methods, Comparator.comparing(Method::getName)); // order is unspecified
for (Method m : methods) {
Route r = m.getAnnotation(Route.class);
if (r != null) System.out.println(r.value() + " -> " + m.invoke(target));
} // prints "/about -> About us" then "/home -> Welcome"
}
}Remember
- Frameworks need
@Retention(RUNTIME)annotations to see them via reflection - Reflection inspects and invokes members at runtime, trading safety and speed for flexibility
- The module system blocks deep reflection into packages that aren't opened
Watch out for
- Forgetting
@Retention(RetentionPolicy.RUNTIME), sogetAnnotationalways returns null.