Skip to main content

Spring Dependency Injection

Introduction

Dependency Injection (DI) is a fundamental concept in Spring that implements Inversion of Control (IoC) for managing object dependencies. Rather than creating objects directly within a class, Spring handles the instantiation and "injects" the required dependencies, making your code more modular, easier to test, and less coupled.

In simple terms, dependency injection means giving an object its instance variables rather than having it construct them itself. This pattern shifts the responsibility of dependency management from the components to the framework, resulting in cleaner, more maintainable code.

Why Use Dependency Injection?

Before diving into Spring's implementation, let's understand why DI is important:

  1. Loose Coupling: Components depend on abstractions rather than concrete implementations
  2. Testability: Dependencies can be easily mocked during testing
  3. Maintainability: Code becomes more modular and easier to understand
  4. Flexibility: Implementations can be swapped without changing the dependent code

Types of Dependency Injection in Spring

Spring framework supports three main types of dependency injection:

1. Constructor Injection

Dependencies are provided through a class constructor.

Advantages:

  • Ensures required dependencies are available at initialization time
  • Supports immutability as dependencies can be declared final
  • Makes it clear which dependencies are mandatory

2. Setter Injection

Dependencies are provided through setter methods.

Advantages:

  • Good for optional dependencies
  • Can be used to resolve circular dependency issues
  • Allows for reconfiguration after initialization

3. Field Injection

Dependencies are injected directly into fields (usually with @Autowired annotation).

Advantages:

  • Less boilerplate code
  • Clean and concise

Constructor Injection Example

Let's create a simple example to demonstrate constructor injection:

java
// Service interface
public interface MessageService {
String getMessage();
}

// Service implementation
@Service
public class EmailService implements MessageService {
@Override
public String getMessage() {
return "Email message";
}
}

// Client class using constructor injection
@Component
public class MessageProcessor {
private final MessageService messageService;

// Constructor injection
@Autowired
public MessageProcessor(MessageService messageService) {
this.messageService = messageService;
}

public void processMessage() {
System.out.println("Processing: " + messageService.getMessage());
}
}

When Spring creates a MessageProcessor bean, it automatically injects an implementation of MessageService into its constructor.

Setter Injection Example

Here's how to implement setter injection:

java
@Component
public class NotificationService {
private MessageService messageService;

// Setter injection
@Autowired
public void setMessageService(MessageService messageService) {
this.messageService = messageService;
}

public void sendNotification() {
System.out.println("Sending notification with: " + messageService.getMessage());
}
}

Field Injection Example

Field injection is achieved by directly annotating the field:

java
@Component
public class UserService {
// Field injection
@Autowired
private MessageService messageService;

public void notifyUser() {
System.out.println("Notifying user with: " + messageService.getMessage());
}
}

Using Qualifiers For Multiple Implementations

When you have multiple implementations of the same interface, Spring needs help identifying which bean to inject. The @Qualifier annotation solves this problem:

java
// First implementation
@Service("emailService")
public class EmailService implements MessageService {
@Override
public String getMessage() {
return "Email message";
}
}

// Second implementation
@Service("smsService")
public class SmsService implements MessageService {
@Override
public String getMessage() {
return "SMS message";
}
}

// Client using qualifier
@Component
public class NotificationManager {
private final MessageService messageService;

@Autowired
public NotificationManager(@Qualifier("smsService") MessageService messageService) {
this.messageService = messageService;
}

public void sendNotification() {
System.out.println("Sending: " + messageService.getMessage());
}
}

Java Configuration for Dependency Injection

Spring also supports a Java configuration approach to dependency injection:

java
@Configuration
public class AppConfig {

@Bean
public MessageService emailService() {
return new EmailService();
}

@Bean
public MessageService smsService() {
return new SmsService();
}

@Bean
public NotificationManager notificationManager() {
// Constructor injection via Java config
return new NotificationManager(smsService());
}
}

XML Configuration Example

For completeness, here's how dependency injection looks in XML configuration:

xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- Define beans -->
<bean id="emailService" class="com.example.EmailService" />
<bean id="smsService" class="com.example.SmsService" />

<!-- Constructor injection -->
<bean id="messageProcessor" class="com.example.MessageProcessor">
<constructor-arg ref="emailService" />
</bean>

<!-- Setter injection -->
<bean id="notificationService" class="com.example.NotificationService">
<property name="messageService" ref="smsService" />
</bean>
</beans>

Real-World Application Example

Let's see a more practical example of dependency injection with a simplified e-commerce service:

java
// Service interfaces
public interface ProductService {
List<String> getAvailableProducts();
}

public interface PricingService {
double calculatePrice(String product);
}

public interface InventoryService {
boolean isInStock(String product);
}

// Implementations
@Service
public class DefaultProductService implements ProductService {
@Override
public List<String> getAvailableProducts() {
return Arrays.asList("Laptop", "Phone", "Tablet");
}
}

@Service
public class DefaultPricingService implements PricingService {
@Override
public double calculatePrice(String product) {
// Simplified pricing logic
switch(product) {
case "Laptop": return 1299.99;
case "Phone": return 699.99;
case "Tablet": return 499.99;
default: return 0.0;
}
}
}

@Service
public class DefaultInventoryService implements InventoryService {
@Override
public boolean isInStock(String product) {
// Simplified inventory check
return true;
}
}

// E-commerce service using constructor injection
@Service
public class ECommerceService {
private final ProductService productService;
private final PricingService pricingService;
private final InventoryService inventoryService;

@Autowired
public ECommerceService(
ProductService productService,
PricingService pricingService,
InventoryService inventoryService) {
this.productService = productService;
this.pricingService = pricingService;
this.inventoryService = inventoryService;
}

public void displayCatalog() {
System.out.println("Available Products:");
for (String product : productService.getAvailableProducts()) {
double price = pricingService.calculatePrice(product);
boolean inStock = inventoryService.isInStock(product);
System.out.println(product + " - $" + price + " - " +
(inStock ? "In Stock" : "Out of Stock"));
}
}
}

Running the application:

java
@SpringBootApplication
public class EcommerceApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(EcommerceApplication.class, args);
ECommerceService ecommerceService = context.getBean(ECommerceService.class);
ecommerceService.displayCatalog();
}
}

Output:

Available Products:
Laptop - $1299.99 - In Stock
Phone - $699.99 - In Stock
Tablet - $499.99 - In Stock

This example demonstrates how dependency injection helps build a modular e-commerce system where each component has a single responsibility, yet they work together through their interfaces.

Best Practices for Spring Dependency Injection

  1. Prefer Constructor Injection: It ensures all required dependencies are available when the object is constructed and supports immutability.

  2. Use Interfaces: Depend on abstractions (interfaces) rather than concrete implementations to increase flexibility.

  3. Keep Components Focused: Each component should have a single responsibility.

  4. Avoid Field Injection in Production Code: While convenient, it makes testing harder and hides dependencies.

  5. Use @Qualifier When Necessary: Be explicit about which implementation you need when multiple options exist.

  6. Consider Using Java Configuration: It provides type safety and better refactoring support compared to XML.

Summary

Spring Dependency Injection is a powerful mechanism that implements the Inversion of Control principle, making your applications more modular, testable, and maintainable. We've covered:

  • The basic concept and benefits of dependency injection
  • Different types of injection: constructor, setter, and field injection
  • How to use qualifiers when multiple implementations are available
  • Java-based and XML-based configuration approaches
  • A real-world example showing dependency injection in action

By leveraging Spring's dependency injection capabilities, you can build loosely coupled applications where components interact through interfaces rather than concrete implementations.

Additional Resources

Exercises

  1. Create a simple Spring application that demonstrates all three types of dependency injection.
  2. Refactor an existing application to use dependency injection instead of direct object instantiation.
  3. Implement a service with multiple implementations and use qualifiers to select specific implementations in different scenarios.
  4. Create a Spring Boot application that uses constructor injection to build a layered architecture (controller → service → repository).


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)