How Java Reflection Powers Spring's Dependency Injection
You write
@Autowired. Spring wires everything. Ever wondered how?
The answer is Reflection — one of Java's most powerful and misunderstood features.
What is Reflection?
Normally, Java is a compile-time language — you must know your classes, methods, and fields before the program runs. Reflection breaks that rule.
It lets you inspect and manipulate any class at runtime — read its fields, invoke its methods, and instantiate it — even if you never imported it.
// Normal way — you must know the class at compile time
OrderService s = new OrderService();
// Reflection way — class name can come from anywhere at runtime
Class<?> clazz = Class.forName("com.myapp.OrderService");
Object s = clazz.getDeclaredConstructor().newInstance();
Think of it like this:
🔦 Normal Java is like reading a book you already own.
🔍 Reflection is like walking into an unknown library, picking any book off the shelf, and reading it on the spot.
Why Does Spring Need It?
Spring is a framework — a library compiled and shipped before your application even exists. It has never seen your OrderService, PaymentService, or UserRepository. Yet at runtime, it finds them, creates them, and wires them together automatically.
The only tool in Java that makes this possible is Reflection.
If Spring used new instead, its internal code would have to look like:
// Spring would have to hardcode YOUR classes — impossible!
if (type == OrderService.class) return new OrderService();
if (type == PaymentService.class) return new StripePaymentService();
// ... every class in your app listed here
Spring is compiled years before your app is written. It cannot new your classes. So it uses reflection to discover and instantiate them at runtime.
How Spring Uses Reflection — Step by Step
Let's say you write this:
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
}
Here's what Spring does internally:
Step 1 — Classpath Scanning
Spring scans your packages for @Service, @Component, @Repository using reflection:
// Finds all classes annotated with @Service at runtime
Set<Class<?>> beans = reflections.getTypesAnnotatedWith(Service.class);
// → finds OrderService, PaymentService, etc.
Step 2 — Instantiation Without Knowing the Class
Class<?> clazz = Class.forName("com.myapp.OrderService");
Object instance = clazz.getDeclaredConstructor().newInstance();
// Spring just created YOUR class without ever writing `new OrderService()`
Step 3 — Reading @Autowired and Injecting
Field[] fields = OrderService.class.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Autowired.class)) {
field.setAccessible(true); // bypasses private modifier!
Object dependency = applicationContext.getBean(field.getType());
field.set(instance, dependency); // injects PaymentService
}
}
This is how Spring injects into private fields — something that's normally forbidden in Java. Only reflection can do it.
The ApplicationContext — Spring's Bean Registry
ApplicationContext is Spring's IoC container. Think of it as a warehouse that stores all your objects (beans).
🏭 Analogy: Spring's ApplicationContext is like an Amazon warehouse.
At startup, all items (beans) are stocked. When you need something, you don't manufacture it yourself — you just request it and it's handed to you instantly.
Internally it works like a map:
ApplicationContext Registry
│
├── beansByName
│ ├── "orderService" → OrderService instance
│ ├── "stripePaymentService" → StripePaymentService instance
│ └── "paypalPaymentService" → PaypalPaymentService instance
│
└── beansByType
├── PaymentService.class → ["stripePaymentService", "paypalPaymentService"]
└── OrderService.class → ["orderService"]
When Spring wires dependencies, it calls:
applicationContext.getBean(PaymentService.class);
// → looks up PaymentService in the registry
// → returns the already-created instance
It does not create a new object every time. The instance was already built at startup and is simply retrieved.
What If Multiple Classes Implement the Same Interface?
@Service public class StripePaymentService implements PaymentService { ... }
@Service public class PaypalPaymentService implements PaymentService { ... }
Now getBean(PaymentService.class) finds two candidates and throws:
NoUniqueBeanDefinitionException: expected single bean but found 2:
stripePaymentService, paypalPaymentService
Spring refuses to guess. You resolve it by:
| Solution | How |
|---|---|
@Primary |
Mark one as the default winner |
@Qualifier("name") |
Explicitly name which one to inject |
List<PaymentService> |
Inject all implementations as a list |
Map<String, PaymentService> |
Inject all, keyed by bean name |
// Inject all and pick at runtime — most flexible
@Autowired
private Map<String, PaymentService> paymentServices;
paymentServices.get("stripePaymentService").charge(amount); // dynamic!
Why Not Just Use new?
new |
Reflection + DI | |
|---|---|---|
| Class must be known at compile time | ✅ Yes | ❌ No — runtime |
| Can inject into private fields | ❌ No | ✅ Yes |
| Can scan annotations automatically | ❌ No | ✅ Yes |
| Works with any class, even unknown ones | ❌ No | ✅ Yes |
Supports AOP, @Transactional, proxies |
❌ No | ✅ Yes |
Without reflection, Spring would not be a framework.
It would just be a very longmain()method callingnewon everything.
Quick Recap
- Reflection lets Java inspect and instantiate classes at runtime without knowing them at compile time.
- Spring uses reflection to scan annotations, create beans, and inject dependencies automatically.
- ApplicationContext is the registry that stores all beans — wired once at startup, retrieved on demand.
-
Multiple implementations of an interface are all stored; you must tell Spring which to use via
@Primary,@Qualifier, or by injecting aList/Map. -
newcannot replace reflection because Spring is compiled before your classes exist — only reflection bridges that gap.
Understanding reflection turns Spring from magic into mechanics — and that's when you go from using a framework to truly mastering it.
Top comments (0)