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
- Constructor injection
- Setters injection
- 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();
}
}
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 typeHitachiHD
would have a bean id ofhitachiHD
.
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:
- Field injection
- Constructor injection
- 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
- Create a properties file
- 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>