There are many different ways to perform dependency injection (DI) in Spring. As Spring evolves over time, the DI configuration also changes over time, which may contribute to some confusion to those who are just learning Spring.
Scenario
Let’s assume we have this scenario: We have Team Awesome that consists of one awesome member, called Donkey Hulk. Donkey Hulk can only do two things: walk and smash. When someone calls Team Awesome for help, Donkey Hulk will walk around and smash stuff. Okay, I’m very tired this early AM, but you get the point here…
Once we have configured the dependency injection, we are going to use the code below to call for help.
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
TeamAwesome teamAwesome = context.getBean(TeamAwesome.class);
System.out.println(teamAwesome.rescue());
}
}
Solution #1: Solving with Angular Stuff
… and no, I’m not referring to AngularJS. Rather, I’m talking about angle brackets here, also known as XML.
To accomplish DI using XML, we created DonkeyHulk
class…
public class DonkeyHulk {
public String walk() {
return "DonkeyHulk walks slowly.";
}
public String smash() {
return "DonkeyHulk smashes some pumpkins.";
}
}
… and created TeamAwesome
class…
public class TeamAwesome {
private DonkeyHulk donkeyHulk;
public void setDonkeyHulk(DonkeyHulk donkeyHulk) {
this.donkeyHulk = donkeyHulk;
}
public String rescue() {
return donkeyHulk.walk() + " " + donkeyHulk.smash();
}
}
Finally, we use XML to wire them up.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="donkeyHulk" class="com.choonchernlim.epicapp.service.DonkeyHulk"/>
<bean id="teamAwesome" class="com.choonchernlim.epicapp.service.TeamAwesome">
<property name="donkeyHulk" ref="donkeyHulk"/>
</bean>
</beans>
Solution #2: @Service and @Autowired
Realizing all the XML mess, Spring 2.5 introduced several useful annotations, most importantly @Autowired
and @Service
.
With these annotations, we essentialy cut down the XML configurations down to one line:-
<?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:component-scan base-package="com.choonchernlim.epicapp"/>
</beans>
For DonkeyHulk
class, all we need to do is to annotate the class with @Service
annotation so that the component scan is aware of this Spring bean.
@Service
public class DonkeyHulk {
public String walk() {
return "DonkeyHulk walks slowly.";
}
public String smash() {
return "DonkeyHulk smashes some pumpkins.";
}
}
Now, this is where things get a little tricky, especially for those who are just learning Spring. The @Autowired
annotation allows you to autowire a bean at the field level, method level or constructor level.
Solution #2a: @Autowired at Field Level
This is probably the simplest way to inject a dependency into a class. All we do here is to create a field and annotate it with @Autowired
. It can’t get any simpler than this.
@Service
public class TeamAwesome {
@Autowired
private DonkeyHulk donkeyHulk;
public String rescue() {
return donkeyHulk.walk() + " " + donkeyHulk.smash();
}
}
While this may seem like an elegant piece of code, the biggest downside to this approach is there’s no way we can mock the injected dependency in our test cases. Donkey Hulk may take one minute to walk before it starts smashing, and everytime we test Team Awesome, we have to suffer through and wait for Donkey Hulk to do its job.
That said, this approach works, if:-
- The injected dependency is relatively fast.
- The injected dependency does not interact with external system(s).
- You don’t write test cases.
Solution #2b: @Autowired at Method Level
To fix the above problem, we can perform the dependency injection at the method level.
@Service
public class TeamAwesome {
private DonkeyHulk donkeyHulk;
@Autowired
public void setDonkeyHulk(DonkeyHulk donkeyHulk) {
this.donkeyHulk = donkeyHulk;
}
public String rescue() {
return donkeyHulk.walk() + " " + donkeyHulk.smash();
}
}
With this approach is we can mock (or spy) Donkey Hulk and make it walk faster.
The downside to this approach is it doesn’t look “semantically” correct, especially for those who comes from the Object Oriented (OO) world. Let’s assume the Team Awesome now consists of three members:-
@Service
public class TeamAwesome {
private DonkeyHulk donkeyHulk;
private SpiderPig spiderPig;
private NinjaTortoise ninjaTortoise;
@Autowired
public void setDonkeyHulk(DonkeyHulk donkeyHulk) {
this.donkeyHulk = donkeyHulk;
}
@Autowired
public void setSpiderPig(SpiderPig spiderPig) {
this.spiderPig = spiderPig;
}
@Autowired
public void setNinjaTortoise(NinjaTortoise ninjaTortoise) {
this.ninjaTortoise = ninjaTortoise;
}
public String rescue() {
return ...;
}
}
See the problem here? The OO programers are taught to create objects using constructors and the constructor’s job is to contruct the object. Now, there are all these setter methods that “seem” to violate the principle of immutability. Further, these setter methods tend to clutter our otherwise perfect code.
Solution #2c: @Autowired at Constructor Level
A much better approach is to perform the dependency injection at the constructor level.
@Service
public class TeamAwesome {
private DonkeyHulk donkeyHulk;
@Autowired
public TeamAwesome(DonkeyHulk donkeyHulk) {
this.donkeyHulk = donkeyHulk;
}
public String rescue() {
return donkeyHulk.walk() + " " + donkeyHulk.smash();
}
}
Even for those who don’t know Spring, by inspecting the constructor, they know that Team Awesome requires Donkey Hulk for it to work.
Going back to the previous example where Team Awesome has three members, it is as easy as passing them into the same constructor.
@Service
public class TeamAwesome {
private DonkeyHulk donkeyHulk;
private SpiderPig spiderPig;
private NinjaTortoise ninjaTortoise;
@Autowired
public TeamAwesome(DonkeyHulk donkeyHulk,
SpiderPig spiderPig,
NinjaTortoise ninjaTortoise) {
this.donkeyHulk = donkeyHulk;
this.spiderPig = spiderPig;
this.ninjaTortoise = ninjaTortoise;
}
public String rescue() {
return ...;
}
}
What if we have two implementations of Donkey Hulk: the default implementation and a faster implementation of Donkey Hulk? We still can choose which implementation to use with @Qualifier
annotation without affecting other injected dependencies.
@Service
public class TeamAwesome {
private DonkeyHulk donkeyHulk;
private SpiderPig spiderPig;
private NinjaTortoise ninjaTortoise;
@Autowired
public TeamAwesome(@Qualifier("fasterDonkeyHulk") DonkeyHulk donkeyHulk,
SpiderPig spiderPig,
NinjaTortoise ninjaTortoise) {
this.donkeyHulk = donkeyHulk;
this.spiderPig = spiderPig;
this.ninjaTortoise = ninjaTortoise;
}
public String rescue() {
return ...;
}
}
Solution #3: Solving with @Configuration
What if you are so-called “Java purist” and you hate to see your project code cluttered with Spring annotations?
The good news is Spring 3.0 introduced @Configuration
annotation that allows us to use Java-based configuration.
Both DonkeyHulk
and TeamAwesome
look like regular Java code.
public class DonkeyHulk {
public String walk() {
return "DonkeyHulk walks slowly.";
}
public String smash() {
return "DonkeyHulk smashes some pumpkins.";
}
}
public class TeamAwesome {
private DonkeyHulk donkeyHulk;
public TeamAwesome(DonkeyHulk donkeyHulk) {
this.donkeyHulk = donkeyHulk;
}
public String rescue() {
return donkeyHulk.walk() + " " + donkeyHulk.smash();
}
}
Then, we create a separate configuration class annotated with @Configuration
that contains the dependency injection configurations.
@Configuration
public class MyConfiguration {
@Bean
public DonkeyHulk getDonkeyHulk() {
return new DonkeyHulk();
}
@Bean
public TeamAwesome getTeamAwesome(DonkeyHulk donkeyHulk) {
return new TeamAwesome(donkeyHulk);
}
}
The downside to this approach is… well, let me tell a story here…
… Once upon a time, we the Java developers liked to hardcode our configurations in the Java code. Then, someone said “That’s a BAD idea and we should externalize them into a flat file!”. As we looked for a solution, we stumbled upon something called the dot properties file. Then, someone said “This key-value pair structure is too simple, and we can’t group things nicely!”. Then, we stumbled upon the epic angle brackets called the Extensible Markup Language. As years passed by, the project scope got larger, and we the Java developers spent more time managing XML code than Java code. The Ruby developers laugh at us and our pride hurt for many years. Then, the annotation-based configuration saved the day and we lived happily ever since. Not too long ago, somebody with too much of free time decided to come up with Java-based configuration to completely eliminate all the remaining XML-based configuration. We the Java developers celebrated and rejoiced for the epic discovery. Then, wait… WTF?? Are we repeating the history here??
For the record, there are many important uses of Java-based configuration especially when dealing with configurations that has to be determined at runtime. However, this example of Team Awesome is not one of them.
What’s My Take?
My take is to use @Autowired
at the constructor level 90% of the time. Then, depending on the problem set, I may use XML-based configuration or Java-based configuration where I deem fit.
Leave a Reply