Reflection is one of those features that feels magical when you first meet it, but behind the scenes it's just the JVM giving you a flashlight and a set of tools to peek inside classes at runtime.
This repo is both code + guide. Run the code in while reading this file. Each section here explains the concepts, scenarios, and caveats. Each code package shows those ideas in action.
Imagine you're writing a framework. You don't know which classes your users will write, what constructors they'll provide, or which methods they'll expose. You only discover that at runtime.
Reflection is the JVM's built-in "X-ray & remote control" that lets you:
- Inspect any class that's been loaded (its fields, methods, constructors, annotations).
- Read & write field values (even private ones).
- Call methods by name and signature, even if you didn't code them directly.
- Instantiate objects through chosen constructors.
Frameworks like Spring, Hibernate, Jackson, JUnit... all
lean on reflection heavily. That's how they can wire dependencies, map
JSON, auto-detect tests, or hook up HTTP routes without you doing new
everywhere.
↪️ In short: Reflection trades compile-time certainty for runtime flexibility.
- Dependency Injection containers (Spring).
- Serialization / Deserialization (Jackson, Gson).
- Plugin systems / dynamic module loading (lombok).
- Testing frameworks (JUnit, Mockito).
- Generic tools like ORMs, mappers, routers.
- You compile
.java
→.class
. These contain metadata: names, types, modifiers, annotations. - A ClassLoader loads the bytes and creates a
Class<?>
object = the runtime "mirror" of your type. - That
Class<?>
lets you explore declared members (fields, methods, constructors). - From those handles, you can invoke, set, get, or instantiate.
flowchart LR
A[.class file] --> B[ClassLoader]
B --> C[Class<?> mirror]
C -->|methods/fields/ctors| D[Reflective Handles]
D -->|invoke/get/set/newInstance| E[Real Object Behavior]
See code here: Reflect Methods
getDeclaredMethods()
→ all methods declared in that class (includingprivate
).getMethods()
→ onlypublic
, including inherited ones.- Look up specific methods with
getDeclaredMethod("name", ParamTypes...)
.
- Instance →
method.invoke(instance, args...)
- Static →
method.invoke(null, args...)
- Return is
Object
→ cast if needed.
- Always match parameter types exactly (
int.class
vsInteger.class
matters). - Exceptions are wrapped in
InvocationTargetException
→ unwrap cause.
NoSuchMethodException
: Thrown when the method name or parameter signature does not match any method declared in the class.IllegalAccessException
: Raised when attempting to invoke a private method without first enabling accessibility usingsetAccessible(true)
.
See code here: Reflect Fields
getDeclaredFields()
→ every field declared on that class, any visibility.getFields()
→ onlypublic
, including from superclasses.
- Instance field:
field.get(obj)
/field.set(obj, value)
. - Static field: pass
null
instead of an instance. - Private field: call
setAccessible(true)
first.
getType()
→ erasure type (e.g.,List
).getGenericType()
→ showsList<String>
or similar.- Cast to
ParameterizedType
to extract the actual arguments.
- Declared vs inherited matters:
getDeclaredFields()
on a child won't show parent's fields.getFields()
will show public parent fields.
- Don't rely on changing them: reflection may appear to "succeed" but the JVM can inline constants or ignore the write.
- Safe assumption: final is read-only.
sequenceDiagram
participant U as Your Code
participant R as Reflection API
participant O as Object
U->>R: getDeclaredField("count")
R-->>U: Field handle
U->>R: setAccessible(true)
U->>O: field.set(obj, value)
O-->>U: field value updated
See code here: Reflect Constructors
getDeclaredConstructors()
→ all cnstructors, any visibility.getConstructors()
→ only public ones.
- Call
constructor.newInstance(args...)
. - Match argument types exactly.
- For
private
constructors, usesetAccessible(true)
.
- Constructors are not inherited.
- A child must declare its own constructors and call
super(...)
.
-
NoSuchMethodException
Thrown when the requested constructor signature does not exactly match any declared constructor in the class. -
IllegalAccessException
Occurs when attempting to call a non-public constructor without first enabling accessibility usingsetAccessible(true)
. -
InvocationTargetException
Wraps any exception that the constructor itself throws during object instantiation. The original cause can be retrieved usinggetCause()
.
flowchart LR
A[Class<?>] --> B[getDeclaredConstructors]
B --> C[match signature]
C --> D[call setAccessible if private]
D --> E[newInstance with args]
E --> F[Fresh object]
Put together:
- Scan classes at runtime.
- Read metadata (annotations).
- Pick constructors and build objects.
- Inject dependencies into fields.
- Dispatch calls by invoking methods dynamically.
That's how Spring wires @Autowired
beans, how JUnit finds @Test
methods, and how Jackson maps JSON keys onto your object fields.
sequenceDiagram
participant S as Scanner
participant M as Metadata
participant I as Instantiator
participant W as Injector
S->>M: discover classes & members
M-->>I: tell what to build
I->>W: object created
W->>App: fields set, ready to run
Annotations are metadata attached to classes, methods, fields, or parameters.
On their own, they are inert - but through Reflection, frameworks can scan, detect, and act on them at runtime.
Think of annotations as labels you place on your code. Reflection is the scanner that reads those labels and turns them into behavior.
Every annotation must declare how long it should live:
RetentionPolicy.SOURCE
→ discarded by the compiler (e.g.,@Override
).RetentionPolicy.CLASS
→ kept in the.class
file but not visible at runtime.RetentionPolicy.RUNTIME
→ kept and visible via reflection.
↪️ Frameworks like Spring require RUNTIME
so they can inspect annotations dynamically.
flowchart TD
A[Annotation in source] --> B[Retention Policy]
B -->|SOURCE| C[Discard after compile]
B -->|CLASS| D[In .class file only]
B -->|RUNTIME| E[Visible via Reflection]
General Idea
Class-level annotations mark the role or category of a class.
See code here: Class Annotations Code Example
flowchart LR
A[Class Loaded] --> B[Reflection API]
B --> C{Annotation Present?}
C -->|Yes| D[Register / Handle]
C -->|No| E[Skip]
In Spring
@Component
,@Controller
,@Service
,@Repository
- During classpath scanning, Spring checks for these annotations with reflection.
- If found, the class is registered as a bean in the ApplicationContext.
-
Missing
@Retention(RUNTIME)
If the retention policy is not set toRUNTIME
, the annotation will not be available for reflection, making it invisible to frameworks at runtime. -
Misunderstanding
@Inherited
The@Inherited
meta-annotation only applies to class-level annotations. It does not propagate to methods, fields, or parameters.
General Idea
Method-level annotations declare that a method has a special behavior.
See code here: Method Annotations Code Example
flowchart LR
A[Method Discovered] --> B[Reflection API]
B --> C{Annotation Present?}
C -->|Yes| D[Extract Metadata]
D --> E[Register Action / Route]
C -->|No| F[Ignore]
In Spring MVC
@GetMapping
,@PostMapping
,@RequestMapping
- Spring scans controller methods, reads route info, and registers mappings.
- On HTTP request, it locates the method and calls
method.invoke(controller, args...)
.
-
Duplicate mappings
Defining two methods with the same route mapping creates ambiguity and leads to runtime conflicts. -
Overloaded methods
Overloaded methods with the same name must be distinguished by exact parameter signatures; otherwise, reflection cannot resolve the correct method. -
Private methods
Invoking private methods requires explicitly enabling accessibility withsetAccessible(true)
.
General Idea
Field-level annotations can signal that a dependency should be injected.
Seecodehere: Field Annotations Code Example
flowchart LR
A[Object Instance] --> B[Reflection API]
B --> C[Scan Fields]
C --> D{Annotation Present?}
D -->|Yes| E[Resolve Dependency]
E --> F["Inject via field.set()"]
D -->|No| G[Skip]
In Spring
@Autowired
,@Inject
- Spring detects annotated fields, resolves the bean type, and sets it via reflection.
-
No matching bean
If no bean of the required type exists in the application context, the framework raises a runtime injection exception. -
Multiple candidates without qualification
When more than one bean matches the type, you must specify which one to inject using@Qualifier
. -
Final fields
Attempting to inject intofinal
fields is unsafe and discouraged, as the assignment cannot be reliably completed. -
Static injection
Injecting dependencies into static fields is strongly discouraged, as it breaks object-oriented design and leads to hard-to-test code.
Parameters can carry metadata to bind values.
See code here: Parameter Annotations Code Example
flowchart LR
A[Method Parameters] --> B[Reflection API]
B --> C{Annotation Present?}
C -->|Yes| D[Extract Metadata]
D --> E[Bind Data / Convert]
C -->|No| F[Ignore]
In Spring MVC
@RequestParam
,@PathVariable
- Spring inspects method parameters, extracts values from the request, converts them, and invokes the method.
-
Missing values
If a required parameter is not provided in the request, the framework may injectnull
or throw an error depending on the configuration. -
Type conversion failures
Occur when the provided input cannot be converted to the expected type (e.g., attempting to parse"abc"
into aLong
). -
Primitive type limitations
Primitive types (int
,boolean
) cannot acceptnull
. Use wrapper types (Integer
,Boolean
) when the parameter is optional.
General Idea
Java supports repeatable annotations (@Repeatable
).
See code here: Repeatable Annotations Code Example
In Spring
- Security annotations often stack (
@Secured
,@RolesAllowed
). - Meta-annotations are common (
@RestController
combines@Controller
+@ResponseBody
).
flowchart LR
A[Annotation Declared] --> B[Repeatable Container]
B --> C[Reflection Reads All Instances]
C --> D[Framework Applies Each Rule]
Spring’s systematic annotation processing:
- Classpath scanning → discover
@Component
,@Controller
, etc. - Bean definition → metadata recorded.
- Dependency injection → resolve and inject fields or constructors.
- Routing → register
@RequestMapping
,@GetMapping
methods. - Parameter binding → handle
@RequestParam
,@PathVariable
. - Advanced behavior → lifecycle (
@PostConstruct
), transactions (@Transactional
), security (@Secured
).
sequenceDiagram
participant S as Scanner
participant B as BeanDefinition
participant D as Dependency Injector
participant R as Request Dispatcher
S->>B: Discover annotated classes
B->>D: Metadata guides injection
D->>R: Beans wired and ready
R->>App: Dispatch request via method.invoke()
Reflection is the engine that turns annotations from labels into runtime behavior.
Annotation Target | Example (Spring) | Reflection API |
---|---|---|
Class | @Controller |
clazz.isAnnotationPresent(..) |
Method | @GetMapping |
method.getAnnotation(..) |
Field | @Autowired |
field.getAnnotation(..) |
Parameter | @RequestParam |
parameter.getAnnotations() |