Command Palette

Search for a command to run...

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.

Key Takeaways

  • Prefer constructor injection: it enables final fields, makes dependencies visible, and allows testing without Spring.
  • @Transactional only works if the call goes through the Spring proxy. Self-invocation (this.method()) bypasses the proxy and the transaction.
  • Singleton is the default scope. Injecting a prototype bean into a singleton requires special handling (ObjectProvider or @Lookup) — otherwise you get the same prototype instance every time.

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();
  }
}