Spring Boot) DI Injection Methods (Field/Setter/Constructor)
This post was migrated from Tistory. You can find the original here.
There are three ways to do dependency injection.
Field Injection
1
2
3
4
5
6
@Service
public class TestService2 {
@Autowired
private TestService3 testService3;
...
}
I’ve read that field injection doesn’t catch circular references. Let’s check that.
1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class TestService2 {
@Autowired
private TestService3 testService3;
...
}
@Service
public class TestService3 {
@Autowired
private TestService2 testService2;
...
}
Running the code above gives:
We can see the build catches the error. It looks like older versions of Spring Boot didn’t catch this.
In Spring Boot 3.1.2, we can see that circular references are prevented even with field injection.
Setter Injection
1
2
3
4
5
6
7
8
9
10
@Service
public class TestService2 {
private TestService3 testService3;
@Autowired
public void setTestService3(TestService3 testService3){
this.testService3 = testService3;
}
...
}
Constructor Injection
1
2
3
4
5
6
7
8
9
@Service
public class TestService2 {
private final TestService3 testService3;
public TestService2(TestService3 testService3){
this.testService3 = testService3;
}
...
}
If there’s only one constructor, you don’t need to explicitly declare @Autowired. Let’s verify this.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class TestService2 {
private Object testService;
public TestService2(TestService3 testService3){
this.testService = testService3;
}
public TestService2(TestService1 testService1){
this.testService = testService1;
}
public void sayClass(){
System.out.println(testService.getClass());
}
}
If you write it as above and call testService2.sayClass(), you get the following error:
NullPointerException: Cannot invoke “Object.getClass()” because “this.testService” is null
When there are two or more constructors and none is annotated with @Autowired, no dependency gets injected at all and the field ends up null.
If you annotate one of the two constructors with @Autowired, the dependency for that constructor gets injected, and getClass prints correctly.
Constructor Injection with Lombok, When There’s Only One Constructor
1
2
3
4
5
6
@Service
@RequiredArgsConstructor
public class TestService2 {
private final TestService3 testService3;
...
}
@RequiredArgsConstructor automatically generates a constructor that takes the required (final) fields as arguments.
1
2
3
4
5
6
7
8
@Service
public class TestService2 {
private final TestService3 testService3;
public TestService2(final TestService3 testService3) {
this.testService3 = testService3;
}
}
The generated code looks like the above.
1
2
3
4
5
6
7
@Service
@RequiredArgsConstructor
public class TestService2 {
@NonNull
private TestService3 testService3;
...
}
Using the @NonNull annotation seems like a better approach.
1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class TestService2 {
private @NonNull TestService3 testService3;
public TestService2(final @NonNull TestService3 testService3) {
if (testService3 == null) {
throw new NullPointerException("testService3 is marked non-null but is null");
} else {
this.testService3 = testService3;
}
}
}
The generated code looks like the above. Even though the field is final, null could still be passed in as an argument, and this adds exception handling for that case.
For circular dependencies, all three methods throw an error before the application even runs.
Basically, if you want to maintain immutability, use constructor injection, and if you need mutability, field injection works fine.
For constructor injection, as long as there’s only one constructor, using Lombok seems more convenient and safer.
