Principles of Dependency Injection and Inversion of Control (IoC) Container
Dependency Injection (DI) and Inversion of Control (IoC) are core mechanisms for achieving loosely coupled design in backend frameworks. Although we have previously discussed the basic concepts, this time we will delve into the specific implementation principles and workflow of the IoC container.
1. Problem Context: Tightly Coupled Dependencies
Suppose we have a UserService class that depends on the UserRepository class:
// Tightly coupled implementation
class UserRepository {
public void save(User user) {
// Database operation
}
}
class UserService {
private UserRepository userRepo = new UserRepository(); // Directly instantiating the dependency
public void createUser(User user) {
userRepo.save(user);
}
}
The problem with this implementation: UserService directly creates the UserRepository instance, resulting in tight coupling between the two, making it difficult to test and extend.
2. Basic Forms of Dependency Injection
Injecting dependencies through the constructor:
class UserService {
private UserRepository userRepo;
// Dependency injected via constructor
public UserService(UserRepository repo) {
this.userRepo = repo;
}
public void createUser(User user) {
userRepo.save(user);
}
}
Now the dependency relationship is controlled externally, but object creation needs to be managed manually:
UserRepository repo = new UserRepository();
UserService service = new UserService(repo); // Manual dependency assembly
3. Core Responsibilities of an IoC Container
The IoC container automates the management of dependency relationships, with main functions including:
- Registration: Recording component types and their dependencies
- Resolution: Automatically constructing objects and their dependency trees
- Lifecycle Management: Controlling object creation and destruction
4. Implementation Steps of a Simple IoC Container
Step 1: Define the container interface
interface Container {
void register(Class<?> type); // Register component
<T> T resolve(Class<T> type); // Resolve instance
}
Step 2: Implement dependency resolution logic
class SimpleContainer implements Container {
private Map<Class<?>, Class<?>> registry = new HashMap<>();
@Override
public void register(Class<?> type) {
registry.put(type, type);
}
@Override
public <T> T resolve(Class<T> type) {
return createInstance(type);
}
private <T> T createInstance(Class<T> type) {
// Get all constructors of the class
Constructor<?>[] constructors = type.getConstructors();
if (constructors.length != 1) {
throw new RuntimeException("Must have exactly one public constructor");
}
Constructor<?> constructor = constructors[0];
// Get parameter types of the constructor
Class<?>[] paramTypes = constructor.getParameterTypes();
// Recursively resolve all dependencies
Object[] dependencies = Arrays.stream(paramTypes)
.map(this::createInstance)
.toArray();
try {
return (T) constructor.newInstance(dependencies);
} catch (Exception e) {
throw new RuntimeException("Failed to create instance", e);
}
}
}
Step 3: Using the container to manage dependencies
// Register components
Container container = new SimpleContainer();
container.register(UserRepository.class);
container.register(UserService.class);
// Automatically resolve the dependency tree
UserService service = container.resolve(UserService.class);
service.createUser(new User());
5. Implementation Principles of Advanced Features
Singleton Management:
class SingletonContainer extends SimpleContainer {
private Map<Class<?>, Object> singletons = new HashMap<>();
@Override
public <T> T resolve(Class<T> type) {
if (!singletons.containsKey(type)) {
singletons.put(type, super.resolve(type));
}
return (T) singletons.get(type);
}
}
Interface Binding:
interface IRepository {}
class SqlRepository implements IRepository {}
container.registerInterface(IRepository.class, SqlRepository.class);
6. Enhanced Features of Modern IoC Containers
- Annotation-driven: Using annotations like
@Inject,@Singleton - Conditional Assembly: Deciding whether to create beans based on configuration conditions
- AOP Integration: Seamless integration with Aspect-Oriented Programming
- Lifecycle Callbacks: Supporting callback methods like
@PostConstruct
7. Summary of Key Design Points
- Inversion of Control: Transferring control over object creation from business code to the container
- Dependency Lookup: The container is responsible for finding and injecting dependencies
- Configuration Methods: Supporting various methods like XML, annotations, and code configuration
- Lazy Loading: Some implementations support creating instances only when needed
Through the IoC container, applications gain better flexibility, testability, and maintainability, which is one of the cornerstones of modern backend framework architecture design.