Stop using field injection in SpringBoot!
This article is a translation, and I have added some of my own understanding. Translation source: https://medium.com
In the context of Spring Boot dependency injection, there is a debate about best practices for injecting dependencies: field injection, setter injection, and constructor injection.
?In this article, we will focus on the pitfalls of field injection and make a case for staying away from it. ?
1 What is field injection?
Field injection involves annotating private fields of a class directly with @Autowired. Here is an example:
@Component public class OrderService {<!-- --> @Autowired private OrderRepository orderRepository; public Order findOrderById(Long id) {<!-- --> return orderRepository.findById(id); } }
2 Why you should stop using field injection
2.1 Testability
Field injection complicates unit testing of components. Since dependencies are injected directly into fields, we cannot easily provide mocks or alternative implementations outside of the Spring context.
Let’s take the sameOrderService class as an example.
If we wish to unit test the OrderService, we will have difficulty mocking the OrderRepository since it is a private field. Here’s how to unit test OrderService:
@RunWith(SpringJUnit4ClassRunner.class) public class OrderServiceTest {<!-- --> private OrderService orderService; @Mock private OrderRepository orderRepository; @Before public void setUp() throws Exception {<!-- --> orderService = new OrderService(); // This will set the mock orderRepository into orderService's private field ReflectionTestUtils.setField(orderService, "orderRepository", orderRepository); } ... }
Although it can be implemented, using reflection to replace private fields is not a good design. It violates object-oriented design principles and makes tests difficult to read and maintain.
However, if we use constructor injection:
@Component public class OrderService {<!-- --> private final OrderRepository orderRepository; public OrderService(OrderRepository orderRepository) {<!-- --> this.orderRepository = orderRepository; } }
We can easily provide a mock OrderRepository during testing:
OrderRepository mockRepo = mock(OrderRepository.class); OrderService orderService = new OrderService(mockRepo);
2.2 Immutability
Field injection makes our beans mutable after construction. With constructor injection, once an object is constructed, its dependencies remain unchanged.
for example:
Field injection class:
@Component public class UserService {<!-- --> @Autowired private UserRepository userRepository; }
Here, userRepository can reassign the reference after the object is created, which breaks the immutability principle.
If we use constructor injection:
@Component public class UserService {<!-- --> private final UserRepository userRepository; public UserService(UserRepository userRepository) {<!-- --> this.userRepository = userRepository; } }
The userRepository field can be declared as final and will remain unchanged after construction is complete.
2.2.1 @Autowired private UserRepository userRepository; Can’t you add a final after private?
In the Spring framework, when using field injection, you usually do not add the final
keyword after the field annotated with @Autowired
. The reason is that when Spring creates an instance of the bean, it needs to be able to set these dependencies that are not provided through the constructor. If a field is declared final
, then it must be initialized when the object is constructed and cannot be changed thereafter.
Field injection is usually done through reflection, which allows fields to be modified by external classes even if they are declared private
. However, if you use final
to modify a field, reflection cannot be used to change its value, because final
fields are not mutable after the object is constructed. This is why you see final
fields in constructor injection but not in field injection.
Constructor injection provides immutability because you can declare all dependencies as final
. This means that once the object is constructed, these dependencies cannot be changed, thus avoiding many problems caused by state changes. This also complies with the principle of immutability and is the currently recommended injection method because it improves the security and robustness of the code.
To summarize, fields annotated with @Autowired
cannot be declared as final
because Spring needs to set these fields after the object is constructed. If you want immutability, you should use constructor injection and declare dependencies as final
.
2.2.2 If I use the constructor to inject a bean, how do I use it?
When you use constructor injection to inject a bean, the Spring container will be responsible for creating an instance of the bean and automatically injecting the dependencies required by the constructor. Here are the basic steps for using constructor injection:
- Define dependency interfaces.
- Create the dependency’s implementation class and mark it as a Spring-managed bean using, for example, the
@Component
annotation. - In the class that uses the dependency, create a constructor that accepts the dependency as a parameter and annotates it with e.g.
@Autowired
(after Spring 4.3, this can be omitted if the class has only one constructor@Autowired
annotation).
Here’s a simple example:
// Dependency interface public interface UserRepository {<!-- --> // Define the required operations, such as finding users, etc. } // Implementation class of dependencies @Component public class UserRepositoryImpl implements UserRepository {<!-- --> // Implement the methods defined in the UserRepository interface } // Classes using dependencies @Component public class UserService {<!-- --> private final UserRepository userRepository; // Constructor injection public UserService(UserRepository userRepository) {<!-- --> this.userRepository = userRepository; } // Other methods in the class can use userRepository public void performAction() {<!-- --> // Use userRepository to perform some operations } }
In this example, UserService
requires an instance of UserRepository
. Spring will automatically find a bean that matches the UserRepository
type (in this case, an instance of UserRepositoryImpl
), and then create an instance of UserService
and add UserRepositoryImpl
is passed as a parameter to the constructor of UserService
.
In a Spring application, you do not need to create an instance of UserService
yourself. The Spring container handles this automatically. When you need to use UserService
, you can let Spring automatically inject it, for example:
@RestController public class UserController {<!-- --> private final UserService userService; //Inject UserService through the constructor in the controller public UserController(UserService userService) {<!-- --> this.userService = userService; } @GetMapping("/users") public ResponseEntity<List<User>> getUsers() {<!-- --> //Use userService to handle the request to obtain the user } }
In the above controller, UserService
will be automatically injected into UserController
. In this way, you can use the methods provided by UserService
in the controller to handle the request.
2.3 Tighter coupling with Spring
Field injection couples our classes more tightly to Spring because it uses Spring-specific annotations ( @Autowired ) directly on our fields. This may cause problems in the following scenarios:
“Scenario without Spring”: Suppose we are building a lightweight command line application that does not use Spring, but we still want to leverage the logic of UserService. In this case, the @Autowired annotation has no meaning and cannot be used to inject dependencies. We would have to refactor the class or implement cumbersome workarounds to reuse UserService.
“Switch to another DI framework”: If we decide to switch to another dependency injection framework, such as Google Guice, the Spring-specific framework @Autowired will become a roadblock. Then we would have to refactor every place that uses Spring specific annotations, which would be very tedious.
“Readability and understandability”: For developers who are not familiar with Spring, encountering the @Autowired annotation may be confusing. They may want to know how to resolve dependencies, thereby increasing learning costs (ps: although there may be very few Java programmers who are not familiar with Spring development).
2.4 Null pointer exception
When a class utilizes field injection and is instantiated through its default constructor, dependent fields remain uninitialized.
For example:
@Component public class PaymentGateway {<!-- --> @Autowired private PaymentQueue paymentQueue; public void initiate (PaymentRequest request){<!-- --> paymentQueue.add(request); ... } } public class PaymentService {<!-- --> public void process (PaymentRequest request) {<!-- --> PaymentGateway gateway = new PaymentGateway(); gateway.initiate(request); } }
From the above code, it is easy to see that if PaymentGateway is accessed in this state at runtime, a NullPointerException will occur. The only way to manually initialize these fields outside of the Spring context is to use reflection. The syntax of the reflection mechanism is cumbersome and error-prone, and has certain problems with program readability, so it is not recommended.
2.4.1 Why does new a spring bean directly in a non-spring class report NPE?
In the code example you provided, the PaymentService
class creates an instance of PaymentGateway
directly through the new
keyword, rather than through Spring’s dependency injection. Obtain. When you create an instance directly using the new
keyword, the Spring container will not intervene in the life cycle of the object, which means that Spring will not automatically inject dependencies in PaymentGateway
paymentQueue
.
Since paymentQueue
is not initialized (because Spring did not inject it), when the initiate
method is called, it tries to access paymentQueue
‘s add
Method. Because paymentQueue
is null
at this time, trying to call its method will result in NullPointerException
(NPE).
In Spring applications, to avoid such problems, you should always obtain Bean instances through the Spring container, so that Spring can automatically manage the Bean life cycle and dependency injection. If you need to use PaymentGateway
in a Spring-managed bean, you should let Spring inject it instead of creating an instance yourself.
For example, the corrected PaymentService
might look like this:
@Service public class PaymentService {<!-- --> private final PaymentGateway paymentGateway; @Autowired public PaymentService(PaymentGateway paymentGateway) {<!-- --> this.paymentGateway = paymentGateway; } public void process(PaymentRequest request) {<!-- --> paymentGateway.initiate(request); } }
In this modified version, PaymentGateway
is injected into PaymentService
by Spring through the constructor, thus ensuring the paymentQueue of
Dependencies will be automatically injected by the Spring container, thus avoiding NPE.PaymentGateway
2.5 Circular dependencies
Field injection can mask circular dependency issues, making them harder to detect during development.
For example:
Consider two interdependent services AService and BService:
@Service
public class AService {
@Autowired
private BService bService;
}
@Service
public class BService {
@Autowired
private AService aService;
}
The above may cause unexpected problems in the application.
Using constructor injection, Spring will immediately throw BeanCurrentlyInCreationException during startup, making us aware of circular dependencies. However, to solve the circular dependency problem, you can use @Lazy to lazy load one of the dependencies.
2.5.1 I remember that circular dependencies are allowed in spring, how to solve it
Yes, the Spring framework does support circular dependencies, but this support is limited to field injection (setter injection) and method injection. Spring solves the circular dependency problem under the singleton scope by using three-level cache, making it possible to inject circularly dependent beans in the constructor.
For constructor injection, Spring cannot handle circular dependencies because each bean’s dependencies must be resolved before the constructor is called. If A requires B to be created, and B also requires A to be created, Spring cannot decide which Bean should be created first, so it will throw a BeanCurrentlyInCreationException
exception.
For the example you mentioned, if both services A and B are injected into each other through the constructor, Spring will detect the circular dependency and throw an exception when the application starts. If you use field injection, Spring can solve the problem of circular dependencies by first instantiating a bean and then injecting it when setting the properties.
2.5.2 How constructor injection solves circular dependency problems
If you want to solve the circular dependency problem in constructor injection, you can use the @Lazy
annotation to delay the loading of dependencies. For example:
@Service public class AService {<!-- --> private final BService bService; @Autowired public AService(@Lazy BService bService) {<!-- --> this.bService = bService; } } @Service public class BService {<!-- --> private final AService aService; @Autowired public BService(AService aService) {<!-- --> this.aService = aService; } }
In the above code, the @Lazy
annotation ensures that BService
will not be created immediately when AService
is instantiated, but will be created when AService
is first accessed. It is created only when code>BService. In this way, Spring can first complete the creation of AService
, and then create BService
instances when needed, avoiding circular dependency problems.
2.6 @Autowired injection is not recommended, so what about @Resource, can it avoid these problems of @Autowired?
@Resource
is an annotation in Java EE 6 that can be used to inject dependencies, and its behavior is slightly different from Spring’s @Autowired
. Here are some differences between the two annotations:
-
source:
@Autowired
is a Spring-specific annotation.@Resource
comes from Java’sjavax.annotation
package.
-
Injection method:
@Autowired
autowires by type by default. When assembly by name is required, it can be used in combination with the@Qualifier
annotation.@Resource
is injected by name by default. If no bean matching the name is found, it will be injected by type. It has two important attributes:name
andtype
.
-
compatibility:
@Autowired
is tightly integrated with Spring and supports Spring-specific functions, such as@Qualifier
,@Primary
, etc.@Resource
is a standard Java annotation, so it does not depend on Spring and can be used in any Java EE-compatible container.
Regarding whether @Autowired
can be avoided:
-
Circular dependency:
@Resource
does not solve the problem of circular dependency during constructor injection. This is because the problem of circular dependency is related to the injection mechanism itself, rather than specific to a certain annotation. When the Spring container needs to create a bean instance, it must resolve all required dependencies, whether these dependencies are injected through@Autowired
or@Resource
. -
Unclear dependencies:
@Resource
is injected by name by default, which allows it to specify which bean it depends on by name when there are multiple beans of the same type, so that in a certain Improves the clarity of injection to a certain extent. -
Autowiring flexibility: When using
@Resource
, you lose some of the flexibility provided by Spring, such as using the@Primary
annotation to specify a preference bean.
In general, the @Resource
annotation provides another way of dependency injection, but it does not solve all the problems that @Autowired
may cause. In the Spring framework, it is recommended to use constructor injection first (whether through @Autowired
or through parameter parsing), because it can help you avoid most of the problems mentioned above, and can also improve the quality of your code. Testability.
2.7 Which type of injection is used most among major Internet companies?
Among major Internet companies, Constructor Injection is usually the preferred dependency injection method for the following reasons:
-
Immutability and safety: Constructor injection allows dependencies to be
final
, which means that once an object is constructed, its dependencies cannot be changed. This immutability reduces problems in multi-threaded environments and ensures that dependencies are fully initialized before use. -
Testability: Using constructor injection, unit testing can be done more easily without starting the entire Spring container.
-
Explicit dependencies: Constructor parameters force dependencies to be provided when creating the object, which makes dependencies more explicit and avoids the possibility of
null
references. -
Framework independence: Constructor injection does not depend on Spring or any other dependency injection framework, which makes the code easier to migrate and refactor.
Although constructor injection is a better choice in many cases, in actual development, the decision to use which injection method will be based on specific scenarios and needs. For example, when there are multiple constructor parameters and some of these parameters are optional, or in some complex dependency scenarios, developers may choose Field Injection or Setter Injection. ).
Both the @Autowired
and @Resource
annotations are used to autowire Spring beans, but since @Autowired
provides tighter integration with Spring and more flexibility, it is often the more preferred choice. However, which one to use actually depends on the specific needs of the project, team habits, and coding standards. In some projects that follow strict Java EE standards, you may prefer to use @Resource
.
Ultimately, regardless of the injection method, it’s important to maintain consistency, clarity, and maintainability. The code specifications of major manufacturers will tend to promote these principles and ensure the implementation of best practices through code reviews, documentation, and team training.
3 Conclusion
While field injection may seem cleaner, its drawbacks far outweigh its simplicity. Constructor injection provides clear advantages in terms of testability, immutability, and overall robustness of the application.
It is very consistent with SOLID principles, ensuring that our Spring Boot applications are maintainable and less error-prone.
Therefore, it is recommended that everyone stop using field injection in Spring Boot!