Spring Dependency Injection
Spring Dependency Injection
1. Purpose
The @Autowired annotation tells Spring to resolve and inject the dependency into the
annotated field, constructor, or method.
It works in conjunction with Spring's ApplicationContext to locate and inject beans.
2. Mechanism
Step 3: Injection
Using reflection, Spring:
Identifies the field, method, or constructor annotated with @Autowired.
Makes the annotated field accessible (if private).
Instantiates and injects the required dependency.
Field Injection :
@Autowired
private MyService myService;
Spring uses reflection to access the private field and inject the required bean.
Setter Injection
@Autowired
public void setMyService(MyService myService) {
this.myService = myService;
}
Constructor Injection
@Autowired
public MyComponent(MyService myService) {
this.myService = myService;
}
Constructor injection is preferred for mandatory dependencies and works well with
@RequiredArgsConstructor in Lombok.
The reflection process used by Spring to handle @Autowired involves accessing private fields,
methods, or constructors in classes and injecting dependencies into them. Here’s a detailed
breakdown of how reflection APIs like Field.setAccessible(true) and Constructor.newInstance()
work in the context of Spring’s dependency injection:
1. Reflection Basics
Java’s Reflection API provides tools to inspect and modify classes, methods, and fields at runtime,
even if they are private. The key classes involved are:
java.lang.reflect.Field: Allows access to fields of a class.
java.lang.reflect.Constructor: Allows instantiation of objects via constructors.
java.lang.reflect.Method: Allows invocation of methods.
Spring uses these APIs during dependency injection.
2. Field Injection
When Spring injects a dependency into a private field annotated with @Autowired, it uses the
following steps:
Example:
@Autowired
private MyService myService;
The reflection process used by Spring to handle @Autowired involves accessing private fields,
methods, or constructors in classes and injecting dependencies into them. Here’s a detailed
breakdown of how reflection APIs like Field.setAccessible(true) and Constructor.newInstance()
work in the context of Spring’s dependency injection:
1. Reflection Basics
Java’s Reflection API provides tools to inspect and modify classes, methods, and fields at runtime,
even if they are private. The key classes involved are:
java.lang.reflect.Field: Allows access to fields of a class.
java.lang.reflect.Constructor: Allows instantiation of objects via constructors.
java.lang.reflect.Method: Allows invocation of methods.
Spring uses these APIs during dependency injection.
2. Field Injection
When Spring injects a dependency into a private field annotated with @Autowired, it uses the
following steps:
Example:
@Autowired
private MyService myService;
How Reflection Works:
1. Retrieve the Field:
o Spring uses Class.getDeclaredField("myService") to get a reference to the field.
o Example:
field.setAccessible(true);
3. Set the Dependency:
o Spring assigns the resolved bean to the field using Field.set().
o Example:
field.set(targetObject, resolvedBean);
4. Here, targetObject is the instance of the class containing the field, and resolvedBean is the
dependency bean.
3. Setter Injection
For setter-based injection, Spring invokes the setter method with the dependency as an argument.
Example:
@Autowired
public void setMyService(MyService myService) {
this.myService = myService;
}
How Reflection Works:
1. Retrieve the Method:
o Spring uses Class.getDeclaredMethod("setMyService", MyService.class) to find the
setter method.
o Example:
method.setAccessible(true);
3. Invoke the Method:
o Spring calls Method.invoke() with the target object and the resolved dependency.
o Example:
method.invoke(targetObject, resolvedBean);
4. Constructor Injection
For constructor-based injection, Spring uses the Constructor.newInstance() method to create an
instance of the class with the required dependencies.
Example:
@Autowired
public MyComponent(MyService myService) {
this.myService = myService;
}
constructor.setAccessible(true);
class MyService {
public void doSomething() {
System.out.println("Service is working!");
}
}
class MyComponent {
@Autowired
private MyService myService;
import java.lang.reflect.Field;
Why RequiredConstructor of Lombok using in constructor is more favored than setter injection ?
@RequiredArgsConstructor
public class MyService {
private final Dependency dependency; // Immutable and required
}
2. Mandatory Dependencies
Constructor-based injection ensures that all required dependencies are provided at the time
of object creation. If a required dependency is missing, the code will fail to compile or throw
an error at runtime.
With setter injection, dependencies can be optional, leading to potential issues if required
dependencies are not set explicitly.
Example:
@RequiredArgsConstructor
public class MyService {
private final Dependency dependency; // Ensures mandatory injection
}
If the dependency is not provided, the application won’t start, ensuring the issue is caught
early.
3. Thread Safety
Constructor-based injection with final fields ensures thread safety because the injected
dependencies cannot be modified after initialization.
With setter injection, dependencies can be altered at any point, making the class vulnerable to
race conditions in multi-threaded environments.
@RequiredArgsConstructor
public class MyService {
private final Dependency dependency; // Clear requirement
}
Setter Injection:
6. Testability
Constructor-based injection improves testability, as dependencies are set up at the time of
object creation. You can easily pass mocks or stubs in tests.
With setter injection, you need to ensure all setters are called correctly in tests, which adds
complexity.
Constructor Injection in Tests:
Without Lombok:
With Lombok:
@RequiredArgsConstructor
public class MyService {
private final Dependency dependency;
}
@RequiredArgsConstructor
public class MyService {
private final Dependency mandatoryDependency;
@Autowired(required = false)
private OptionalDependency optionalDependency;
}
@Component
public class MyService {
private final Dependency dependency;
@Component
public class MyService {
private Dependency dependency;
public MyService() {
// Default constructor
}
@Autowired
public void setDependency(Dependency dependency) {
this.dependency = dependency;
}
}
Pros:
o Suitable for optional dependencies.
o Easier to manage for beans with many dependencies.
Cons:
o Allows dependencies to be null if setters are not called.
o Fields cannot be final, reducing immutability.
o Dependencies can be changed post-instantiation, which can lead to bugs.
7. Real-World Usage
Constructor Injection:
o Preferred for mandatory dependencies.
o Used in modern applications due to its alignment with immutability and best practices.
o Often combined with Lombok’s @RequiredArgsConstructor.
Setter Injection:
o Used for optional dependencies.
o Common in legacy applications or scenarios requiring flexibility.
An example of re-assignment occurs when a field is not final, and its value is accidentally or
deliberately changed after the object has been instantiated. This can lead to unexpected behavior or
bugs, especially when the field represents a dependency that the object relies on.
Here’s a detailed example:
@Component
public class MyService {
private NotificationService notificationService;
@Autowired
public MyService(NotificationService notificationService) {
this.notificationService = notificationService;
}
@Component
public class AlternativeNotificationService implements NotificationService {
@Override
public String getName() {
return "AlternativeNotificationService";
}
}
@Component
public class DefaultNotificationService implements NotificationService {
@Override
public String getName() {
return "DefaultNotificationService";
}
}
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
// Simulate re-assignment
NotificationService alternativeService = context.getBean(AlternativeNotificationService.class);
myService.setNotificationService(alternativeService);
Problem:
The notificationService dependency is re-assigned, potentially introducing inconsistencies.
This may break the application's logic if the new dependency behaves differently than
expected.
2. With final Field (Preventing Re-assignment)
Example Code:
@Component
public class MyService {
private final NotificationService notificationService;
@Autowired
public MyService(NotificationService notificationService) {
this.notificationService = notificationService; // Set only once
}
Main Application:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
Benefit:
The final modifier ensures that notificationService can only be assigned during construction.
Any attempt to re-assign the field results in a compile-time error, eliminating the risk of
runtime bugs caused by accidental re-assignment.
3. Key Points
Without final:
o Fields are mutable, allowing re-assignment after the object is created.
o This can lead to bugs if the state of the object changes unpredictably.
With final:
o Fields are immutable, ensuring they are assigned once and never changed.
o This makes the object safer, especially in multithreaded or shared contexts.
Here’s an example demonstrating thread safety in the context of constructor-based injection and
setter-based injection in Java:
Constructor-based injection (Thread-Safe Example)
In constructor-based injection, dependencies are provided when the object is created and cannot be
changed afterwards because they are typically assigned to final fields. This ensures that the object is
immutable and thread-safe.
class SomeDependency {
public void doWork() {
// Simulating work
System.out.println("Work done");
}
}
t1.start();
t2.start();
}
}
Explanation:
In this example, SomeDependency is injected via the constructor, and the dependency field is
marked as final, ensuring that it cannot be changed after initialization.
Multiple threads can safely access the performTask() method because the injected
dependency is immutable, preventing race conditions or changes during execution.
Setter-based injection (Not Thread-Safe Example)
With setter-based injection, dependencies can be changed after object creation, which introduces the
risk of modifying dependencies in a multi-threaded environment.
class SomeDependency {
public void doWork() {
// Simulating work
System.out.println("Work done");
}
}
t1.start();
t2.start();
}
}
Explanation:
In this case, the dependency is set via a setter method, allowing it to be changed at any point
during the object's lifecycle.
If the setDependency() method is called concurrently from multiple threads, the dependency
might be modified while other threads are using it, causing potential race conditions or
inconsistent behavior.
Key Points:
Constructor-based injection ensures that the object's state is immutable after creation,
making it thread-safe in concurrent environments.
Setter-based injection allows dependencies to be changed at runtime, making it more
vulnerable to race conditions if not handled carefully.
The thread safety in the constructor-based injection example is partly achieved by using the final
keyword for the injected dependencies. Let's break it down further:
Why final Ensures Thread Safety:
The final keyword in Java indicates that a field can only be assigned once. Once the
dependency is injected via the constructor, the final field cannot be reassigned. This
guarantees that the object’s state is immutable after initialization, making it safe to access
from multiple threads without worrying about the field being changed during execution.
Key Points:
1. Immutability: When you mark a dependency as final, you ensure that the object cannot be
reinitialized after the object is constructed. This means there’s no risk of another thread
changing the dependency while other threads are using it.
2. Thread-Safety in Concurrent Environments: In multi-threaded environments, if an object’s
state can change (like with setter-based injection), you might encounter race conditions
where multiple threads modify the object’s state at the same time. With final fields, this risk is
eliminated for the injected dependencies because they can’t be modified after the constructor
has completed.
Example Recap:
In this example, the dependency is immutable because of the final keyword, ensuring that no other
thread can change it after it's been set in the constructor. This helps avoid race conditions where the
state of the dependency might change unexpectedly.
In contrast, with setter injection, there’s no guarantee that the dependency will remain constant over
time, making it more prone to issues in concurrent scenarios.
Additional Thread Safety Measures:
Synchronization: Although final ensures the dependency can't be changed, using
synchronized or other mechanisms (like volatile or atomic operations) might still be
necessary if the method involves shared mutable state (like updating variables or performing
operations that could affect other threads).