A typical enterprise application consists of multiple objects working together, even the simplest applications require several components collaborating to present a coherent experience to end users.
Understanding Dependency Injection
Dependency Injection (DI) is a process whereby objects define their dependencies through constructor arguments, factory method parameters, or properties set after object construction. The container injects these dependencies when creating the bean. This process is fundamentally about inversion of control—hence the name—whereby objects delegate the responsibility of locating and instantiating their dependenceis to an external mechanism rather than handling it themselves through direct class instantiation or service locator patterns.
Code written following DI principles tends to be cleaner, with better separation of concerns when objects receive their dependencies rather than creating them internally. Objects do not need to search for their dependencies, nor are they aware of the dependency locations or concrete implementations. This design makes classes significantly easier to test, particularly when dependencies are expressed through interfaces or abstract base classes, enabling the use of stubs or mock implementations in unit tests.
Two primary variants of DI exist: constructor-based injection and setter-based injection.
Constructor-Based Dependency Injection
Constructor-based DI is accomplished when the container invokes a constructor with multiple arguments, where each argument represents a dependency. Calling a static factory method with specific arguments to construct a bean follows nearly identical semantics. This discussion treats constructor and static factory method arguments equivalently. The following class demonstrates constructor-based DI:
public class MovieRecommender {
private final RatingEngine ratingEngine;
public MovieRecommender(RatingEngine engine) {
this.ratingEngine = engine;
}
// Implementation details omitted
}
This class carries no special characteristics. It is a POJO with no dependencies on container-specific interfaces, base classes, or annotations.
Constructor Argument Resolution
Constructor argument resolution occurs by matching argument types. When no ambiguity exists among constructor parameters in a bean definition, the parameter order in the bean definition determines the order passed to the constructor during instantiation. Consider this configuration:
package com.example;
public class OrderProcessor {
public OrderProcessor(PaymentGateway gateway, InventoryService inventory) {
// Initialization logic
}
}
Assuming PaymentGateway and InventoryService share no inheritance relationship, no ambiguity exists. The following XML configuration works without explicitly specifying parameter index or type:
<beans>
<bean id="orderProcessor" class="com.example.OrderProcessor">
<constructor-arg ref="paymentGateway"/>
<constructor-arg ref="inventoryService"/>
</bean>
<bean id="paymentGateway" class="com.example.StripeGateway"/>
<bean id="inventoryService" class="com.example.DefaultInventoryService"/>
</beans>
When referencing another bean, the type is already known and matching proceeds smoothly. However, when using simple types like <value>true</value>, Spring cannot determine the type without additional guidance. Consider this scenario:
package com.example;
public class ConfigurationManager {
private final int cacheSize;
private final String environment;
public ConfigurationManager(int cacheSize, String environment) {
this.cacheSize = cacheSize;
this.environment = environment;
}
}
When the container cannot infer types from references, explicitly specifying types using the type attribute enables matching:
<bean id="configManager" class="com.example.ConfigurationManager">
<constructor-arg type="int" value="512"/>
<constructor-arg type="java.lang.String" value="production"/>
</bean>
Alternatively, use the index attribute to specify parameter position:
<bean id="configManager" class="com.example.ConfigurationManager">
<constructor-arg index="0" value="512"/>
<constructor-arg index="1" value="production"/>
</bean>
Beyond resolving ambiguity among multiple simple values, specifying indices also resolves scenarios where a constructor contains two parameters of identical types.
Indices are zero-based.
Constructor parameter names can also disambiguate values:
<bean id="configManager" class="com.example.ConfigurationManager">
<constructor-arg name="cacheSize" value="512"/>
<constructor-arg name="environment" value="production"/>
</bean>
For this to function out of the box, code must be compiled with debug information enabled, allowing Spring to inspect constructor parameter names. If compiling with debug flags is not feasible, the @ConstructorProperties JDK annotation can explicitly name constructor parameters:
package com.example;
public class ConfigurationManager {
@ConstructorProperties({"cacheSize", "environment"})
public ConfigurationManager(int cacheSize, String environment) {
this.cacheSize = cacheSize;
this.environment = environment;
}
}
Setter-Based Dependency Injection
Setter-based DI occurs after the container instantiates a bean by calling a no-argument constructor or a no-argument static factory method, then invokes setter methods on the bean. The following class demonstrates pure setter-based DI. This regular Java class functions as a POJO without dependencies on container-specific interfaces, base classes, or annotations:
public class EmailNotificationService {
private MessageQueue messageQueue;
public void setMessageQueue(MessageQueue queue) {
this.messageQueue = queue;
}
// Service methods omitted
}
The ApplicationContext supports both constructor-based and setter-based DI for the beans it manages. It also supports setter-based DI following partial constructor injection. Dependencies are configured as BeanDefinition instances, combined with PropertyEditor instances to handle format conversion. However, most Spring users interact with XML bean definitions, annotated components (@Component, @Controller, and similar), or @Bean methods in Java-based @Configuration classes. These sources internally convert to BeanDefinition instances that load the complete Spring IoC container.
Constructor Injection Versus Setter Injection
Since constructor and setter injection can be combined, a useful guideline emerges: employ constructors for mandatory dependencies, while using setters or configuration methods for optional ones. While @Autowired on a setter makes a property required, constructor injection with programmatic validation represents the preferable approach.
The Spring team generally advocates constructor injection because it enables implementing application components as immutable objects and guarantees required dependencies are never null. Furthermore, constructor-injected components always return to calling code in a fully initialized state. However, numerous constructor parameters indicate code smells, suggesting the class may bear excessive responsibilities and requires refactoring to better address separation of concerns.
Setter injection suits optional dependencies that can receive reasonable defaults within the class. Otherwise, null checks become necessary every where the dependency gets used. Setter injection provides the advantage of allowing object reconfiguration or subsequent reinjection. Management through JMX MBeans represents a compelling use case for setter injection.
Select the DI style most appropriate for the specific situation. When working with third-party classes lacking source code access, the choice may already be predetermined. For instance, if a third-party class exposes no setter methods, constructor injection may be the sole available DI approach.
Dependency Resolution Process
The container resolves bean dependencies through this sequence:
-
The
ApplicationContextinitializes using configuration metadata describing all beans. This metadata originates from XML, Java code, or annotations. -
Each bean's dependencies are expressed through properties, constructor arguments, or static factory method parameters. These dependencies receive their actual values when the bean is created.
-
Every property or constructor argument constitutes either an actual value or a reference to another bean within the container.
-
Each property or constructor argument value converts from its specified format to the actual target type. By default, Spring converts string-formatted values to built-in types including
int,long,String,boolean, and others.
The container validates each bean's configuration at creation time, though bean properties themselves are not set until actual instantiation. By default, singleton-scoped beans marked for pre-instantiation are created when the container starts. Otherwise, beans are created only upon request. Bean creation may trigger creation of dependency chains, as a bean's dependencies and their dependencies (and so forth) instantiate and get assigned. Configuration mismatches among these dependencies might surface later—specifically, when the affected bean first gets created.
Circular Dependencies
Primary use of constructor injection can create circular dependency scenarios that resist resolution.
Consider: Class A requires a constructor-injected instance of Class B, while Class B simultaneously requires a constructor-injected instance of Class A. Configuring both beans to inject each other causes the Spring IoC container to detect this circular reference at runtime and throw BeanCurrentlyInCreationException.
Potential solutions include modifying source code to use setters instead of constructors, or avoiding constructor injection entirely in favor of setter injection. Although not recommended, setter injection permits configuring circular dependencies.
Unlike typical scenarios without circular dependencies, the A-to-B dependency forces one bean to receive the other before completing its own initialization—a classic chicken-and-egg problem.
Generally, trusting Spring's correctness serves well. The framework detects configuration problems like nonexistent bean references and circular dependencies during container loading. Spring defers property setting and dependency resolution until bean creation. Consequently, if object creation or dependency resolution fails (due to missing or invalid properties causing exceptions), a properly loaded Spring container may still surface exceptions later when requesting objects. This delayed visibility of potential configuration problems explains why ApplicationContext implementations pre-instantiate singleton beans by default. While creating these beans before actual demand consumes time and memory, configuration problems surface during ApplicationContext creation rather than later. The default behavior can be overridden to enable lazy initialization for singletons.
When circular dependencies do not exist and multiple collaborating beans inject into a dependent bean, each collaborating bean is fully configured before injection occurs. If bean A depends on bean B, the Spring container fully configures bean B before calling bean A's setter methods. In other words, beans instantiate (unless pre-instantiated singletons), dependencies set, and lifecycle methods such as configured init methods or InitializingBean callbacks execute.
Practical DI Examples
The following examples demonstrate setter-based DI using XML configuration metadata. A Spring XML configuration fragment defines several beans:
<bean id="userService" class="com.example.UserService">
<property name="repository">
<ref bean="jpaRepository"/>
</property>
<property name="eventPublisher" ref="applicationEventPublisher"/>
<property name="timeoutSeconds" value="30"/>
</bean>
<bean id="jpaRepository" class="com.example.JpaUserRepository"/>
<bean id="applicationEventPublisher" class="com.example.DefaultEventPublisher"/>
The corresponding UserService class:
public class UserService {
private UserRepository repository;
private EventPublisher eventPublisher;
private int timeoutSeconds;
public void setRepository(UserRepository repository) {
this.repository = repository;
}
public void setEventPublisher(EventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public void setTimeoutSeconds(int timeoutSeconds) {
this.timeoutSeconds = timeoutSeconds;
}
}
The declared setters correspond to the properties specified in the XML configuration. The following example employs constructor-based DI:
<bean id="userService" class="com.example.UserService">
<constructor-arg>
<ref bean="jpaRepository"/>
</constructor-arg>
<constructor-arg ref="applicationEventPublisher"/>
<constructor-arg type="int" value="30"/>
</bean>
<bean id="jpaRepository" class="com.example.JpaUserRepository"/>
<bean id="applicationEventPublisher" class="com.example.DefaultEventPublisher"/>
The matching UserService implementation:
public class UserService {
private final UserRepository repository;
private final EventPublisher eventPublisher;
private final int timeoutSeconds;
public UserService(UserRepository repository, EventPublisher eventPublisher, int timeoutSeconds) {
this.repository = repository;
this.eventPublisher = eventPublisher;
this.timeoutSeconds = timeoutSeconds;
}
}
The constructor arguments from the bean definition map directly to the constructor parameters.
Consider a variant where Spring invokes a static factory method instead of a constructor:
<bean id="userService" class="com.example.UserService" factory-method="createService">
<constructor-arg ref="jpaRepository"/>
<constructor-arg ref="applicationEventPublisher"/>
<constructor-arg value="30"/>
</bean>
<bean id="jpaRepository" class="com.example.JpaUserRepository"/>
<bean id="applicationEventPublisher" class="com.example.DefaultEventPublisher"/>
The corresponding UserService class:
public class UserService {
private UserService(...) {
// Private constructor
}
// Static factory method accepts dependencies as parameters,
// which become the dependencies of the returned bean
// regardless of how those arguments are ultimately used
public static UserService createService(
UserRepository repository, EventPublisher eventPublisher, int timeoutSeconds) {
UserService service = new UserService(...);
// Additional initialization steps
return service;
}
}
Static factory method arguments use <constructor-arg/> elements identically to actual constructor scenarios. The type returned by the factory method need not match the class containing the static factory method (though in this example they align). Instance (non-static) factory methods follow the same pattern, substituting the factory-bean attribute for the class attribute.