Dependency Injection (DI) is a design pattern that shifts the responsibility of creating and managing an object’s dependencies from the object itself to an external container—in this case, the Spring IoC (Inversion of Control) container. Instead of having a class manually create its dependencies, the container creates and injects them, leading to a decoupled, testable, and maintainable codebase.
The Spring IoC container creates and manages objects called beans. These beans are the building blocks of your application and can be declared using annotations such as @Component or methods annotated with @Bean inside configuration classes. DI is the process by which these beans are injected into one another to form a complete, working application.
Methods of Dependency Injection in Spring
Spring supports several ways to inject dependencies. Here are the three common approaches:
1. Field Injection
With field injection, you annotate a class field with @Autowired so that Spring automatically sets the dependency. For example:
Java
@Component
public class Customer {
@Autowired
private Product product;
}Kotlin
@Component
class Customer {
@Autowired
lateinit var product: Product
}Although this method is quick to write, it’s not recommended for production code because it makes testing harder and hides the dependency requirements.
2. Setter Injection
Setter-based injection uses a setter method marked with @Autowired to inject the dependency. This approach provides some flexibility but still doesn’t offer immutability.
Java
@Component
public class Customer {
private Product product;
@Autowired
public void setProduct(Product product) {
this.product = product;
}
}Kotlin
@Component
class Customer {
private lateinit var product: Product
@Autowired
fun setProduct(product: Product) {
this.product = product
}
}Spring invokes the setter during the object initialization.
Now we have more flexibility, but there are still some disadvantages, such as less readable code, which is harder to debug and maintain. Also, we can't make it final.
3. Constructor Injection (Recommended)
Constructor-based injection is the most recommended approach. It makes dependencies explicit, enhances immutability (by allowing the dependency to be final), and simplifies testing.
Java
@Component
public class Customer {
private final Product product;
public Customer(Product product) {
this.product = product;
}
}Kotlin
@Component
class Customer(val product: Product)Advantages of Constructor Injection:
• Testability: All dependencies are explicit and can be easily mocked.
• Immutability: Dependencies can be declared as final, preventing reassignment.
• Safety: It guarantees that required dependencies are provided at creation, reducing the chance of NullPointerExceptions.
So, which type of DI to use? According to official docs:
Constructor injection is the most recommended and preferable option for mandatory dependencies.
Setter injection should only be used for optional dependencies.
Field injection is to be avoided.
Managing Beans: Spring Bean and Spring Component
Spring Bean
A Spring Bean is an object that is instantiated, assembled, and otherwise managed by the Spring IoC container. Beans are typically defined in configuration classes using the @Bean annotation. For example:
Java
@Configuration
public class AppConfig {
@Bean
public String address() {
return "Green Street, 102";
}
@Bean
public Customer customer(@Autowired String address) {
return new Customer("Clara Foster", address);
}
}Kotlin
@Configuration
class AppConfig {
@Bean
fun address(): String = "Green Street, 102"
@Bean
fun customer(@Autowired address: String): Customer = Customer("Clara Foster", address)
}Key Points:
• Lifecycle Management: Beans are created and managed by the container, which handles initialization, dependency injection, and destruction.
• Singleton by Default: By default, Spring beans are singletons, meaning one instance is shared across the entire application (this behavior can be customized).
• Naming: By default, the bean’s name is the method name, though you can change it using syntax like @Bean("customName").
Spring Component
A Spring Component is a stereotype annotation that marks a class for automatic detection and registration as a bean within the Spring container. The most common stereotype annotations include:
• @Component – a general-purpose stereotype.
• @Service – for service-layer classes.
• @Repository – for data access objects.
• @Controller – for web controllers.
Example using @Component:
Java
@Component
public class InkSupply {
private final String INK_MESSAGE = "This is ink supply";
public String getMessage(){
return INK_MESSAGE;
}
}And injecting it into another component:
@Component
public class Printer {
private final InkSupply inkSupply;
@Autowired
public Printer(InkSupply inkSupply) {
this.inkSupply = inkSupply;
}
public void printHello(){
System.out.println(inkSupply.getMessage());
}
}Kotlin
@Component
class InkSupply {
private val inkMessage = "This is ink supply"
fun getMessage() = inkMessage
}
@Component
class Printer(@Autowired private val inkSupply: InkSupply) {
fun printHello() {
println(inkSupply.getMessage())
}
}And injecting it into another component:
@Component
class InkSupply {
private val inkMessage = "This is ink supply"
fun getMessage() = inkMessage
}
@Component
class Printer(@Autowired private val inkSupply: InkSupply) {
fun printHello() {
println(inkSupply.getMessage())
}
}Key Points:
• Automatic Scanning: Classes annotated with @Component (or its derivatives) are automatically discovered by Spring during component scanning.
• Autowiring: These components can be injected into other beans either via constructor injection (preferred) or field/setter injection.
• Decoupling: By allowing Spring to manage the creation and wiring of beans, the application becomes loosely coupled and easier to test and maintain.
Type matching
Let's start by creating beans and adding them to the context.
First, we will create a normal class to be our bean:
Java
public class Car {
private String name;
private String model;
public Car(String name, String model) {
this.name = name;
this.model = model;
}
//omitted getters and setters
}Kotlin
class Car(var name: String, var model: String)Now let's create the config class, which of course will be the container for our beans:
Java
@Configuration
public class Config {
@Bean
public Car teslaCar() {
return new Car("Tesla", "2023");
}
@Bean
public Car toyotaCar() {
return new Car("Toyota", "2023");
}
}Kotlin
@Configuration
class Config {
@Bean
fun teslaCar(): Car {
return Car("Tesla", "2023")
}
@Bean
fun toyotaCar(): Car {
return Car("Toyota", "2023")
}
}We have created two beans with the same type Car and annotated it with @Bean . Now let's print them out and see what we will get:
Java
public class DiApplication {
public static void main(String[] args) {
// Create the Spring application context
var context = new AnnotationConfigApplicationContext(Config.class);
// Retrieve an instance of MyBean
Car myBean = context.getBean(Car.class);
System.out.println(myBean.getName());
}
}Kotlin
@SpringBootApplication
class Di1Application
fun main(args: Array<String>) {
runApplication<Di1Application>(*args)
// Create the Spring application context
val context = AnnotationConfigApplicationContext(Config::class.java)
// Retrieve an instance of MyBean
val myBean = context.getBean(Car::class.java)
println(myBean.name)
}Now run the application. BOOM! We get NoUniqueBeanDefinitionException. Why? Simply put, this happened as we are trying to get the bean by type and there are two beans of the same type in the context.
The first solution to this problem is to use @Primary which tells Spring that if there are two or more beans of the same type, you need to annotate the bean with this annotation. Now let's modify the Config class:
Java
@Configuration
public class Config {
@Bean
@Primary
public Car teslaCar() {
return new Car("Tesla", "2023");
}
@Bean
public Car toyotaCar() {
return new Car("Toyota", "2023");
}
}Kotlin
@Configuration
class Config {
@Bean
@Primary
fun teslaCar(): Car {
return Car("Tesla", "2023")
}
@Bean
fun toyotaCar(): Car {
return Car("Toyota", "2023")
}
}After running the code again, it should work and you should find "Tesla" in the output.
Another solution to this problem is matching the bean by the name, so that after removing @Primary from the bean we can do the following:
Java
public class DiApplication {
public static void main(String[] args) {
// Create the Spring application context
var context = new AnnotationConfigApplicationContext(Config.class);
// Retrieve an instance of MyBean
Car myBean = context.getBean("toyotaCar",Car.class);
System.out.println(myBean.getName());
}
}Kotlin
@SpringBootApplication
class Di1Application
fun main(args: Array<String>) {
runApplication<Di1Application>(*args)
// Create the Spring application context
val context = AnnotationConfigApplicationContext(Config::class.java)
// Retrieve an instance of MyBean
val myBean = context.getBean("toyotaCar", Car::class.java)
println(myBean.name)
}We added another argument to the getBean() method, which is responsible for matching the bean by the provided name. By default, the name is the method name. Now what if we want to change the name? We can just pass the name inside the @Bean annotation as the following:
Java
@Configuration
public class Config {
@Bean("tesla")
public Car teslaCar() {
return new Car("Tesla", "2023");
}
@Bean("toyota")
public Car toyotaCar() {
return new Car("Toyota", "2023");
}
}Kotlin
@Configuration
class Config {
@Bean("tesla")
fun teslaCar(): Car {
return Car("Tesla", "2023")
}
@Bean("toyota")
fun toyotaCar(): Car {
return Car("Toyota", "2023")
}
}That's It. We passed the name in the parenthesis. Now let's retrieve it as we did in the previous example:
Java
public class DiApplication {
public static void main(String[] args) {
// Create the Spring application context
var context = new AnnotationConfigApplicationContext(Config.class);
// Retrieve an instance of MyBean
Car myBean = context.getBean("tesla",Car.class);
System.out.println(myBean.getName());
}
}Kotlin
@SpringBootApplication
class Di1Application
fun main(args: Array<String>) {
runApplication<Di1Application>(*args)
// Create the Spring application context
val context = AnnotationConfigApplicationContext(Config::class.java)
// Retrieve an instance of MyBean
val myBean = context.getBean("tesla", Car::class.java)
println(myBean.name)
}The output will be "Tesla".
@Qualifier
Let's do something different and try to inject one of these beans like this:
Java
public class Engine {
private String brand;
private boolean isRunning;
public Engine(String brand, boolean isRunning) {
this.brand = brand;
this.isRunning = isRunning;
}
//omitted getters and setters
}Kotlin
class Engine(
var brand: String,
var isRunning: Boolean
)We created a new class Engine. Let's add it to the context and then inject it into Car.
Java
@Configuration
public class Config {
@Bean("tesla")
public Car teslaCar() {
return new Car("Tesla", "2023");
}
@Bean("toyota")
public Car toyotaCar() {
return new Car("Toyota", "2023");
}
@Bean
public Engine teslaEngine(){
return new Engine("Tesla", true);
}
@Bean
public Engine toyotaEngine(){
return new Engine("Toyota", true);
}
}Kotlin
@Configuration
class Config {
@Bean("tesla")
fun teslaCar(): Car = Car("Tesla", "2023")
@Bean("toyota")
fun toyotaCar(): Car = Car("Toyota", "2023")
@Bean
fun teslaEngine(): Engine = Engine("Tesla", true)
@Bean
fun toyotaEngine(): Engine = Engine("Toyota", true)
}Now let's inject it into the car:
Java
public class Car {
private String name;
private String model;
@Autowired
private Engine engine;
public Car(String name, String model) {
this.name = name;
this.model = model;
}
//omitted getters and setters
}Kotlin
class Car(
var name: String,
var model: String
) {
@Autowired
lateinit var engine: Engine
}Still not enough. The compilation error suggests using @Qualifier as there is more than one of the Engine type.
Now let's update the code:
Java
public class Car {
private String name;
private String model;
@Qualifier("teslaEngine")
@Autowired
private Engine engine;
public Car(String name, String model) {
this.name = name;
this.model = model;
}
//omitted getters and setters
}Kotlin
class Car(
var name: String,
var model: String,
@Qualifier("teslaEngine") var engine: Engine
)Good enough! So what does @Qualifier do here? It removes the ambiguity for Spring to choose which bean to inject.
Now let's run this code:
Java
@SpringBootApplication
public class Di1Application {
public static void main(String[] args) {
var context = new AnnotationConfigApplicationContext(Config.class);
var bean = context.getBean("tesla", Car.class);
System.out.println(bean.getEngine().getBrand());
}
}Kotlin
@SpringBootApplication
class Di1Application
fun main(args: Array<String>) {
runApplication<Di1Application>(*args)
val context = AnnotationConfigApplicationContext(Config::class.java)
val bean = context.getBean("tesla", Car::class.java)
println(bean.engine.brand)
}The output will be "Tesla" again.
loose coupling
Let's create the following:
Java
public interface Engine {
void start();
}Kotlin
interface Engine {
fun start()
}Java
public class DieselEngine implements Engine {
@Override
public void start() {
System.out.println("Goes r-r-r-r... and exhausts black smoke");
}
}Kotlin
class DieselEngine : Engine {
override fun start() {
println("Goes r-r-r-r... and exhausts black smoke")
}
}Java
public class ElectricEngine implements Engine {
@Override
public void start() {
System.out.println("Goes b-z-z-z-z... and produces sparks");
}
}Kotlin
class ElectricEngine : Engine {
override fun start() {
println("Goes b-z-z-z-z... and produces sparks")
}
}Java
public class Vehicle {
private Engine engine;
public Vehicle(Engine engine) {
this.engine = engine;
}
public void drive() {
engine.start();
}
}Kotlin
class Vehicle(private val engine: Engine) {
fun drive() {
engine.start()
}
}Java
@Configuration
public class Config {
@Bean
public Engine dieselEngine() {
return new DieselEngine();
}
@Bean
public Engine electricEngine() {
return new ElectricEngine();
}
@Bean
public Vehicle vehicle(@Qualifier("electricEngine") Engine engine) {
return new Vehicle(engine);
}
}Kotlin
@Configuration
class Config {
@Bean
fun dieselEngine(): Engine = DieselEngine()
@Bean
fun electricEngine(): Engine = ElectricEngine()
@Bean
fun vehicle(@Qualifier("electricEngine") engine: Engine): Vehicle = Vehicle(engine)
}We created the interface Engine and two concrete classes that implement it. Then we injected Engine into the Vehicle using constructor injection. Finally, we created the beans in the class Config and, to remove ambiguity, we use @Qualifier, which tells Spring which one to inject.
Now let's run it and see the results:
Java
@SpringBootApplication
public class DiApplication {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
Vehicle vehicle = context.getBean(Vehicle.class);
vehicle.drive();
}
}Kotlin
@SpringBootApplication
class DiApplication
fun main(args: Array<String>) {
runApplication<Di1Application>(*args)
val context: ApplicationContext = AnnotationConfigApplicationContext(Config::class.java)
val vehicle: Vehicle = context.getBean(Vehicle::class.java)
vehicle.drive()
}The output:
// Goes b-z-z-z-z... and produces sparksConclusion
In this topic, you learned how to implement Spring in an application using different dependency injection methods setter-based, constructor, and field injection. Constructor injection is recommended for its clarity, safety, and simplicity. You also explored type matching and strategies to resolve ambiguity in Spring.