Spring DI Patterns: The Good, The Bad, and The Ugly
Let's explore the three ways Spring lets you declare dependencies using annotations: field injection, setter injection, and constructor injection.
Join the DZone community and get the full member experience.
Join For FreeSpring developers will be familiar with its powerful Dependency Injection API. It allows you to declare @Bean
s that Spring then instantiates and manages. Any dependencies between these beans are then resolved by Spring and injected automagically.
Three Annotation-Based Injection Patterns
There are three ways Spring lets you declare the dependencies of your class using annotations:
- Field injection (the bad)
import org.springframework.beans.factory.annotation.Autowired; public class MyBean { @Autowired private AnotherBean anotherBean; //Business logic... }
- Setter injection (the ugly)
import org.springframework.beans.factory.annotation.Autowired; public class MyBean { private AnotherBean anotherBean; @Autowired public void setAnotherBean(final AnotherBean anotherBean) { this.anotherBean = anotherBean; } //Business logic... }
- Constructor injection (the good)
public class MyBean { private final AnotherBean anotherBean; public MyBean(final AnotherBean anotherBean) { this.anotherBean = anotherBean; } //Business logic... }
The Inconvenient Truth About Field Injection
The most common of all of these patterns is the field injection pattern — most likely because it’s the most convenient of the three patterns. Unfortunately, because of its ubiquity, developers rarely learn about the other two patterns and the pros and cons associated with each of them.
Classes Using Field Injection Tend to Become Harder to Maintain
When you use the field injection pattern and want to further add dependencies to your class, you only need to make a field, slap an @Autowired
or @Inject
annotation on it, and you’re good to go. At first, this sounds great, but a few months down the line, you realize that your class has grown into a God class that’s hard to manage. Granted, this may very well happen with the two other patterns as well, but they force you to pay more attention to the dependencies in your class.
Every Time You Use Field Injection, a Unit Test Dies
This line has stuck with me since I saw Josh Long’s talk on Spring Boot, and in a sense is what motivated me to write this article. How do you go about testing classes using field injection? Chances are you’re somewhere along the path of memorizing the unintuitive Mockito
idiom for doing this:
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class MyBeanTest {
@Mock
private AnotherBean anotherBean;
@InjectMocks
private MyBean target;
//Tests...
}
This reflection workaround is riddled with additional workarounds developers have to master:
- What if
MyBean
has multiple dependencies of typeAnotherBean
? - Should you create a new instance of the
target
, or just declare it? Is there a difference? - Do you have type safety with dependencies using generics?
- What if you want a real implementation of some of the dependencies, but not for others?
Classes Using Field Injection Are Non-Final, but Are Prone to Circular Dependencies
If you try and declare an @Autowired
field as final
, you’ll get a compile error. This is annoying in and of itself, as the field is only being set once anyway! Unless you also annotate it as being @Lazy
, Spring tries to resolve your dependencies as part of a DAG during startup, and your field may be the source of a BeanCurrentlyInCreationException
due to unexpected circular dependencies.
For example:
public class A {
@Autowired
private B b;
}
public class B {
@Autowired
private C c;
}
public class C {
@Autowired
private A a;
}
The problem is never this simple, and the actual problem tends to span dozens of classes through a maze of inheritance, libraries, and across architecture boundaries. This problem can be resolved by either making one of the dependencies not required (allow the field to be null with @Autowired(required = false)
) or lazy (set the field after resolving beans that depend on this bean with @Lazy
). As everyone who has experienced this dreadful exception knows, finding exactly which beans make up this circle is usually a long and arduous endeavor. And once you have found all the pieces to your puzzle, which dependency do you sacrifice? How do you document this decision properly?
There is an unreasonable number of workarounds to solving the circular dependency problem, but hopefully by now, something is starting to smell.
Pros
- The tersest of all the patterns.
- Most Java developers are aware of this pattern.
Cons
- Convenience tends to hide code design red flags.
- Hard to test.
- Dependencies are unnecessarily mutable.
- Prone to circular dependency issues.
- Requires the use of (multiple) Spring or Java EE annotations.
public void setDependency(Dependency dependency)
Boilerplate and Encapsulation
Among all the patterns, the setter injection pattern has the most boilerplate by far. Each bean must have a setter and each setter must be decorated with an @Autowired
or @Inject
annotation. Although this boilerplate definitely solves the problem of not thinking about the number of dependencies you’re injecting into your class (the plethora of setters in your class should be a dead giveaway), it comes with another design smell. You’re violating encapsulation by exposing the innards of your class.
Classes Using Setter Injection Make Testing Easy
There’s no reflection magic required. Just set the dependencies you wish you had.
import org.junit.Before;
import org.mockito.Mockito;
public class MyBeanTest {
private MyBean target = new MyBean();
private AnotherBean anotherBean = Mockito.mock(AnotherBean.class);
@Before
public void setUp() {
myBean.setAnotherBean(anotherBean);
}
//Tests...
}
Classes Using Setter Injection Are Immune to Circular Dependencies
In using this pattern, Spring doesn’t try to represent your beans in a DAG, which means that circular dependencies are permitted, but it also means that circular dependencies are permitted. Allowing circular dependencies is a double-edged sword. You won’t have to debug the nasty issues that occur as a result of circular dependencies, but your code is much harder to break apart later on. The BeanCurrentlyInCreationException
is, in fact, informing you on startup that your design is flawed.
Pros
- Immune to circular dependency issues.
- Highly coupled classes are easily identified as setters are added.
Cons
- Violates encapsulation.
- Circular dependencies are hidden.
- The most boilerplate code of the three patterns.
- Dependencies are unnecessarily mutable.
The Solution: Constructor Injection
It turns out that the best solution to dependency injection is constructor injection. Newer platforms that support DI, such as Angular, have learned the lessons from other platforms and only support constructor injection.
Constructor Injection Exposes Excessive Coupling
Whenever your class needs a new dependency, you’re forced to add it to the constructor’s parameters. This, in turn, forces you to recognize the number of coupling in your class. I’ve found that fewer than 3 dependencies is good, and anything above 5 is begging for refactoring. Counting my dependencies is a piece of cake when they’re defined on just a few contiguous lines.
As an added bonus, since final
fields can be initialized in the constructor, our dependencies can be immutable — as they should be!
Testing Constructor Injected Classes Is Trivial
It’s arguably even easier than with setter injection.
import org.mockito.Mockito;
public class MyBeanTest {
private AnotherBean anotherBean = Mockito.mock(AnotherBean.class);
private MyBean target = new MyBean(anotherBean);
//Tests...
}
Constructor Injected Subclasses Must Have Non-Default Constructors
Any subclass of a class using constructor injection must have a constructor that calls the parent constructor. This is annoying if you have inherited Spring components. I personally haven’t run into this very often. I try and avoid injected dependencies in parent components — usually done through composition over inheritance.
Pros
- Dependencies can be immutable.
- Recommended by Spring.
- Easiest to test out of all the patterns.
- Highly coupled classes are easily identified as constructor parameters grow.
- Familiar to developers coming from other platforms.
- No dependency on the
@Autowired
annotation.
Cons
- Constructors trickle down to subclasses.
- Prone to circular dependency issues.
Conclusion
Use the constructor injection pattern.
There are times where the other patterns make sense too, but “for the sake of being consistent with the rest of the codebase” and “it’s just easier to use the field injection pattern” are not valid excuses.
For example, using the setter injection pattern is a good intermediary step when migrating from beans declared in XML files that already use the setter pattern, or when you need to fix a BeanCurrentlyInCreationException
in production ASAP (not that you should ever end up in that position anyway).
Even the field injection pattern is sufficient when, say, sketching out a solution or answering questions on StackOverflow, unless their question is about dependency injection in Java of course. In which case you should refer them to this article.
Opinions expressed by DZone contributors are their own.
Comments