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, andConstructortypes let you get/set fields, invoke methods, and create instances. The cost is performance and visibility (you needsetAccessiblefor 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
JavaMethod 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
JavaField 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
Javafor (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 thatgetDeclared*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
| Scenario | Example annotations | Who reads them and what they do |
|---|---|---|
| Dependency injection | @Autowired, @Resource | Container scans, injects via reflection or proxy |
| Transaction | @Transactional | AOP starts/commits a transaction around the method |
| Validation | @NotNull, @Size | Validation framework reads annotations and validates fields/params |
| Serialization | @JsonProperty, @JsonIgnore | Jackson reads annotations to control field names and what to ignore |
| Mapping | @Mapper, @Select | MyBatis scans interfaces and generates implementations |
Core Mechanism / Behavior
- Reflection: The JVM exposes
Classand related objects.getMethod,getField, etc. may trigger class loading and resolution.invokehas checks and boxing overhead; avoid reflection on hot paths. - Annotations: Compile-time retention can be SOURCE/CLASS or RUNTIME. Reflection APIs like
getAnnotationandgetDeclaredAnnotationsread 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
MethodandFieldreferences when you must use reflection; avoid repeated lookups in loops.
Common Pitfalls
- InvocationTargetException:
Method.invokewraps the real exception. UsegetCause()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
getMethodorgetDeclaredFieldinside hot loops; cache the references once.
When to Use Reflection
| Use reflection | Avoid reflection |
|---|---|
| Framework/plugin loading | Business logic with known types |
| Serialization/deserialization | High-frequency code paths |
| DI containers, validation | When direct call is possible |
| Testing mocks and spies | Simple 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:
setAccessiblecan 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.