Why you should stop using field annotations in SpringBoot and use constructor injection instead

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:

  1. Define dependency interfaces.
  2. Create the dependency’s implementation class and mark it as a Spring-managed bean using, for example, the @Component annotation.
  3. 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 PaymentGateway Dependencies will be automatically injected by the Spring container, thus avoiding NPE.

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:

  1. source:

    • @Autowired is a Spring-specific annotation.
    • @Resource comes from Java’s javax.annotation package.
  2. 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 and type.
  3. 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:

  1. 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.

  2. Testability: Using constructor injection, unit testing can be done more easily without starting the entire Spring container.

  3. 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.

  4. 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!