Welcome to the second post in our Spring Boot Technical Concepts series! Today we’ll dive deep into Dependency Injection with Spring Boot. If you want to start at the beginning, be sure to check out Default Starters – Spring Boot Technical Concepts Series, Part 1. But now, let’s talk Dependency Injection!
There was a great convergence of ideas and technology right around 1995. That was when when Bob Martin (fondly called Uncle Bob) first started talking about the “commandments” of Object Oriented Design. It’s also when Java – a then new Object Oriented programming language – was first released.
By the year 2000, Bob Martin had firmly codified his Design Principles and Design Patterns for Object Oriented Development. Somewhere in the early 2000s, Michael Feathers coined the mnemonic SOLID to describe the first 5 principles of OOD – in particular, class design. Each letter also has a 3-letter representation.
S | SRP | Single Responsibility Principle |
O | OCP | Open Closed Principle |
L | LSP | Liskov Substitution Principle |
I | ISP | Interface Segregation Principle |
D | DIP | Dependency Inversion Principle |
In this post, we are focusing on the “D” – Dependency Inversion Principle. Uncle Bob tells us: “Depend on abstractions, not on concretions.” This is sometimes referred to as Inversion of Control. In 2004, Martin Fowler referred to it as Dependency Injection.
What’s Dependency Injection?
Let’s take a look at a definition of Inversion of Control. Uncle Bob Martin says there are three aspects of a bad design:
- Rigidity – hard to change because of the impact on every other part of the system.
- Fragility – changes break other parts of the system.
- Immobility – reuse elsewhere is hard because of entanglement between components of the system
Inversion of Control addresses these three issues using Uncle Bob’s simple approach: Depend on abstractions, not on concretions. In the world of Java, that means relying on interfaces and not implementations.
The World Before Dependency Injection
Let’s take a look at a counter example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
// DON'T DO THIS public class BadExample { public static void main(String[] args) { new ComputerProcessor() .addComputer(new Doubler()) .addComputer(new Squarer()) .computeAll(8); } static class ComputerProcessor { private List computers = new ArrayList(); public ComputerProcessor addComputer(Object o) { computers.add(o); return ComputerProcessor.this; } public void computeAll(long value) { for (Object o : computers) { long computedValue = -1; if (o instanceof Doubler) { computedValue = ((Doubler) o).computeDouble(value); } else if (o instanceof Squarer) { computedValue = ((Squarer) o).computeSquare(value); } String name = o.getClass().getSimpleName(); System.out.println("Computer: " + name + ", value: " + value + " computed value: " + computedValue); } } } static class Doubler { public long computeDouble(long value) { return value*2; } } static class Squarer { public long computeSquare(long value) { return value*value; } } } |
The ComputerProcessor
is very fragile in this example. If we add a new class called Cuber
, and we want to be able to represent it in ComputerProcessor
, we need another if statement and more casting in order to call its computeCube
method.
Here’s the same example using Inversion of Control:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
public class IoCExample { public static void main(String[] args) { new ComputerProcessor() .addComputer(new Doubler()) .addComputer(new Squarer()) .computeAll(8); } static class ComputerProcessor { private List<Computer> computers = new ArrayList<>(); public ComputerProcessor addComputer(Computer c) { computers.add(c); return ComputerProcessor.this; } public void computeAll(long value) { for (Computer c : computers) { String name = c.getClass().getSimpleName(); System.out.println("Computer: " + name + ", value: " + value + " computed value: " + c.compute(value)); } } } interface Computer { long compute(long value); } static class Doubler implements Computer { public long compute(long value) { return value*2; } } static class Squarer implements Computer { public long compute(long value) { return value*value; } } } |
The enabling technology here is the Computer
interface. Now, ComputerProcessor
only deals with objects that conform to the Computer
interface. If we want to add a new Cuber
class, all it has to do is implement the Computer
interface. Nothing need change in ComputerProcessor
to deal with Cuber
objects.
Dependency Injection takes this concept and systematizes it in a way that makes it even easier to interact with your interfaces. Throughout this post, I will be using a Spring Boot example to demonstrate Dependency Injection. Since 2003, Dependency Injection has been a core feature of the Spring framework. The source code can be found here.
Dependency Injection, Spring Style
Let’s jump into a simple example and then break it down:
1 2 3 4 5 6 7 8 9 10 11 12 |
@RestController public class HomeController { @Autowired GreetingService greetingService; @RequestMapping("/") public String home() { return greetingService.greet(); } } |
Spring introduced the @Autowired
annotation for dependency injection. Any of the Spring components can be autowired. These include, components, configurations, services and beans. We’ll look at a few of these in detail below.
It’s a common pattern for controllers to be responsible for managing requests and responses while services perform business logic. Let’s look at our GreetingService
:
1 2 3 4 5 |
public interface GreetingService { String greet(); } |
Pretty straightforward. Here’s an implementation class:
1 2 3 4 5 6 7 8 9 |
@Service public class EnglishGreetingService implements GreetingService { @Override public String greet() { return "Hello World!"; } } |
The @Service
annotation makes it autowireable. Spring injects the dependency into our controller. If I set a breakpoint, I can see the implementation class backing greetingService
:
Together, this setup adheres to the Dependency Inversion Principle. If the internals of the implementation class change, we don’t need to touch HomeController
.
Now, let’s say I want to support another GreetingService
implementation. Say, FrenchGreetingService
:
1 2 3 4 5 6 7 8 9 |
@Service public class FrenchGreetingService implements GreetingService { @Override public String greet() { return "Bonjour Monde!"; } } |
If I try to fire up my Spring Boot app now, I will get an error:
1 2 |
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [com.stormpath.example.service.GreetingService] is defined: expected single matching bean but found 2: englishGreetingService,frenchGreetingService |
Spring Boot has no way of knowing which one of my implementation classes I want to inject into the HomeController
. However, it does have a number of powerful annotations to deal with this. Let’s say that I want to inject the language specific version of the greeting service based on a property setting. We can use a Configuration
to tell Spring Boot which Service we want.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@Configuration public class GreetingServiceConfig { @Bean @ConditionalOnProperty(name = "language.name", havingValue = "english", matchIfMissing = true) public GreetingService englishGreetingService() { return new EnglishGreetingService(); } @Bean @ConditionalOnProperty(name = "language.name", havingValue = "french") public GreetingService frenchGreetingService() { return new FrenchGreetingService(); } } |
On line 1, we are telling Spring Boot that this is a @Configuration
. That causes this class to be instantiated and the beans defined within to (potentially) be exposed for use throughout the Spring ecosystem. I’ve also removed the @Service
annotation from the EnglishGreetingService
and FrenchGreetingService
classes as it’s now this @Configuration
‘s responsibility to instantiate and expose the services as beans.
The @Bean
tells Spring Boot to expose a GreetingService
(this is how it is able to be autowired into our HomeController
). But, we are defining both beans with the same return type. That doesn’t seem right. The @ConditionalOnProperty
annotation ensures that only one of these beans will be injected. It’s looking for the language.name
property. Notice that there’s a matchIfMissing
parameter in the case of the EnglishGreetingService
. By default, that property is false. By setting it to true here, we are indicating to Spring Boot that if it doesn’t find a language.name
property, that EnglishGreetingService
is the default.
Let’s look at this in action. One thing to note is that Spring Boot automatically converts environment variables that are in all-caps with underscores to lowercase, dotted properties. Also, the examples below use HTTPie (https://github.com/jkbrzt/httpie), an alternative to curl. Curl will work as well. Try executing the following after doing mvn clean install
from your command line:
English
1 2 3 |
java -jar target/*.jar & http localhost:8080 |
Also English
1 2 3 |
LANGUAGE_NAME=english java -jar target/*.jar & http localhost:8080 |
French
1 2 3 |
LANGUAGE_NAME=french java -jar target/*.jar & http localhost:8080 |
Important Note: Spring has very deep support for internationalization that the above example is not taking advantage of. The example is meant for demonstration purposes only.
Well, we jumped right into the deep end of Dependency Injection (DI) with Spring. Let’s take a step back and look at the different types of DI supported by Spring.
DI Types
There are two basic types of Dependency Injection: constructor and setter. Spring recommends constructor based dependency in most cases. From the Spring docs: “Since you can mix constructor-based and setter-based DI, it is a good rule of thumb to use constructors for mandatory dependencies and setter methods or configuration methods for optional dependencies.”
Constructor DI
Let’s say I have this POJO:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class MeaningOfLife { final int lifeInt; final String lifeString; public MeaningOfLife(int lifeInt, String lifeString) { this.lifeInt = lifeInt; this.lifeString = lifeString; } public int getLifeInt() { return lifeInt; } public String getLifeString() { return lifeString; } } |
Notice that the two properties are final so they cannot be changed after instantiation. I can expose this class as a Spring managed bean by putting the @Component
annotation at the top. If that’s all I did, we’d get an error because Spring would not know how to instantiate a MeaningOfLife
object.
To address this, we can put the @Autowired
annotation before the constructor and give Spring hints as to what the values of the constructor parameters should be. Here’s how the class looks now:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@Component public class MeaningOfLife { final int lifeInt; final String lifeString; @Autowired public MeaningOfLife( @Value("#{ @environment['life.int'] ?: 0 }") int lifeInt, @Value("#{ @environment['life.string'] ?: '0' }") String lifeString ) { this.lifeInt = lifeInt; this.lifeString = lifeString; } public int getLifeInt() { return lifeInt; } public String getLifeString() { return lifeString; } } |
Here, we are using the @Value
annotation to pick up properties from the environment and, importantly, we are providing defaults. This construct: @Value("#{ @environment['life.int'] ?: 0 }") int intLife
can be read as:
1 2 |
Set the value of intLife to 0 unless Spring can load a property named life.int from application.properties or an environment variable named LIFE_INT |
By virtue of it being a component, we can now autowire MeaningOfLife
elsewhere in our app, such as our HomeController
:
1 2 3 4 5 6 7 8 9 10 11 |
... @Autowired MeaningOfLife meaningOfLife; ... @RequestMapping("meaningOfLife") public String meaningOfLife() { return meaningOfLife.getLifeString(); } ... |
If you launch the app like so:
1 2 3 |
mvn clean install LIFE_STRING=42 java -jar target/*.jar & |
and then hit our meaning of life endpoint: http localhost:8080/meaningOfLife
, you will see it returns 42
.
Setter DI
Let’s say I have this interface:
1 2 3 4 5 |
public interface Nameable { public String getName(); } |
And I have a couple of implementation classes that I’ll expose as beans:
1 2 3 4 5 6 7 8 9 |
@Component public class Dog implements Nameable { @Override public String getName() { return "Fluffy"; } } |
and
1 2 3 4 5 6 7 8 9 |
@Component public class Person implements Nameable { @Override public String getName() { return "Micah"; } } |
Let’s look at another component that will inject Person
and Dog
as setters:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
@Component public class NameHelper { private Nameable person; private Nameable dog; public String getName(String type) { switch (type) { case "person": return person.getName(); case "dog": return dog.getName(); default: return "UNKNOWN"; } } @Autowired @Qualifier("person") public void setPerson(Nameable person) { this.person = person; } @Autowired @Qualifier("dog") public void setDog(Nameable dog) { this.dog = dog; } } |
Notice that both of our setters take a Nameable
parameter. In order have Spring call these setters properly when it instantiates the NameHelper
, we have to give it a hint to know which implementation to inject. In this case, we use the @Qualifier
annotation with the bean name we want to inject. There’s a little bit of Spring magic going on here in that a bean’s default name will be it’s class name converted into standard Java variable format. So, it the class was named MyVeryImportantBean
, it’s default name in Spring would be myVeryImportantBean
.
In our HomeController
, I’ve added a new method:
1 2 3 4 5 6 7 8 9 10 11 |
... @Autowired NameHelper nameHelper; ... @RequestMapping("/name") public String name(@RequestParam String type) { return nameHelper.getName(type); } ... |
By default a @RequestParam
is required, so in order to hit this endpoint, you must give it a type
query parameter:
1 2 |
http localhost:8080/name?type=person |
The above will respond with Micah
.
Among the cool features of Spring is that you can autowire an array of bean interface type and Spring will load it up with all of the concrete objects its instantiated of that type. For instance, I’ve added the following to our NameHelper
component:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
... private Nameable[] nameables; ... public String[] getAllNames() { return Arrays.stream(nameables).map((Nameable::getName)).toArray(String[]::new); } ... @Autowired public void setNameables(Nameable[] nameables) { this.nameables = nameables; } ... |
The @Autowired
setter will be called by Spring with an array containing a Person
and a Dog
Nameable
. If we added another class that implements Nameable
, it would automatically be added to this list.
The getAllNames
method uses some of the nice Java 8 streams interface to give us an array of the calls to the getName
method for each of the Nameable
s in the array.
In our HomeController
we can add an endpoint to exercise this:
1 2 3 4 5 6 7 8 9 10 |
... @Autowired NameHelper nameHelper; ... @RequestMapping("/allNames") public String[] name() { return nameHelper.getAllNames(); } ... |
Hitting this endpoint, http localhost:8080/allNames
returns:
1 2 3 4 5 6 7 8 9 10 11 |
HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Date: Wed, 27 Jul 2016 21:36:20 GMT Server: Apache-Coyote/1.1 Transfer-Encoding: chunked [ "Fluffy", "Micah" ] |
Bringing it Home
It’s easy to get a list of all of the beans that are loaded into the Spring application context. These beans can be referenced and injected into other components of your application.
I’ve added a /beans
endpoint to our HomeController
to drive this point home, so to speak.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
... @Autowired ApplicationContext appContext; ... @RequestMapping("/beans") public Map<String, String[]> beans(@RequestParam(required = false) String q) { Map<String, String[]> retMap = new HashMap<>(); String[] retArray = Arrays.stream(appContext.getBeanDefinitionNames()) .filter(beanName -> (q == null || q.length() == 0) || beanName.toLowerCase().contains(q.trim().toLowerCase()) ) .toArray(String[]::new); retMap.put("beans", retArray); return retMap; } ... |
In this case, it takes an options query parameter: q
. If you don’t provide the parameter, it will return all the beans Spring has available.
Try: http localhost:8080/beans
It’s a long list. Let’s look for some of the beans we created in this example:
`http localhost:8080/beans?q=greeting
1 2 3 4 5 6 7 8 9 10 11 12 13 |
HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Date: Wed, 27 Jul 2016 21:42:21 GMT Server: Apache-Coyote/1.1 Transfer-Encoding: chunked { "beans": [ "greetingServiceConfig", "englishGreetingService" ] } |
Notice that there’s no frenchGreetingService
. That’s because of our @ConditionalOnProperty
setting in GreetingServiceConfig
.
We’ve covered a lot of ground in this post. We looked at the history of Dependency Injection, its use in Spring services and components, as well as different approaches including constructor and setter dependency injection.
There’s an extensive section on Dependency Injection in the official Spring documentation here.
If you have any questions, you can tweet me at @afitnerd or send an email to [email protected].