Dependency Injection (2)

There are three ways to implement DI with @Component annotation: constructor-based injection, setter-based injection, and field-based injection. This article is talking about the different implementations.

Constructor Injection

Before Spring 4.3, we had to add an @Autowired annotation to the constructor. With newer versions, this is optional if the class has only one constructor.

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
@Component
class Cake {

private Flavor flavor;

Cake(Flavor flavor) {
Objects.requireNonNull(flavor);
this.flavor = flavor;
}

Flavor getFlavor() {
return flavor;
}
...
}

// flavor
@Component
public class Flavor {
String flavorType;
String color;

public String getFlavorType() { return flavorType; }
public String getColor() { return color; }

@Override
public String toString() {
return "Flavor [flavorType=" + flavorType + ", color=" + color + "]";
}
}

// test
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = ExampleApplicationCI.class)
public class TestCakeClassConstructorInjection {

@Autowired
private Cake cake;

@Test
public void testConstructorInjection() {
System.out.println(cake.toString());
Assert.assertNotNull(cake.getFlavor());
}
}

When we have a class with multiple constructors, we need to explicitly add the @Autowired annotation to any one of the constructors so that Spring knows which constructor to use to inject the dependencies.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
class Sandwich {

private Topping toppings;
private Bread breadType;

Sandwich(Topping toppings) {
this.toppings = toppings;
}

@Autowired
Sandwich(Topping toppings, Bread breadType) {
this.toppings = toppings;
this.breadType = breadType;
}
...
}

Setter Injection

Spring will find the @Autowired annotation and call the setter to inject the dependency.

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
@Component
class Cookie {

private Topping toppings;

@Autowired
void setTopping(Topping toppings) {
this.toppings = toppings;
}

Topping getTopping() {
return toppings;
}
...
}

// test
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = ExampleApplicationSI.class)
public class TestCakeClassSetterInjection {

@Autowired
private Cookie cookie;

@Test
public void testSetterInjection() {
Assert.assertNotNull(cake.getTopping());
}
}

Field Injection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
class IceCream {

@Autowired
private Topping toppings;

Topping getToppings() {
return toppings;
}

void setToppings(Topping toppings) {
this.toppings = toppings;
}
}

What will happen if we add @Autowired to both, a field and a setter? In this case, Spring injects dependency using the setter injection method. This is bad practice since it makes the code less readable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
class Pizza {

@Autowired
private Topping toppings;

Topping getToppings() {
return toppings;
}

@Autowired
void setToppings(Topping toppings) {
this.toppings = toppings;
}
}

Why constructor Injection

Identifying Code Smells

Constructor injection helps us to identify if our bean is dependent on too many other objects. If our constructor has a large number of arguments this may be a sign that our class has too many responsibilities. We may want to think about refactoring our code to better address proper separation of concerns.

Preventing Errors in Tests

Constructor injection simplifies writing unit tests. The constructor forces us to provide valid objects for all dependencies. Using mocking libraries like Mockito, we can create mock objects that we can then pass into the constructor.

We can also pass mocks via setters, of course, but if we add a new dependency to a class, we may forget to call the setter in the test, potentially causing a NullPointerException in the test.

Immutability

Constructor injection helps in creating immutable objects because a constructor’s signature is the only possible way to create objects. Once we create a bean, we cannot alter its dependencies anymore. With setter injection, it’s possible to inject the dependency after creation, thus leading to mutable objects which, among other things, may not be thread-safe in a multi-threaded environment and are harder to debug due to their mutability.

Reference: Why You Should Use Constructor Injection in Spring

Thank you for reading!