Dependency injection in Spring Boot

Dependency injection in Spring Boot

In this post, we will learn six types of dependency injection in Spring Boot. We will cover each approach with a simple example and compare them.

Types of dependency injection in spring boot

Dependency injection in Spring can be done through

  1. Constructor injection
  2. Setters injection
  3. Field injection

Now, each approach can be executed in two ways — you can:

  • use Java annotations like @Autowired and let Spring scan for components in your codebase
  • define each bean in your spring config XML file.

Pre-requisites

Since the idea of this post is to learn about different methods of injections, our project setup is simple.

Laptop.java class represents a Laptop. Now a Laptop has different attributes like DisplayHardDiskBattery. For now, let’s only consider HardDisk.java.

public class Laptop {
    private HardDisk hardDisk;

    public Laptop(HardDisk hardDisk){
        this.hardDisk = hardDisk;
    }
    public void saveData(){
        hardDisk.saveData();
    }
}

HardDisk.java is an interface which is implemented by two HardDisk implementations: SanDiskHD and HitachiHD.

public interface HardDisk {
    public void saveData();
}
public class HitachiHD implements HardDisk {
    public void saveData(){
        System.out.println("HitachiHD: saveData");
    }
}
public class SanDiskHD implements HardDisk {
    public void saveData(){
        System.out.println("SanDiskHD: saveData");
    }
}

The point of having dependency injection is to follow the loose coupling principle in our code design.

Now we will go over the ways in which different implementations of HardDisk (i.e. SanDiskHD and HitachiHD) can be injected into Laptop.

Injection using Annotations

  • In my experience, this approach is widely used for large projects. This is also the one I use the most.

Spring will scan your Java code for annotations and automatically register beans in the spring container.

For enabling injection through annotations, you should:

Run, and see the results.

 20:13:24.494 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'hardDisk'
 HitachiHD: saveData

Fetch the bean using bean id.

 public static void main(String[] args) {
         ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
         HardDisk hd = ctx.getBean("hardDisk", HardDisk.class);
         hd.saveData();
         SpringApplication.run(DemoApplication.class, args);
 }

Mark classes with @Component annotation. Optionally, you can also provide a bean id.

 @Component("hardDisk")
 public class HitachiHD implements HardDisk {
     public void saveData(){
         System.out.println("HitachiHD: saveData");
     }
 }

Enable component scanning in your project in your spring config file.

 <?xml version="1.0" encoding="UTF-8"?>
 <beans xmlns="<http://www.springframework.org/schema/beans>"
        xmlns:xsi="<http://www.w3.org/2001/XMLSchema-instance>"
        xmlns:context="<http://www.springframework.org/schema/context>"
        xsi:schemaLocation="<http://www.springframework.org/schema/beans>
     <http://www.springframework.org/schema/beans/spring-beans.xsd>
     <http://www.springframework.org/schema/context>
     <http://www.springframework.org/schema/context/spring-context.xsd>">

 <!--    Enable component scan-->
     <context:component-scan base-package="com.learningspring.demo.src" />
 </beans>
💡 Now, spring also assigns a default bean id for a bean if you don’t give one. The pattern is a camel-case version of the class name.

So a bean of type HitachiHD would have a bean id of hitachiHD.

Injection using Annotations and Autowiring

What is Autowiring?

Spring can automatically find beans by matching the type (class or interface) and inject it automatically.

So, if you want to inject a HardDisk implementation into Laptop, you can annotate the HardDisk attribute of Laptop class with @Autowired.

This will tell spring to scan all the components in your code, find a bean which matches the class of type HardDisk, and inject it into the Laptop.

For eg: If you annotate HitachiHD with @Component, it will inject HitachiHD into Laptop.

Now, there are three ways of autowiring beans:

  1. Field injection
  2. Constructor injection
  3. Setter injection

Field Injection

To test this autowiring, let’s get the Laptop bean and invoke it’s saveData() to see the results.

 public static void main(String[] args) {
         ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
         Laptop l = ctx.getBean("laptop", Laptop.class);
         l.saveData();
         SpringApplication.run(DemoApplication.class, args);
     }

Inject the HardDisk bean into Laptop by annotating the HardDisk attribute with @Autowired.

 @Component
 public class Laptop {
     @Autowired
     private HardDisk hardDisk;

     public void saveData(){
         hardDisk.saveData();
     }
 }

Annotate the implementation of HardDisk you want to inject into Laptop with @Component.

 @Component
 public class HitachiHD implements HardDisk {
     public void saveData(){
         System.out.println("HitachiHD: saveData");
     }
 }
21:23:50.350 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'hitachiHD'
21:23:50.351 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'laptop'
HitachiHD: saveData

Drawbacks:

  • Costlier than constructor-based or setter-based injection.
  • Incentivises to have many dependencies injected into a class, which cause design issues.

Constructor Injection

There are two patterns:

Annotating only the specific attribute with with @Autowired in the constructor.

 @Component
 public class Laptop {
     private HardDisk hardDisk;

     public Laptop(@Autowired HardDisk hardDisk) {
         this.hardDisk = hardDisk;
     }

     public void saveData(){
         hardDisk.saveData();
     }
 }

Annotating entire constructor with @Autowired.

 @Component
 public class Laptop {
     private HardDisk hardDisk;

     @Autowired
     public Laptop(HardDisk hardDisk) {
         this.hardDisk = hardDisk;
     }

     public void saveData(){
         hardDisk.saveData();
     }
 }

Setter Injection

Here you annotate the setter method of the relevant attribute with @Autowired.

@Component
public class Laptop {
    private HardDisk hardDisk;

    @Autowired
    public void setHardDisk(HardDisk hardDisk) {
        this.hardDisk = hardDisk;
    }

    public void saveData(){
        hardDisk.saveData();
    }
}

What kind of dependency injection should you use?

A good answer might be — to follow the pattern already followed in the codebase you’re working on.

What if there are multiple implementations annotated with@Component?

Because autowiring by type may lead to multiple candidates, it is often necessary to have more control over the selection process. One way to accomplish this is with Spring’s @Primary annotation.

@Primary indicates that a particular bean should be given preference when multiple beans are candidates to be autowired to a single-valued dependency. If exactly one primary bean exists among the candidates, it becomes the autowired value.

If you want to always autowire HitachiHD implementation as a HardDisk, you can annotate it with @Primary:

@Primary
@Component
public class HitachiHD implements HardDisk {
    public void saveData(){
        System.out.println("HitachiHD: saveData");
    }
}

Dependency Injection with XML configuration

Another way to configure Spring runtime with constructor-based dependency injection is to use an XML configuration file.

In this case, we need to bootstrap our Spring application context using ClassPathXmlApplicationContext.

This approach is not used commonly, and you’ll see it rarely in professional codebases. This is because for large projects, defining each bean in XML configuration file is not practical.

Constructor Injection

For injecting a bean into a class,

You have to define the beans you want to inject into Laptop object using constructor-arg in applicationContext.xml file. applicationContext.xml is a spring config file from which we can the set different properties for our Spring container.

 <?xml version="1.0" encoding="UTF-8"?>
 <beans xmlns="<http://www.springframework.org/schema/beans>"
        xmlns:xsi="<http://www.w3.org/2001/XMLSchema-instance>"
        xmlns:context="<http://www.springframework.org/schema/context>"
        xsi:schemaLocation="<http://www.springframework.org/schema/beans>
     <http://www.springframework.org/schema/beans/spring-beans.xsd>
     <http://www.springframework.org/schema/context>
     <http://www.springframework.org/schema/context/spring-context.xsd>">

     <context:annotation-config/>
     <!--Defining beans here-->

     <bean id="hardDisk" class="com.learningspring.demo.src.harddisk.HitachiHD"/>
     <bean id="laptop" class="com.learningspring.demo.src.laptop.Laptop" >
         <constructor-arg ref="hardDisk"/>
     </bean>
 </beans>

First, you need to accept the HardDisk bean in the constructor definition:COPY

 public class Laptop {
     private HardDisk hardDisk;

     public Laptop(HardDisk hardDisk){
         this.hardDisk = hardDisk;
     }
     public void saveData(){
         hardDisk.saveData();
     }
 }

Let’s test this thing out by fetching the Laptop bean and invoking it’s saveData() method.

public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        Laptop l = ctx.getBean("laptop", Laptop.class);
        l.saveData();
        SpringApplication.run(DemoApplication.class, args);
    }
00:22:04.154 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'hardDisk'
00:22:04.158 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'laptop'
HitachiHD: saveData

So basically, we injected HitachiHD bean into our laptop object through its constructor.

This is called constructor injection.

Setters Injection

Configure dependency injection in applicationContext.xml.

 <?xml version="1.0" encoding="UTF-8"?>
 <beans xmlns="<http://www.springframework.org/schema/beans>"
        xmlns:xsi="<http://www.w3.org/2001/XMLSchema-instance>"
        xmlns:context="<http://www.springframework.org/schema/context>"
        xsi:schemaLocation="<http://www.springframework.org/schema/beans>
     <http://www.springframework.org/schema/beans/spring-beans.xsd>
     <http://www.springframework.org/schema/context>
     <http://www.springframework.org/schema/context/spring-context.xsd>">

     <context:annotation-config/>
     <!--Defining beans here-->

     <bean id="hd" class="com.learningspring.demo.src.harddisk.HitachiHD"/>
     <bean id="laptop" class="com.learningspring.demo.src.laptop.Laptop" >
         <property name="hardDisk" ref="hd"/>
     </bean>
 </beans>

Create a setter method in Laptop class for injections.

 public class Laptop {
     private HardDisk hardDisk;

     public void setHardDisk(HardDisk hardDisk) {
         this.hardDisk = hardDisk;
     }

     public void saveData(){
         hardDisk.saveData();
     }
 }

Let’s run our application.

00:37:43.878 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'hd'
00:37:43.882 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'laptop'
HitachiHD: saveData

So basically, we injected HitachiHD bean into our laptop object through its setter method.

This is called setter injection.

Injecting Literal Values through setters

Let’s say we want to inject some hard-coded value into our object. How do we do that?

Well, we can leverage the setter methods for the literal value we want to inject and pass it in applicationContext.xml file.

<bean id="hd" class="com.learningspring.demo.src.harddisk.HitachiHD"/>
<bean id="laptop" class="com.learningspring.demo.src.laptop.Laptop" >
    <property name="hardDisk" ref="hd"/>
    <property name="brandName" value="Apple"/>
</bean>
public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        Laptop l = ctx.getBean("laptop", Laptop.class);
        l.saveData();
        System.out.println(l.getBrandName());
        SpringApplication.run(DemoApplication.class, args);
    }
00:48:23.773 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'hd'
00:48:23.777 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'laptop'
HitachiHD: saveData
Apple

Injecting values from properties file

  1. Create a properties file
  2. Load properties file in the spring config file
brandName = Apple

Reference values from properties file in Java code. We can access it through ${variable_name}.

 <?xml version="1.0" encoding="UTF-8"?>
 <beans xmlns="<http://www.springframework.org/schema/beans>"
        xmlns:xsi="<http://www.w3.org/2001/XMLSchema-instance>"
        xmlns:context="<http://www.springframework.org/schema/context>"
        xsi:schemaLocation="<http://www.springframework.org/schema/beans>
     <http://www.springframework.org/schema/beans/spring-beans.xsd>
     <http://www.springframework.org/schema/context>
     <http://www.springframework.org/schema/context/spring-context.xsd>">

     <context:annotation-config/>
     <context:property-placeholder location="classpath:application.properties"/>
     <!--Defining beans here-->

     <bean id="hd" class="com.learningspring.demo.src.harddisk.HitachiHD"/>
     <bean id="laptop" class="com.learningspring.demo.src.laptop.Laptop" >
         <property name="hardDisk" ref="hd"/>
         <property name="brandName" value="${brandName}"/>
     </bean>
 </beans>