Welcome to this lesson on Dependency Injection in Spring Boot. So far, we've covered the basics of Spring and Spring Boot, examined the typical project structure, and delved into the important files within a Spring Boot project. We've also discussed core concepts like Inversion of Control (IoC
) and Dependency Injection (DI
). In our last lesson, we learned how to create simple beans without dependencies. Today, we’ll build on that foundation by learning how to create beans with dependencies, leveraging Spring’s powerful DI capabilities.
Dependency Injection is a straightforward concept that becomes incredibly powerful when used correctly. Essentially, if you manually instantiate an object, you have a few approaches:
- Use the default constructor and instantiate properties via setters.
- Use a non-default constructor and pass all parameters into it.
- Use the default constructor with reflection to instantiate private class fields.
Spring simplifies this process by searching for dependencies by type, making it easier to wire dependencies into your classes.
To handle this automatic wiring, Spring uses the @Autowired
annotation, which marks the points where dependencies should be injected. In Kotlin, you can use val
, var
, and property injection effectively.
Constructor-based DI is the preferred method in Kotlin due to its compatibility with the primary constructor paradigm and alignment with immutability principles. This involves passing all dependencies into the constructor to create an object:
Kotlin1package com.codesignal 2 3import org.springframework.stereotype.Component 4 5@Component 6class Salad( 7 private val lettuce: Lettuce, 8 private val tomato: Tomato 9)
In this example, the Salad
class uses a primary constructor where the dependencies lettuce
and tomato
are injected. The @Autowired
annotation is omitted because Spring will automatically use the primary constructor for dependency injection when only one constructor is present. Constructor injection is generally preferred in Kotlin for better readability, immutability, and testability.
Although Spring boot automatically detects only one constructor, you can explicitly mark the constructor with @Autowired
:
Kotlin1package com.codesignal 2 3import org.springframework.beans.factory.annotation.Autowired 4import org.springframework.stereotype.Component 5 6@Component 7class Sandwich @Autowired constructor( 8 private val bread: Bread, 9 private val cheese: Cheese 10)
Here, the @Autowired
annotation explicitly marks the constructor, making it clear where Spring should perform the injection.
Field-based DI involves directly injecting dependencies into class fields:
Kotlin1package com.codesignal 2 3import org.springframework.beans.factory.annotation.Autowired 4import org.springframework.stereotype.Component 5 6@Component 7class Juice { 8 9 @Autowired 10 lateinit var water: Water 11 12 @Autowired 13 lateinit var sugar: Sugar 14 15 // Getters for water and sugar can be added if needed 16}
In this example, @Autowired
is placed directly on the properties water
and sugar
. Note that the lateinit
keyword is used to indicate that these properties will be initialized later. However, constructor injection is generally preferred for better testability and encapsulation.
Using a mixed approach can be beneficial by injecting essential dependencies through the constructor and optional ones through field injection with setters if needed:
Kotlin1package com.codesignal 2 3import org.springframework.beans.factory.annotation.Autowired 4import org.springframework.stereotype.Component 5 6@Component 7class Pizza @Autowired constructor( 8 private val dough: Dough, 9 private val sauce: Sauce 10) { 11 12 @Autowired 13 lateinit var cheese: Cheese 14}
In this example, the Pizza
class is annotated with @Component
, making it a Spring-managed bean. The class uses constructor-based dependency injection for Dough
and Sauce
as essential dependencies. Although the @Autowired
annotation is not required on the constructor (since Spring will automatically use the primary constructor if only one is present), it can be included for clarity.
Additionally, the cheese
property is marked with @Autowired
and lateinit
, indicating that Spring will inject this dependency after the object's construction. This example demonstrates a mix of mandatory constructor-based injection and field-based injection, providing flexibility in managing dependencies.
Spring also allows for method-based dependency injection using the @Bean
annotation. This technique involves injecting dependencies via method parameters in Kotlin configuration classes.
Example:
Kotlin1package com.codesignal 2 3import org.springframework.context.annotation.Bean 4import org.springframework.context.annotation.Configuration 5 6@Configuration 7class AppConfig { 8 9 @Bean 10 fun sandwich(bread: Bread, cheese: Cheese): Sandwich { 11 return Sandwich().apply { 12 this.bread = bread 13 this.cheese = cheese 14 } 15 } 16 17 @Bean 18 fun bread(): Bread { 19 return Bread() 20 } 21 22 @Bean 23 fun cheese(): Cheese { 24 return Cheese() 25 } 26}
In this example, the sandwich
method is annotated with @Bean
, and it takes Bread
and Cheese
as parameters. Spring's application context will automatically resolve these dependencies by calling the bread
and cheese
methods, respectively, to supply the required beans. This method-based injection is useful for creating bean instances that depend on other beans defined in the same configuration class.
In this lesson, we explored various Dependency Injection methods in Spring Boot, including setter, constructor, field injection, @Bean
method injection, along with a mixed approach combining these methods. These techniques help create modular, testable, and maintainable applications. Up next, you’ll get hands-on practice with these concepts to solidify your understanding.