Spring Framework is renowned for its robust Dependency Injection (DI) capabilities, which simplifies the management of dependencies and its lifecycle of beans in a Java application.

This article delves into how to use @Autowired for field, constructor, and setter injection methods in Spring, and how these methods compare to traditional Java dependency management.


What is Dependency Injection?

Dependency injection is a design pattern that allows an object to receive its dependencies from an external source rather than creating them internally. This pattern promotes loose coupling, making code more modular, testable, and easier to manage.

Example to illustrate DI and reduced coupling

// Concrete implementation of InventoryService
public class InventoryServiceImpl implements InventoryService {
    @Override
    public void checkStock(Order order) {
        // some implementation
    }
}

Without DI:

public class OrderService {

    // OrderService depends on concrete implementation directly
    private InventoryServiceImpl inventoryService = new InventoryServiceImpl();

    public void placeOrder(Order order) {
        inventoryService.checkStock(order);
        // other logic
    }
}

With DI:

public class OrderService {
    private InventoryService inventoryService;

    // OrderSrvice depends on the interface rather than the concrete class
    public OrderService(InventoryService inventoryService) {
        // Constructor injection
        this.inventoryService = inventoryService;
    }

    public void placeOrder(Order order) {
        inventoryService.checkStock(order);
        // other logic
    }
}

In the first example, OrderService is tightly coupled with the concrete implementation of InventoryService. In the second example, the dependency is injected, allowing OrderService to be independent of the concrete implementation of InventoryService. This reduces coupling and enhances flexibility, making it easier to change InventoryService implementation or mock it in tests.

Spring’s @Autowired annotation

The @Autowired annotation in Spring is used to inject dependencies automatically. This can be done via field injection, constructor injection, and setter injection.

1. Field injection

Field injection involves directly injecting dependencies into a field of a class. This is the simplest form of dependency injection in Spring.

Example:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MyService {

    @Autowired
    private MyRepository myRepository;
    
    public void performService() {
        // calls myRepository
    }
}

The @Autowired annotation on myRepository tells Spring to inject an instance of MyRepository into this field. Spring will then automatically creates an instance of MyRepository (or retrieves an existing one if it is a singleton) and injects it into MyService when MyService is created. This is done through Spring’s Inversion of Control (IoC) container which automatically manages the lifecycle and dependencies of beans.

In this case, MyService is able to use MyRepository without explicitly instantiating it, promoting loose coupling and adherance to the dependency inversion principle.

2. Constructor injection

In many cases, constructor injection might be preferred as it ensures that dependencies are provided at object creation time and enforces immutability.

Example:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MyService {

    private final MyRepository myRepository;
    
    @Autowired
    public MySerivce(Repository repository) {
        myRepository = repository;
    }

    public void performService() {
        // calls myRepository
    }
}

In this case, @Autowired on the constructor tells Spring to use this constructor for dependency injection. When Spring creates the MyService bean, it recognizes that MyService has a constructor with a single parameter of type MyRepository. Spring will then create an instance of MyRepository and passes it as an argument to the MyService constructor.

Example with multiple constructors:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MyService {

    private final MyRepository myRepository;
    private final AnotherDependency anotherDependency;

    // Primary constructor for dependency injection
    @Autowired
    public MyService(MyRepository myRepository) {
        this.myRepository = myRepository;
        this.anotherDependency = null; // Or handle appropriately
    }

    // Another constructor
    public MyService(AnotherDependency anotherDependency) {
        this.myRepository = null; // Or handle appropriately
        this.anotherDependency = anotherDependency;
    }

    public void performService() {
        // Use myRepository or anotherDependency
    }
}

When a class has multiple constructors, specifying @Autowired is crucial because it tells Spring explicitly which constructor to use for dependency injection (i.e. ensures that Spring knows which dependencies to inject when the class is instantiated).

3. Setter injection

Similar to the previous example, setter injection involves providing dependencies via setter methods. This approach is useful when the dependencies are optional or when you want to change them after object’s creation.

Example:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MyService {

    private MyRepository myRepository;

    @Autowired
    public void setMyRepository(MyRepository myRepository) {
        this.myRepository = myRepository;
    }

    public void performService() {
        // Use myRepository
    }
}

Comparison with traditional Java code

In traditional java code, dependencies are typically managed manually:

public class MyService {

    private MyRepository myRepository;

    public MyService(MyRepository myRepository) {
        this.myrepository = myRepository; 
    }

    public void performService() {
        // Use myRepository
    }
}
public class Main {
    public static void main(String[] args) {
        // Manual wiring of dependencies
        MyRepository myRepository = new MyRepository(); 
        MyService myService = new MyService(myRepository);

        myService.performService();
    }
}

There are certain issues regarding this:

1. Tight coupling

In this case, the MyService class is tightly coupled with MyRepository. In traditional approach, classes instantiate their dependencies directly. Any change in MyRepository (e.g. constructor change, needing additional parameters) would require a change in MyService.

2. Testing difficulties

There is no easy substitution of MyRespository as MyService creates its own MyRepository instance.

However, personally I think these issues are more of a poor design problem rather than whether the codebase is using traditional or Spring.

3. Poor scalability

While the first two issues often stem from poor design practices, regardless of whether a project uses traditional java, it is difficult to manage all these manual wiring of dependencies as our application grow.

public class MyServiceTest {
    @Test
    public void testPerformService() {
        MyService myService = new MyService(); // Uses real MyRepository
        myService.performService();
        // Hard to test because MyRepository is not mockable
    }
}

Benefits of Spring Boot @Autowired

With Spring Boot, we don’t need to manually configure dependencies. The framework automatically scans for components (@Autowired, @Component, @Service, @Repository, etc.) and wires them appropriately.