Level 2 · 20 min
Spring DI
Spring's IoC container is the foundation of the Spring ecosystem. Understanding bean lifecycle, injection styles, scopes, and AOP proxies is essential for diagnosing mysterious Spring bugs — null dependencies, circular dependency errors, and advice that silently doesn't fire.
IoC Container and Bean Lifecycle
The Spring IoC container manages bean creation, wiring, and destruction. Bean lifecycle: 1) Container reads configuration (annotations/XML), 2) Bean instance created (constructor), 3) Dependencies injected, 4) @PostConstruct called (custom init), 5) Bean ready for use, 6) On shutdown: @PreDestroy called, 7) Bean destroyed. ApplicationContext is the main container — it is a superset of BeanFactory, adding event publication, i18n, AOP, and more.
@Autowired vs Constructor Injection, Bean Scopes
Constructor injection is preferred over @Autowired field injection for three reasons: 1) Fields can be declared final (immutability guarantee), 2) Constructor injection makes dependencies explicit and visible, 3) Constructor injection enables unit testing without a Spring container — just call new MyService(mockDep). Field injection hides dependencies, makes the class harder to instantiate manually, and breaks when the bean has no-arg constructor. Bean scopes: singleton (one instance per container, default), prototype (new instance per injection point), request (one per HTTP request, needs web context), session (one per HTTP session). Effective Java (Item 5) makes the architectural case for constructor injection explicitly: 'pass the resource into the constructor when creating a new instance. This is one form of dependency injection... The dependency injection pattern is so simple that many programmers use it for years without knowing it has a name... It preserves immutability, so multiple clients can share dependent objects.' Bloch further notes that dependency injection frameworks can be viewed as powerful service providers — Spring's ApplicationContext is essentially an industry-strength implementation of this pattern. A useful variant is the factory pattern: instead of injecting the resource directly, inject a Supplier<T> or factory, allowing each use site to create a fresh instance (useful for prototype-scoped dependencies injected into singleton beans).
AOP Proxies and Circular Dependencies
Spring AOP works by wrapping beans in CGLIB or JDK dynamic proxies. This is why self-invocation does not trigger advice (@Transactional on a method calling another @Transactional method in the same class is a common gotcha — the second call goes through this, not the proxy). Circular dependencies: Spring can resolve circular dependencies between singleton beans when using setter or field injection (via a three-phase construction: create, inject, initialize). Constructor injection circular dependencies fail at startup (which is actually better — it forces you to break the cycle by design). Spring Boot 2.6+ disallows circular dependencies by default.
Code example
// PREFERRED: constructor injection
@Service
public class OrderService {
private final PaymentService payment;
private final InventoryService inventory;
public OrderService(PaymentService payment, InventoryService inventory) {
this.payment = payment;
this.inventory = inventory;
}
}
// Bean lifecycle hooks
@Component
public class CacheService {
@PostConstruct
public void init() { /* warm up cache */ }
@PreDestroy
public void cleanup() { /* flush cache */ }
}
// Prototype in singleton via ObjectProvider
@Service
public class TaskRunner {
private final ObjectProvider<Worker> workerProvider;
public void run() {
Worker w = workerProvider.getObject();
w.execute();
}
}