Reflection & Annotations - Practical Use Cases

Reflection lets you inspect and invoke classes, methods, fields, and constructors at runtime. It is widely used in frameworks, serialization, dependency injection, and validation. Annotations provide metadata; combined with reflection, they drive behavior at runtime (e.g. @Valid, @Transactional, @Mapper). This article covers typical usage, caveats, and a reference table so you can use them correctly in both business and framework code.

Overview

  • Reflection: The Class, Method, Field, and Constructor types let you get/set fields, invoke methods, and create instances. The cost is performance and visibility (you need setAccessible for private members). Use reflection for frameworks and generic logic; prefer direct calls in business code.
  • Annotations: Declarative metadata with retention (SOURCE/CLASS/RUNTIME) and target (TYPE/METHOD/FIELD, etc.). Only RUNTIME annotations can be read at runtime via reflection.
  • Typical combination: Frameworks scan for classes/methods with certain annotations and use reflection to create instances or invoke methods (e.g. Spring @Component, JUnit @Test, MyBatis @Mapper, Bean Validation).

Example

Example 1: Reflection — invoking a method by name

Java
Method m = Foo.class.getMethod("bar", String.class);
m.invoke(fooInstance, "arg");
  • Use when: plugins, scripting, or calling different methods based on config. Watch for wrapped exceptions (InvocationTargetException), performance, and access control.

Example 2: Reflection — accessing private fields

Java
Field f = MyClass.class.getDeclaredField("secret");
f.setAccessible(true);
Object value = f.get(instance);
  • setAccessible(true) bypasses normal access checks. Use sparingly; it can break encapsulation and fail under security managers.

Example 3: Annotations + reflection — simple validation

Java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface NotNull {}

public static void validate(Object obj) {
    for (Field f : obj.getClass().getDeclaredFields()) {
        if (f.getAnnotation(NotNull.class) != null) {
            f.setAccessible(true);
            if (f.get(obj) == null) throw new ValidationException(f.getName() + " is null");
        }
    }
}
  • This is illustrative; in practice use Bean Validation (@NotNull etc.), which relies on annotations and reflection internally.

Example 4: Getting all methods and iterating

Java
for (Method m : MyClass.class.getDeclaredMethods()) {
    if (m.isAnnotationPresent(Deprecated.class)) {
        System.out.println("Deprecated: " + m.getName());
    }
}
  • Use getDeclaredMethods() for all methods (including private); getMethods() returns only public ones including inherited. Remember that getDeclared* does not include inherited members.

Example 6: Annotation retention and target

Java
@Retention(RetentionPolicy.RUNTIME)  // Available at runtime for reflection
@Target({ElementType.METHOD, ElementType.FIELD})
public @interface Audited {
    String value() default "";
}
  • SOURCE: discarded after compilation (e.g. @Override). CLASS: in bytecode but not loaded at runtime. RUNTIME: loadable by JVM, readable via reflection.

Example 7: Common annotation usage

ScenarioExample annotationsWho reads them and what they do
Dependency injection@Autowired, @ResourceContainer scans, injects via reflection or proxy
Transaction@TransactionalAOP starts/commits a transaction around the method
Validation@NotNull, @SizeValidation framework reads annotations and validates fields/params
Serialization@JsonProperty, @JsonIgnoreJackson reads annotations to control field names and what to ignore
Mapping@Mapper, @SelectMyBatis scans interfaces and generates implementations

Core Mechanism / Behavior

  • Reflection: The JVM exposes Class and related objects. getMethod, getField, etc. may trigger class loading and resolution. invoke has checks and boxing overhead; avoid reflection on hot paths.
  • Annotations: Compile-time retention can be SOURCE/CLASS or RUNTIME. Reflection APIs like getAnnotation and getDeclaredAnnotations read only RUNTIME annotations.
  • AOP: Many annotation-driven behaviors are implemented with AOP: the framework scans beans for annotations, creates proxies, and weaves logic before/after calls (e.g. transactions, logging). This may not require explicit reflection in your code.
  • Performance: Reflection is slower than direct calls. Cache Method and Field references when you must use reflection; avoid repeated lookups in loops.

Common Pitfalls

  • InvocationTargetException: Method.invoke wraps the real exception. Use getCause() to get the underlying error.
  • NoSuchMethodException: Method name or parameter types may not match (overloading, primitives vs wrappers). Double-check signatures.
  • Annotation not found: Only RUNTIME retention annotations are visible; SOURCE and CLASS are not available at runtime.
  • Performance in loops: Avoid calling getMethod or getDeclaredField inside hot loops; cache the references once.

When to Use Reflection

Use reflectionAvoid reflection
Framework/plugin loadingBusiness logic with known types
Serialization/deserializationHigh-frequency code paths
DI containers, validationWhen direct call is possible
Testing mocks and spiesSimple object creation

Key Rules

  • Prefer direct calls over reflection; use reflection for frameworks, generic utilities, and when types cannot be determined at compile time.
  • Annotations: Define retention and target clearly. Only RUNTIME can be read at runtime. Prefer standard annotations (JSR 303/380, Spring, Jackson) where possible.
  • Performance: Do reflection and annotation scanning at startup or first use; avoid reflection in hot paths. Cache Method/Field references.
  • Security: setAccessible can bypass encapsulation; use with care, especially when handling untrusted input.

What's Next

See Spring IOC and Bean lifecycle and Spring AOP for annotation- and reflection-driven behavior. See MyBatis Mapper for annotation-based mapping.