This content originally appeared on DEV Community and was authored by Felix Coutinho
Or "Why you shouldn't use Field Injection when using Spring".
*TL;DR *
Injecting beans directly into fields using @Autowired makes your dependencies "hidden" and encourages bad design. Use constructor-based injection instead.
It is very likely that you have already seen an @Autowired annotation when coding in a Java/Spring application. However, what most of my friends developers don't know is that auto-wiring is one of the most commonly seen anti-patterns in codebases that use the Spring IoC/DI Framework.
Just to refresh your memory here is a short example of what I'm talking about.
@Service
public class UserService {
@Autowired // Field Injection
private UserRepository userRepository;
// ...
}
It may seem like a simple and pragmatic way to do Dependency Injection, right? But the consequences of this can turn your codebase slowly (or fast) into spaghetti.
But why do developers keep choosing this approach? Possibly because it's easy to use and provides an out-of-the-box experience when a developer needs to inject a dependency into a class. However, similar to any other bad practice, this is very easy to replicate across the rest of the codebase, and usually people will never argue why that code design choice was made. And what's the result of using @Autowired? Your code will suffer from coupling, a bad test experience, hidden dependencies, and others.
In my perspective, the absence of comprehension of the underlying mechanics of a system can potentially result in disastrous codebases. This is applicable to the usage of '@Autowired' when using Spring. Without a thorough grasp of crucial concepts such as Inversion of Control and Dependency Injection, developers are more likely to make errors and become ensnared in this pitfall.
To gain a better understanding of code design best practices, it's important for developers and architects to review key concepts and explore different approaches to Dependency Injection with Spring. By doing so, we can evaluate which method is optimal for our needs, and potentially uncover the drawbacks of using @Autowired as a dependency injection strategy.
Dependency Injection with Spring
Dependency Injection is a crucial pattern in software development and is present in production-ready frameworks like Spring. It promotes loose coupling between classes, improves testability, and facilitates modularity.
There are basically three ways to do Dependency Injection using the Spring Framework:
- Field Injection uses reflection to set the values of private attributes/fields.
- Setter Injection uses public setters to set the value of the attributes/fields.
- Constructor Injection happens at the time of creating the object itself.
Field Injection
Field injection can be achieved using Spring by adding the @Autowired annotation to a class field. And what does @Autowired do? @Autowired is an annotation provided by Spring that allows the automatic wiring of the dependency. When applied to a field, method, or constructor, Spring will try to find a suitable dependency of the required type and inject it into the target class.
@Autowired alone is not the root of all evil. The key aspect is WHERE you are using @Autowired. Most of the time, developers use it at the field/attribute level. Using it at the setter method can reduce the damage, but not eliminate all of it at all.
But why @Autowired is so bad? Basically, @Autowired violates some good code design principles.
Dependency Inversion (D from SOLID)
If you want to use your class outside the application container, for example for unit testing, you are forced to use a Spring container to instantiate your class as there is no other possible way (but reflection) to set the @Autowired fields.
When using field-based dependency injection with @Autowired, the class is inherently hiding these dependencies from the outside world. In other words, no point of injection exists.
Below you can see an example of this scenario.
@Component
public class Calculator {
@Autowired
private Multiplier multiplier;
public int add(int a, int b) {
return a + b;
}
public int multiply(int a, int b) {
return multiplier.multiply(a, b);
}
}
@Component
public class Multiplier {
public int multiply(int a, int b) {
return a * b;
}
}
Despite the fact of in runtime Spring will inject the dependency (an instance of Multiplier) there is no way to inject the dependency manually, for instance when unit testing this class.
The below unit test class will compile, but at runtime it's a good candidate to throw a NullPointerException, why? Because the dependency between Calculator and Multiplier was not satisfied.
public class CalculatorTest {
@Test
public void testAdd() {
Calculator calculator = new Calculator();
int result = calculator.add(2, 3);
assertEquals(5, result);
}
@Test
public void testMultiply() {
Calculator calculator = new Calculator();
int result = calculator.multiply(2, 3);
assertEquals(6, result);
}
}
Clean Code / Screaming Architecture / Single Responsibility Principle (S from SOLID)
Code must scream its design. And the SRP principle states that a class should have only one reason to change. This means that a class should have only one responsibility or concern. @Autowired annotation, in itself, does not violate this principle.
However, if a class has multiple responsibilities and dependencies injected through the @Autowired annotation, it could be a clear sign of a violation of the SRP. In this case, it might be better to refactor the class and extract the responsibilities into separate classes.
Otherwise, if constructor-based dependency injection is used instead, as more dependencies are added to your class, the constructor grows bigger and bigger and the code starts to smell, sending clear signals that something is wrong. This is described in the Clean Code / Screaming Architecture pattern in the book written by Robert C. Martin aka Uncle Bob.
Complexity
@Autowired can make the code more complex, especially when dealing with circular dependencies. When two or more classes depend on each other, it becomes hard to determine the order in which they should be instantiated. This can lead to runtime errors that are hard to debug.
Most likely your project doesn't need @Autowired at all
What should you use instead?
The short answer is Constructor Injection.
In OOP we create an object by calling its constructor. If the constructor expects all required dependencies as parameters, then we can be 100% sure that the class will never be instantiated without having its dependencies injected.
Below an example using Constructor-based Dependency Injection instead of @Autowired.
@Component
public class Calculator {
private Multiplier multiplier;
// Default constructor. It will be used by Spring to
// inject the dependencies declared here.
public Calculator(Multiplier multiplier) {
this.multiplier = multiplier;
}
public int add(int a, int b) {
return a + b;
}
public int multiply(int a, int b) {
return multiplier.multiply(a, b);
}
}
@Component
public class Multiplier {
public int multiply(int a, int b) {
return a * b;
}
}
public class CalculatorTest {
@Test
public void testAdd() {
Calculator calculator = new Calculator(mock(Multiplier.class));
int result = calculator.add(2, 3);
assertEquals(5, result);
}
@Test
public void testMultiply() {
Multiplier mockMultiplier = mock(Multiplier.class);
when(mockMultiplier.multiply(2, 3)).thenReturn(6);
Calculator calculator = new Calculator(mockMultiplier);
int result = calculator.multiply(2, 3);
assertEquals(6, result);
}
}
Now the dependency between the two components was explicitly declared and the code design allows a mock instance of Multiplier to be injected at runtime.
Also, Constructor injection helps in creating immutable objects simply because a constructor is the only possible way to create objects. Once we create a bean, we cannot alter its dependencies anymore. On the other hand, by using setter injection, it’s possible to inject the dependency after creation or change the dependency, 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.
By explicitly defining the dependencies of the class on the constructor, you can make the code more maintainable, testable, and flexible.
- Maintainable because you rely only on OOP concepts to inject your class dependencies and any change to the class dependencies contract will be communicated to the other class with no effort.
- Testable because you have a point of injection that will allow the unit tests to pass, for instance, a mock object instead of a concrete implementation. Besides, we make this class immutable by allowing only injections at the constructing time.
- Flexible because you still able to add more dependencies to the class with minimum effort by adding more parameters to the default constructor.
This approach makes it easier to understand and track the dependencies of your classes. Additionally, it makes it easier to test the class in isolation by forcing your to explicitly providing mock objects for the required dependencies at coding time.
Setter Injection
One more method to do Dependency Injection using Spring is by using setter methods to inject a instance of the dependency.
Below you can see an example of how Setter Injection looks like when using Spring.
public class UserService {
private UserRepository userRepository;
...
@Autowired // Setter Injection
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
...
}
This approach can lead to problems such as:
- Optional dependencies. Setter Injection allows for optional dependencies, which can lead to null pointer exceptions if the dependencies are not properly checked for null values.
- Incomplete object state. If an object is partially constructed and a setter is called, it can result in the object being in an incomplete state, which can lead to unexpected behavior or null pointer exceptions again.
- Hidden dependencies. As the dependency is not explicitly declared in the constructor, making it harder to understand the code and its dependencies.
Despite the drawbacks, Setter Injection sits somewhere between the Field and the Constructor Injection because at least we have a public method that allows developers to inject mocks instead of real implementation making the class easier to test.
Further Considerations
The @Autowired annotation can be omitted from codebases using Spring Framework 4.3 or higher and Spring will use the default constructor and inject all necessary dependencies for you when the class is being managed by the Application Context.. (https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-constructor-vs-setter-injection)
So, since the Spring team decided @Autowired should be optional to the default constructor. We can conclude since it's not helping the Spring framework to make a decision, its presence is just noise. Get rid of it!
The below comment from the Spring Framework development team gives one more reason to choose Constructor Injection as your default injection method.
"Furthermore, constructor-injected components are always returned to the client (calling) code in a fully initialized state." and "Setter injection should primarily only be used for optional dependencies that can be assigned reasonable default values within the class."
Basically, what they are saying is by using constructor injection you can guarantee the ready-to-use state of the class. If you are using @Autowired or even setter injection your class instance can be instantiated by Spring in a bad state which can cause unexpected behaviour, such as NullPointerException.
Why @Autowired even exist?
If you need to inject optional or changeable dependencies, you may want to use @Autowired to mark the setter method (or a non-default constructor) as the point of injection. But it only makes sense if your dependency is optional or changeable and your implementation can handle the absence of that dependency (nulls).
If several constructors are available and there is no primary/default constructor, at least one of the constructors must be annotated with @Autowired to instruct Spring which one to use. This is a scenario where @Autowired can be considered.
Conclusion
At the end of the day, @Autowired can be useful for small, simple projects, but it is not a good choice for large, complex, and production-ready projects where the code design should be open for extension, flexible, and easy to test. Instead, the recommendation is to use explicit Dependency Injection via class constructor. By doing so, you can ensure that your code remains robust and adaptable.
Something that you and your team will gain for free is that the rest of the project will implicitly reflect this code design decision. Sometimes, developers never learn about proper Dependency Injection, class contracts, or even how to test code effectively simply because they use @Autowired and never question its real necessity and applicability.
Additional reading and references
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-constructor-vs-setter-injection
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-autowired-annotation
https://betulsahinn.medium.com/why-is-autowired-annotation-not-recommended-4939c46da1f8
https://blog.marcnuri.com/field-injection-is-not-recommended
https://reflectoring.io/constructor-injection/
https://kinbiko.com/posts/2018-02-13-spring-dependency-injection-patterns/
https://blog.frankel.ch/my-case-against-autowiring/
https://www.linkedin.com/pulse/when-autowire-spring-boot-omar-ismail/?trk=articles_directory
https://eng.zemosolabs.com/when-not-to-autowire-in-spring-spring-boot-93e6a01cb793
https://stackoverflow.com/questions/41092751/spring-injects-dependencies-in-constructor-without-autowired-annotation
This content originally appeared on DEV Community and was authored by Felix Coutinho
Felix Coutinho | Sciencx (2023-04-26T17:18:14+00:00) You should stop using Spring @Autowired. Retrieved from https://www.scien.cx/2023/04/26/you-should-stop-using-spring-autowired/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.