Lesson 4
Dependency Injection in Spring
Introduction

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 at a Glance

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 Dependency Injection

Setter-based DI involves creating an object using the default constructor and then setting its dependencies through setter methods.

Example:

Java
1package 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 Dependency Injection

Constructor-based DI involves passing all dependencies into the constructor to create an object.

Example:

Java
1package 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 Dependency Injection

Field-based DI involves directly injecting dependencies into class fields, even if they are private.

Example:

Java
1package 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.

Mixed Approach

Using a mixed approach can be beneficial by injecting mandatory dependencies through the constructor and optional ones through setters.

Example:

Java
1package 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.

Method Dependency Injection with `@Bean`

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:

Java
1package 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.

Summary

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.

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.