Spring IoC & Dependency Injection

- Published on
- /10 mins read/
Trong thế giới phát triển ứng dụng Java, Spring Framework là cái tên thống trị tuyệt đối. Và nếu ví toàn bộ hệ sinh thái Spring như một cỗ xe đồ sộ, thì IoC Container (Inversion of Control) và Dependency Injection (DI) chính là khối động cơ nguyên khối vận hành toàn bộ cỗ xe đó.
Mặc dù hầu hết các kỹ sư backend đều sử dụng @Component, @Service, và @Autowired hàng ngày, nhưng khi bước vào các bài toán kiến trúc hệ thống quy mô lớn, nhiều câu hỏi bản chất vẫn là điểm mù:
- DIP, IoC và DI khác nhau như thế nào về mặt lý thuyết và kiến trúc?
- Một Spring Bean thực sự trải qua những giai đoạn nào trong vòng đời từ khi nạp class đến lúc bị hủy? Tại sao
@Transactionalhay Spring Security Proxy chỉ được sinh ra ở giai đoạnBeanPostProcessor? - Tại sao Field Injection (
@Autowiredtrên private field) bị coi là một anti-pattern nguy hiểm và chính thức bị khai tử trong các tiêu chuẩn code hiện đại? - Spring giải quyết vấn đề phụ thuộc vòng (Circular Dependency) bên dưới mã nguồn
DefaultSingletonBeanRegistrythông qua bộ nhớ đệm 3 tầng (Three-Level Cache) như thế nào?
Bài viết này sẽ phân tích chi tiết cơ chế hoạt động của Spring IoC Container từ góc nhìn của một Kỹ sư phần mềm cao cấp và Kiến trúc sư giải pháp.
# phân biệt bộ ba dip vs ioc vs di
Rất nhiều lập trình viên sử dụng ba khái niệm này thay thế cho nhau một cách tùy tiện. Về mặt khoa học máy tính, chúng nằm ở 3 tầng trừu tượng hoàn toàn khác biệt:
- DIP: Là tôn chỉ thiết kế phần mềm trừu tượng.
- IoC: Là mẫu kiến trúc đảo ngược luồng điều khiển. Thay vì code của bạn chủ động
new OrderRepository(), bạn nhường quyền đó cho Spring Container. - DI: Là hành động cụ thể của Spring: tạo ra instance
OrderRepositoryvà "bơm" (inject) vàoOrderService.
# vòng đời 12 giai đoạn của spring bean
Một Spring Bean từ khi được khai báo cho đến khi sẵn sàng phục vụ HTTP requests phải trải qua một quy trình kiểm duyệt và biến đổi nghiêm ngặt bên trong ApplicationContext:
# tại sao beanPostProcessor afterInitialization là then chốt
Khi bạn đánh dấu một method là @Transactional hoặc @Secured, đối tượng thực sự được Spring tiêm vào các Controller/Service khác không phải là class nguyên bản của bạn!
- Tại bước này, Spring AOP chặn lại và bọc đối tượng bằng một CGLIB Dynamic Proxy Class (ví dụ:
OrderService$$SpringCGLIB$$0). - Nếu bạn gọi hàm nội bộ
this.methodWithTransactional(), cuộc gọi diễn ra bên trong instance gốc mà không đi qua Proxy, dẫn đến việc Transaction bị bypass hoàn toàn!
TIP
Để tránh bẫy self-invocation của Spring AOP, hãy tách các method cần @Transactional hoặc @Async sang một Service riêng biệt, hoặc inject chính Service đó thông qua lazy lookup để đi qua CGLIB Proxy.
# vì sao field injection bị khai tử
Trong các tutorial cũ, bạn luôn thấy kiểu viết này:
@Service
public class OrderService {
@Autowired // FIELD INJECTION: ANTI-PATTERN NGUY HIỂM!
private PaymentClient paymentClient;
@Autowired
private OrderRepository orderRepository;
}Mặc dù ngắn gọn, Field Injection chính thức bị cấm trong các tiêu chuẩn code Enterprise vì 4 lý do chết người:
# constructor injection chuẩn mực với requiredArgsConstructor
@Service
@RequiredArgsConstructor // Tự động sinh Constructor cho toàn bộ các trường 'final'
public class OrderService {
// Tuyệt đối bất biến (Immutable), an toàn đa luồng 100%
private final PaymentClient paymentClient;
private final OrderRepository orderRepository;
// Dễ dàng viết Unit Test trong 0.1ms mà không cần Mockito Reflection:
// OrderService service = new OrderService(mockPayment, mockRepo);
}# bộ nhớ đệm 3 tầng xử lý circular dependency
Một bài toán kinh điển: Class A cần inject B, và Class B lại cần inject A.
Nếu chỉ khởi tạo thông thường: Để tạo A, Spring phải có B. Để tạo B, Spring lại phải có A -> Rơi vào vòng lặp vô tận và ném ra BeanCurrentlyInCreationException!
Để hóa giải điều này cho các singleton bean, lớp DefaultSingletonBeanRegistry của Spring duy trì bộ nhớ đệm 3 tầng (Three-Level Cache):
WARNING
Thay đổi kiến trúc từ Spring Boot 2.6+: Mặc dù bộ nhớ đệm 3 tầng giải quyết được phụ thuộc vòng cho setter/field injection, circular dependency luôn là dấu hiệu của kiến trúc bị chia tách sai trách nhiệm. Kể từ Spring Boot 2.6, Spring mặc định cấm hoàn toàn circular dependency (spring.main.allow-circular-references: false). Ứng dụng sẽ fail-fast ngay khi khởi động!
# scope mismatch: hiểm họa inject prototype vào singleton
Theo mặc định, mọi bean trong Spring đều là singleton (chỉ có duy nhất một instance trong ApplicationContext).
Giả sử bạn có một ReportGenerator (scope: prototype - mỗi lần gọi tạo một object mới độc lập) được inject vào một BillingService (scope: singleton):
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ReportGenerator {
private String reportId = UUID.randomUUID().toString();
}
@Service // Singleton (Mặc định)
@RequiredArgsConstructor
public class BillingService {
private final ReportGenerator reportGenerator; // Bẫy Scope Mismatch!
public void generate() {
System.out.println(reportGenerator.getReportId());
}
}# tại sao code bị lỗi logic?
Do BillingService là singleton, nó chỉ được khởi tạo đúng 1 lần duy nhất lúc Spring startup. Tại thời điểm đó, một instance ReportGenerator được inject vào.
- Trong toàn bộ vòng đời của ứng dụng, mọi lời gọi
billingService.generate()sẽ tái sử dụng mãi mãi đúng 1 instanceReportGeneratorban đầu. - Đặc tính
prototypebị vô hiệu hóa hoàn toàn!
# giải pháp kiến trúc
# cách 1: sử dụng ObjectProvider
@Service
@RequiredArgsConstructor
public class BillingService {
private final ObjectProvider<ReportGenerator> reportGeneratorProvider;
public void generate() {
// Mỗi lần gọi getObject() sẽ kích hoạt container sinh ra một prototype mới
ReportGenerator generator = reportGeneratorProvider.getObject();
System.out.println(generator.getReportId());
}
}# cách 2: sử dụng scoped proxy
Spring sẽ inject một CGLIB Proxy thay cho object thật. Khi method của proxy được gọi, nó sẽ định tuyến động tới instance prototype tương ứng (@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)).
TIP
Lựa chọn thực chiến: Ưu tiên dùng ObjectProvider<T> vì cú pháp tường minh, dễ viết unit test giả lập (mocking) và không làm phát sinh thêm overhead của dynamic proxy CGLIB trong runtime.
# so sánh các kiểu dependency injection
| Tiêu chí | Constructor Injection (Khuyên dùng) | Setter Injection | Field Injection (Anti-Pattern) |
|---|---|---|---|
Tính bất biến (final) | 🟢 Hỗ trợ 100% | 🔴 Không thể | 🔴 Không thể |
| Độ dễ dàng unit test | 🟢 Cực dễ (Không cần Spring) | 🟡 Trung bình (gọi setter) | 🔴 Khó (Cần Reflection / Mockito Runner) |
| Ngăn chặn khớp nối chặt | 🟢 Cảnh báo ngay nếu constructor quá dài | 🔴 Dễ bị lạm dụng | 🔴 Hoàn toàn che giấu mùi code |
| Xử lý null safety | 🟢 Tránh hoàn toàn NPE | 🔴 Dễ dính NPE nếu quên set | 🔴 Dễ dính NPE ngoài Spring context |
| Phát hiện circular dep | 🟢 Fail-Fast ngay lúc startup | 🟡 Tự động hóa giải ngầm | 🟡 Tự động hóa giải ngầm |
# tổng kết và khuyến nghị
Hiểu sâu về Spring IoC & Dependency Injection là lằn ranh phân biệt giữa việc chỉ sử dụng framework theo thói quen và một kỹ sư làm chủ toàn bộ hành vi của hệ thống:
- Dẹp bỏ field injection: Luôn sử dụng constructor injection với
finalfields để đảm bảo tính bất biến và dễ dàng viết unit test không phụ thuộc container. - Nắm vững vòng đời 12 bước của bean: Để biết chính xác thời điểm proxy AOP được hình thành và tránh các bẫy tự gọi method nội bộ (
this). - Cảnh giác với scope mismatch: Luôn dùng
ObjectProviderkhi inject các bean có vòng đời ngắn hơn (prototype, request scope) vào singleton bean.
Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!
Happy coding 😎 👍🏻 🚀 🔥.
On this page
- # phân biệt bộ ba dip vs ioc vs di
- # vòng đời 12 giai đoạn của spring bean
- # tại sao beanPostProcessor afterInitialization là then chốt
- # vì sao field injection bị khai tử
- # constructor injection chuẩn mực với requiredArgsConstructor
- # bộ nhớ đệm 3 tầng xử lý circular dependency
- # scope mismatch: hiểm họa inject prototype vào singleton
- # tại sao code bị lỗi logic?
- # giải pháp kiến trúc
- # cách 1: sử dụng ObjectProvider
- # cách 2: sử dụng scoped proxy
- # so sánh các kiểu dependency injection
- # tổng kết và khuyến nghị