DEV Community

Cover image for Six ways of dependency injection in Spring Boot — A Deep Dive
Aditya Karad
Aditya Karad

Posted on • Originally published at codingchronicles.adityakarad.com

Six ways of dependency injection in Spring Boot — A Deep Dive

Introduction

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 Display, HardDisk, Battery. 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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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

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:

  1. 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>
    
  2. 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");
        }
    }
    
  3. 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);
    }
    
  4. 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
    

💡 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

  1. 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");
        }
    }
    
  2. 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();
        }
    }
    
  3. 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);
        }
    
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
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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();
        }
    }
    
  2. 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();
        }
    }
    

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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,

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

    public class Laptop {
        private HardDisk hardDisk;
    
        public Laptop(HardDisk hardDisk){
            this.hardDisk = hardDisk;
        }
        public void saveData(){
            hardDisk.saveData();
        }
    }
    
  2. 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>
    

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);
    }
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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

This is called constructor injection.

Setters Injection

  1. 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();
        }
    }
    
  2. 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>
    

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
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);
    }
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

Injecting values from properties file

  1. Create a properties file

  2. Load properties file in the spring config file

brandName = Apple
Enter fullscreen mode Exit fullscreen mode
  1. 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>
    

That's it, folks

Coding Chronicles logo

If you like these posts, do subscribe to the newsletter. You'll be the first to know when a new blog post is released on Coding Chronicles.

Top comments (0)