Why not to use Field Injection in Spring Boot
2023-12-12 15:17:28 Author: blogs.sap.com(查看原文) 阅读量:3 收藏

What is Field Injection?

Field injection involves directly annotating private fields of a class with @Autowired. Here’s an example:

@Component
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;
    
    public Order findOrderById(Long id) {
        return orderRepository.findById(id);
    }
}

Why You Should Stop Using Field Injection

1. Testability

Field injection complicates the unit testing of your components. Since dependencies are directly injected into the fields, you cannot easily provide mocks or alternative implementations outside of the Spring context.

Lets take the same example of the sameOrderService class.

If you wish to unit test the OrderService, you’d face difficulty in mocking the OrderRepository because it’s a private field. Here’s a way 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);
    }

    ...
}

Though possible, using Reflection to replace the private fields is not a great design. It contradicts object-oriented design principles and makes tests difficult to read and maintain.

On the other hand, with constructor injection:

@Component
public class OrderService {
    private final OrderRepository orderRepository;

    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }
}

You can easily provide a mock OrderRepository during testing:

OrderRepository mockRepo = mock(OrderRepository.class);
OrderService orderService = new OrderService(mockRepo);

2. Immutability

Field injection makes your beans mutable post-construction. With constructor injection, once an object is constructed, its dependencies remain unchanged.

Example:

The field-injected class:

@Component
public class UserService {
    @Autowired
    private UserRepository userRepository;
}

Here, the userRepository reference can be re-assigned after the object has been created, breaking 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 is can be declared final and made immutable post-construction.

3. Tighter coupling with Spring:

Field injection makes your classes more tightly coupled to Spring, as it uses Spring-specific annotations (@Autowired) directly on your fields. This can introduce problems in below scenarios:

  1. Using Outside Spring: Let’s say you’re building a lightweight command-line application that doesn’t use Spring, but you still want to utilize the UserService logic. In this context, the @Autowired annotation means nothing and can’t be used to inject dependencies. You’d have to either refactor the class or implement cumbersome workarounds to reuse UserService.
  2. Switching to Another DI Framework: If you decide to switch to a different Dependency Injection framework, like Google Guice, the Spring-specific @Autowired becomes an obstacle. You’d have to refactor every place where you’ve used Spring-specific annotations, which can be a tedious process.
  3. Readability and Understanding: For a developer not familiar with Spring, encountering the @Autowired annotation might be confusing. They might wonder how dependencies are resolved, leading to a steeper learning curve.

4. NullPointerException

When a class utilizes field injection and is instantiated via its default constructor, the dependent fields remain uninitialized.

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);
    }   
}

Thus, if PaymentGateway is accessed at runtime in this state, a NullPointerException will occur. The only way to manually initialize these fields outside of the Spring context would be to use reflection, which is not advisable for various reasons.

5. Circular Dependencies

Field injection might mask circular dependencies problems, making them harder to catch during development.

Example:

Consider two services, AService and BService, that depend on each other:

@Service
public class AService {
    @Autowired
    private BService bService;
}

@Service
public class BService {
    @Autowired
    private AService aService;
}

The above can lead to unexpected behavior and problems in the application.

Using constructor injection, Spring would immediately throw a BeanCurrentlyInCreationException during startup, making you aware of the circular dependency. To solve the circular dependency problem, however, you can lazily load one of the dependencies using @Lazy.

Conclusion

While field injection might seem more concise, its disadvantages far outweigh its brevity. Constructor injection provides clear advantages in terms of testability, immutability, and overall robustness of your application. It aligns well with the SOLID principles, ensuring your Spring Boot applications are maintainable and less error-prone.


文章来源: https://blogs.sap.com/2023/12/12/why-not-to-use-field-injection-in-spring-boot/
如有侵权请联系:admin#unsafe.sh