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 and 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. You can place @Autowired
on constructors, methods, and properties to indicate where Spring should inject dependencies.
Setter-based DI involves creating an object using the default constructor and then setting its dependencies through setter methods.
Example:
Java1package com.codesignal; 2 3import org.springframework.beans.factory.annotation.Autowired; 4import org.springframework.stereotype.Component; 5 6@Component 7public class Sandwich { 8 9 private Bread bread; 10 private Cheese cheese; 11 12 public Sandwich() { 13 // Default constructor 14 } 15 16 @Autowired 17 public void setBread(Bread bread) { 18 this.bread = bread; 19 } 20 21 @Autowired 22 public void setCheese(Cheese cheese) { 23 this.cheese = cheese; 24 } 25}
In this example, @Autowired
on the setBread
and setCheese
methods indicates that Spring should automatically call these setter methods to pass the appropriate Bread
and Cheese
dependencies. Spring first uses the default constructor to create an instance of Sandwich
, then calls these methods to inject the dependencies.
Constructor-based DI involves passing all dependencies into the constructor to create an object.
Example:
Java1package com.codesignal; 2 3import org.springframework.beans.factory.annotation.Autowired; 4import org.springframework.stereotype.Component; 5 6@Component 7public class Salad { 8 9 private Lettuce lettuce; 10 private Tomato tomato; 11 12 // @Autowired is optional here 13 public Salad(Lettuce lettuce, Tomato tomato) { 14 this.lettuce = lettuce; 15 this.tomato = tomato; 16 } 17}
In this example, although @Autowired
is used on the constructor (it is optional when there is only one constructor), it indicates that Spring should use this constructor and provide the necessary Lettuce
and Tomato
dependencies. Spring finds all the required dependencies in the application context and uses them in the Salad
constructor to instantiate the object.
Field-based DI involves directly injecting dependencies into class fields, even if they are private.
Example:
Java1package com.codesignal; 2 3import org.springframework.beans.factory.annotation.Autowired; 4import org.springframework.stereotype.Component; 5 6@Component 7public class Juice { 8 9 @Autowired 10 private Water water; 11 12 @Autowired 13 private Sugar sugar; 14 15 // Getters for water and sugar can be added if needed 16}
In this example, @Autowired
is placed directly on the fields water
and sugar
. Spring uses reflection to inject dependencies into these private fields. While convenient, this method is generally discouraged due to its reduced testability and encapsulation issues.
Using a mixed approach can be beneficial by injecting mandatory dependencies through the constructor and optional ones through setters.
Example:
Java1package com.codesignal; 2 3import org.springframework.beans.factory.annotation.Autowired; 4import org.springframework.stereotype.Component; 5 6@Component 7public class Pizza { 8 9 private Dough dough; 10 private Sauce sauce; 11 private Cheese cheese; 12 13 @Autowired 14 public Pizza(Dough dough, Sauce sauce) { 15 this.dough = dough; 16 this.sauce = sauce; 17 } 18 19 @Autowired 20 public void setCheese(Cheese cheese) { 21 this.cheese = cheese; 22 } 23}
In this example, dough
and sauce
are mandatory and are injected through the constructor, while cheese
is optional and is injected via a setter method. The @Autowired
annotation helps Spring understand where and how to inject the dependencies.
Spring also allows for method-based dependency injection using the @Bean
annotation. This technique involves injecting dependencies via method parameters in Java configuration classes.
Example:
Java1package com.codesignal; 2 3import org.springframework.context.annotation.Bean; 4import org.springframework.context.annotation.Configuration; 5 6@Configuration 7public class AppConfig { 8 9 @Bean 10 public Sandwich sandwich(Bread bread, Cheese cheese) { 11 return new Sandwich(bread, cheese); 12 } 13 14 @Bean 15 public Bread bread() { 16 return new Bread(); 17 } 18 19 @Bean 20 public Cheese cheese() { 21 return new Cheese(); 22 } 23}
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.