Spring Core
table of contents
Dependency Injection
Process where objects use their dependent objects without a need to define or create them is called
Dependency injection.
We can inject dependent objects in three ways, using:
- Constructor injection
- Setter injection
- Field injection : involves injecting dependencies directly into the class using the @Autowired annotation.
Dependency Injection in Spring official doc
Field injection is not recommended
- Null safety
- Immutability
- Design Problems
- Single Responsibility Violation
- Circular Dependencies
- Testing
Null-Safety
- Field injection creates a risk of NullPointerException if dependencies aren’t correctly initialized.
- Using the field injection, we didn’t provide a direct way of instantiating the EmailService with required dependencies.
- We are able to create the EmailService instance using the default constructor.
- we can reduce the risk of NullPointerException using the constructor injection as it exposes the required dependencies publicly.
@Service
public class EmailService {
@Autowired
private EmailValidator emailValidator;
}
Immutability
- Using the field injection, we are unable to create immutable classes.
- We need to instantiate the final fields when they’re declared or through the constructor.
- Spring performs autowiring once the constructors have been called. Therefore, it’s impossible to autowire the final fields using field injection.
- Since the dependencies are mutable, there’s no way we can ensure they will remain unchanged once they’re initialized.
- reassigning non-final fields can cause unexpected side effects when running the application.
- Alternatively, we can use constructor injection for mandatory dependencies and setter injection for optional ones. This ensure the required dependencies will remain unchanged.
Design Problem - Single Responsibility
- One class should be responsible for only one action and, thus, have only one reason to change.
- We can easily add more dependencies than necessary and create a class that’s doing more than one job.
- Using constructor injection, we’d notice we might have a design problem if a constructor has more than a few dependencies.
- Furthermore, even the IDE would issue a warning if there are more than seven parameters in the constructor.
Design Problem Circular Dependencies
- When two or more classes depend on each other, and it’s impossible to construct objects, and the execution can end up with runtime errors or infinite loops.
- With Field Injection, dependencies are injected when needed and not on the context load, Spring won’t throw BeanCurrentlyInCreationException.
- With constructor injection, it’s possible to detect circular dependencies at compile time since they would create unresolvable errors.
- circular dependencies in our code are a sign something is wrong with our design. Therefore, we should consider redesigning our application if possible.
- Since Spring Boot 2.6. version circular dependencies are no longer allowed by default.
Testing Field Injection
- We can’t directly replace the field injected dependency with a mocked version.
- We can instantiate our class through reflection.
- Mockito will try to inject mocks using the @InjectMocks annotation. However, if the field injection strategy fails, Mockito won’t report the failure.
@Component used as syncronized
- Using @Component in combination with synchronized in Spring indicates a desire to make a bean thread-safe, ensuring that only one thread can execute specific methods or code blocks at a time.
- By default, Spring beans are singletons, meaning the same instance is shared across threads, making manual synchronization necessary for thread safety.
Key Approaches to Synchronization in Spring Components
- Synchronized Methods : You can mark methods within your @Component class as synchronized. Since the bean is a singleton, this locks the entire object instance.
- Synchronized Blocks (Fine-Grained) : Instead of locking the entire method, you can use synchronized(this) or a dedicated lock object for better performance, allowing other parts of the component to remain accessible.
@Component
public class MyComponent {
private int count = 0;
private final Object lock = new Object();
// Only one thread can enter this method per instance
public synchronized void increment() {
count++;
}
public void doTask() {
// Specific critical section
synchronized(lock) {
// ... thread-safe code ...
}
}
}
Considerations and Best Practices
- Performance: Excessive use of synchronized can lead to bottlenecks, as other threads will be blocked until the lock is released.
- Deadlocks: Avoid nesting synchronized blocks or locking multiple objects in different orders, as this can cause deadlocks.
- Alternative (Concurrent Classes): For managing shared data, consider using java.util.concurrent classes (e.g., AtomicInteger, ConcurrentHashMap) instead of synchronized for better scalability.
- Lock Scope: A synchronized method locks the specific Bean instance.
- If you have multiple instances of the same @Component (e.g., using @Scope(“prototype”)), they will not block each other.
- If you need to lock across multiple instances, consider static synchronization, which locks the class object.
Note: In many scenarios, if the component is stateless, no synchronization is required at all.
ReentrantLock instead of synchronized
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
@Component
public class SmartInventoryComponent {
private final ReentrantLock lock = new ReentrantLock(); // Add 'true' for fairness
private int stock = 100;
public void updateStockWithTimeout() throws InterruptedException {
// Try to get the lock within 2 seconds instead of waiting forever
if (lock.tryLock(2, TimeUnit.SECONDS)) {
try {
stock--;
} finally {
lock.unlock(); // Always unlock in finally
}
} else {
// Handle failure to acquire lock (e.g., log a warning or return an error)
System.out.println("Could not acquire lock, system busy");
}
}
}
While synchronized is simpler and often optimized by the JVM for low-contention scenarios, ReentrantLock offers specific advantages:
- TryLock: Attempt to acquire the lock immediately or within a specific timeframe (tryLock()), allowing the thread to perform other tasks if the lock is held.
- Fairness: You can initialize it with new ReentrantLock(true) to ensure the longest-waiting thread gets the lock next, preventing “starvation”.
- Interruption: Threads waiting for a lock via lockInterruptibly() can be interrupted, which is useful for clean shutdowns.
- Advanced Conditions: You can create multiple Condition variables from a single lock, providing more flexibility than basic wait() and notify().