Adetayo Akinsanya unkletayo.dev

Java Reflection Under the Hood: Classloading, Instantiation, and Metadata Inspection

Understanding how frameworks inspect, instantiate, and inject private Java fields at runtime.

Adetayo Akinsanya (unkletayo) 2026-08-21

Part 2 in Series — Catch up on the previous article: Why Manual Object Wiring Fails at Scale: The Inversion of Control (IoC) Problem (Part 1) before diving into this post.

Consider a simple Spring service annotated with @Service:

@Service
public class PaymentService {
    @Autowired
    private StripeClient stripeClient;

    private PaymentService() {
        // Private constructor!
    }
}

Notice two curious Java language violations:

  1. PaymentService has a private constructor. In standard Java code, calling new PaymentService() from outside the class generates a compiler error: 'PaymentService()' has private access.
  2. The stripeClient field is private and lacks a setter method. Yet Spring injects the dependency into stripeClient without throwing a NullPointerException.

How can the Spring Framework instantiate classes with private constructors and inject values directly into private fields without invoking setters or calling new?

The answer lies in Java Reflection.


1. What Is Java Reflection?

Reflection is a capability in the Java Virtual Machine (JVM) that allows executing code to inspect, discover, and manipulate internal class structures at runtime.

Using Reflection, a framework can inspect an unknown Java class file byte structure, query its methods, examine its annotations, invoke its constructors, and write directly into private fields.

JAVA REFLECTION METADATA INSPECTION
+-------------------------------------------------------------------+
| Class<?> clazz = Class.forName("com.example.PaymentService");     |
+-------------------------------------------------------------------+
                                  |
                                  v
+-------------------------------------------------------------------+
| 1. Query Constructors -> clazz.getDeclaredConstructors()          |
| 2. Query Fields       -> clazz.getDeclaredFields()                |
| 3. Query Annotations  -> field.getAnnotation(Autowired.class)     |
+-------------------------------------------------------------------+

2. Dynamic Instantiation via Reflection

To instantiate a class dynamically without invoking the new operator:

// 1. Load class metadata into RAM via ClassLoader
Class<?> clazz = Class.forName("com.example.PaymentService");

// 2. Fetch the declared constructor (even if private!)
Constructor<?> constructor = clazz.getDeclaredConstructor();

// 3. Bypass Java language access control checks!
constructor.setAccessible(true);

// 4. Instantiate object instance in JVM Heap
Object serviceInstance = constructor.newInstance();

Bypassing Accessibility Checks (setAccessible(true))

By default, the JVM enforces standard Java language access rules (public, protected, package-private, private).

When a framework calls setAccessible(true) on a Constructor, Field, or Method object, it instructs the SecurityManager and JVM runtime to bypass visibility checks, allowing full read and write access to private members.


3. Inspecting Annotations and Field Injection

How does Spring discover fields annotated with @Autowired and inject values dynamically?

Field[] fields = clazz.getDeclaredFields();

for (Field field : fields) {
    // Check if the field is annotated with @Autowired
    if (field.isAnnotationPresent(Autowired.class)) {
        field.setAccessible(true); // Bypass private visibility
        
        // Fetch matching bean instance from container storage
        Object dependencyInstance = container.getBean(field.getType());
        
        // Inject dependency directly into the target object's private memory field!
        field.set(serviceInstance, dependencyInstance);
    }
}

Step-by-Step Runtime Execution:

  1. Spring iterates through all declared fields returned by getDeclaredFields().
  2. It calls field.isAnnotationPresent(Autowired.class) to detect target injection points.
  3. It queries the field.getType() (e.g., StripeClient.class) to locate a matching bean instance inside the IoC Container.
  4. It calls field.set(serviceInstance, dependencyInstance), writing the memory reference directly into the private field.

4. Performance Implications of Reflection

Historically, Reflection operations were significantly slower than direct bytecode execution because the JVM could not inline reflective calls or optimize type checks.

Modern JVMs (Java 17+) optimize Reflection using Inflaters and MethodHandles.

However, to prevent reflection overhead during production runtime, Spring employs two performance optimization strategies:

  1. Reflection Metadata Caching: Spring scans class metadata and annotations once during application startup, caching Field, Method, and Constructor references in ReflectionUtils data structures.
  2. Zero Reflection During Request Execution: Once beans are wired during startup, processing HTTP requests executes native Java method calls without reflective overhead.

Direct Bytecode vs Reflection Mechanics Matrix

Feature / OperationDirect Java Code (new)Java Reflection API
Instantiation Mechanismnew PaymentService()constructor.newInstance()
Visibility EnforcementStrictly enforced by compilerBypassed via setAccessible(true)
Type CheckingCompile-Time Static CheckingRuntime Dynamic Type Verification
Annotation InspectionIgnored at execution timeQueried via isAnnotationPresent()
Primary Use CaseBusiness domain logicFramework Container Engines (Spring, Hibernate, Jackson)

Summary & Next Steps

Java Reflection provides the runtime foundation for modern enterprise frameworks:

  • Reflection allows frameworks to inspect class structures, annotations, and private fields dynamically at runtime.
  • setAccessible(true) bypasses Java access rules to instantiate objects with private constructors and inject values into private fields.
  • Annotation Inspection (isAnnotationPresent) enables Spring to identify @Autowired, @Component, and @Service targets automatically.
  • Spring caches Reflection metadata during startup to ensure zero performance overhead during production HTTP request execution.

In the next article, we examine Dependency Injection Mechanics: Constructor, Field, and Setter Injection Trade-offs.

References & Further Reading

  1. Oracle Corporation. Java SE 21 Reflection API & Dynamic Proxies (java.lang.reflect). Oracle Docs.
  2. OpenJDK. HotSpot JVM Specification — Class Loading and Reflection Mechanics. OpenJDK Docs.
  3. Bloch, J. (2018). Effective Java (3rd Edition) — Item 80: Prefer Interfaces to Reflection. Addison-Wesley.

Up Next in Series →

Part 3: Dependency Injection Mechanics: Constructor, Field, and Setter Injection Trade-offs

Continue to Part 3 →