Dependency Injection (DI) is a fundamental concept in Spring Boot that facilitates the management of dependencies between various components in your application. Spring Boot primarily uses the @Autowired
annotation to enable automatic wiring of beans. Below, I’ll explain how Dependency Injection works using @Autowired
in Spring Boot, step by step.
What is Dependency Injection?
Instead of creating dependencies manually within components, Spring’s IoC (Inversion of Control) container injects dependencies into an object automatically. This reduces tight coupling and makes code more reusable and flexible.
How Does @Autowired
Work?
@Autowired
is used to mark a dependency that Spring should handle automatically. It tells Spring, “Find the appropriate bean in the IoC container and inject it here.”
Here’s how it works:
- Mark the class as a Spring Bean using
@Component
,@Service
,@Repository
, or by defining it explicitly via@Bean
in a configuration class. - Use the
@Autowired
Annotation to inject a required dependency into a field, constructor, or setter method.
Approaches to Using @Autowired
1. Field Injection
This is the simplest form of dependency injection where the field is annotated with @Autowired
.
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
@Component
public class CarService {
@Autowired
private Engine engine;
public void startCar() {
engine.start();
}
}
@Component
public class Engine {
public void start() {
System.out.println("Engine started!");
}
}
- Pros: Quick and readable.
- Cons: Field injection is less testable and not recommended for complex applications because the dependency is hard to mock in unit tests.
2. Constructor Injection (Recommended)
Here, dependencies are injected via the constructor. This is the most preferred approach as it promotes immutability and easy testing.
import org.springframework.stereotype.Component;
@Component
public class CarService {
private final Engine engine;
@Autowired // Optional in Spring Boot since 4.3+
public CarService(Engine engine) {
this.engine = engine;
}
public void startCar() {
engine.start();
}
}
@Component
public class Engine {
public void start() {
System.out.println("Engine started!");
}
}
- Pros: Cleaner, better for unit testing, adheres to immutability principles.
- Cons: Requires explicit constructors, but modern tools (e.g., Lombok) simplify this.
3. Setter Injection
Dependencies are injected via setter methods. This is helpful when a bean property is optional and not required during object creation.
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
@Component
public class CarService {
private Engine engine;
@Autowired
public void setEngine(Engine engine) {
this.engine = engine;
}
public void startCar() {
engine.start();
}
}
@Component
public class Engine {
public void start() {
System.out.println("Engine started!");
}
}
- Pros: Useful when you want to inject optional dependencies.
- Cons: Makes the class mutable and harder to test.
Behind the Scenes
- Bean Discovery: The Spring IoC container detects beans annotated with
@Component
,@Service
,@Repository
,@Controller
, or those defined explicitly in configuration classes. - Dependency Resolution: When
@Autowired
is detected on a field, constructor, or setter, Spring scans its ApplicationContext to find a matching bean (by type). - Injection: The resolved bean (dependency) is injected into the target where
@Autowired
is present.
Handling Ambiguity
If multiple beans of the same type are defined, Spring would throw a NoUniqueBeanDefinitionException
. You can resolve this by:
- Using
@Qualifier
:
@Autowired
@Qualifier("specificEngine")
private Engine engine;
- Marking a bean as a primary candidate:
@Primary
@Component
public class Engine { ... }
Example: A Working Spring Boot Application
Here’s a complete example of how @Autowired
can be used in a Spring Boot application.
Application Entry Point
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootExampleApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootExampleApplication.class, args);
}
}
Service & Dependency
import org.springframework.stereotype.Service;
@Service
public class GreetingService {
public String getGreeting() {
return "Hello, Dependency Injection!";
}
}
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
private final GreetingService greetingService;
public GreetingController(GreetingService greetingService) {
this.greetingService = greetingService;
}
@GetMapping("/greet")
public String greet() {
return greetingService.getGreeting();
}
}
Run the application, and visiting http://localhost:8080/greet will give you the message:
Hello, Dependency Injection!
Best Practices
- Use Constructor Injection for mandatory dependencies.
- Avoid Field Injection, except for small applications or testing purposes.
- Annotate beans properly using stereotypes like
@Service
,@Component
, etc. - For optional beans, use
@Autowired(required = false)
or setter injection. - Handle bean ambiguity with
@Qualifier
or@Primary
.