Dependency Injection: So Sánh 3 Hình Thức Nối Object
So sánh constructor, setter và field injection; cơ chế resolve @Autowired, generic-aware matching và các anti-pattern cần tránh.
TL;DR: Dependency Injection (DI) trong Spring có 3 hình thức chính: Constructor Injection (tiêu chuẩn khuyến nghị cao nhất nhờ tính bất biến final, hỗ trợ kiểm thử cô lập và phát hiện phụ thuộc vòng sớm), Setter Injection (chỉ phù hợp cho các phụ thuộc tùy chọn - optional), và Field Injection (anti-pattern vì phá vỡ tính bao đóng và trói chặt vào container). Spring giải quyết @Autowired tuần tự qua các bước: kiểm tra kiểu dữ liệu, đối soát @Qualifier, ưu tiên @Primary, so khớp tên bean, và báo lỗi nếu có sự nhập nhằng.
Ở bài học trước về Inversion of Control: Bản Chất Đảo Quyền Điều Khiển, chúng ta đã biết DI là công cụ chủ đạo để thực hiện hóa nguyên lý IoC. Bài viết này đào sâu vào cơ chế: Tại sao Spring Team khuyến nghị Constructor Injection trên phương diện kỹ thuật, và container thực sự làm gì khi đối mặt với chú thích @Autowired?
Ba hình thức Dependency Injection trong Spring vận hành như thế nào?
Xét một lớp dịch vụ xử lý đơn hàng OrderService cần tương tác với cổng thanh toán PaymentGateway. Ta có thể liên kết chúng qua 3 cách tiếp cận:
1. Constructor Injection (Chuẩn mực khuyến nghị)
@Service
public class OrderService {
private final PaymentGateway payment; // Bat buoc bat bien (immutable)
public OrderService(PaymentGateway payment) {
this.payment = payment;
}
}
Từ phiên bản Spring 4.3 trở đi, nếu một class chỉ có duy nhất một constructor, bạn hoàn toàn không cần khai báo @Autowired. Container tự động nhận diện và tiêm các tham số tương ứng vào constructor này.
2. Setter Injection (Phụ thuộc tùy chọn)
@Service
public class ReportService {
private NotificationService notifier;
@Autowired(required = false)
public void setNotifier(NotificationService notifier) {
this.notifier = notifier;
}
public void generate() {
if (notifier != null) notifier.notify("Report generated successfully");
}
}
Field không thể đánh dấu final và có thể bị thay đổi giá trị bất kỳ lúc nào sau khi khởi tạo. Phương pháp này chỉ nên dùng khi dependency thực sự không bắt buộc và có giá trị fallback an toàn.
3. Field Injection (Anti-pattern cần loại bỏ)
@Service
public class BadOrderService {
@Autowired // Anti-pattern: Dat truc tiep tren private field
private PaymentGateway payment;
}
Bản thân chú thích @Autowired không có lỗi; lỗi nằm ở việc đặt nó trực tiếp lên private field. Để thực hiện việc này, Spring buộc phải dùng Reflection can thiệp thô bạo vào bộ nhớ sau khi đối tượng đã khởi tạo xong.
| Tiêu chí so sánh | Constructor Injection | Setter Injection | Field Injection |
|---|---|---|---|
Khả năng khai báo final | Có (Đảm bảo Thread-safe) | Không (Dễ bị mutate) | Không |
| Bắt buộc có phụ thuộc tại init | Có (Fail-fast ngay lúc nạp) | Không | Không |
| Viết Unit Test thuần Java | Có (new Service(mock)) | Cồng kềnh | Không (Bắt buộc dùng Reflection) |
| Phát hiện phụ thuộc vòng | Báo lỗi ngay khi Startup | Bị trì hoãn | Bị trì hoãn |
| Nhận diện code phình to | Tự nhiên (Constructor quá dài) | Bị che giấu | Bị che giấu hoàn toàn |
Tại sao Constructor Injection luôn là giải pháp được khuyến nghị hàng đầu?
Bốn nguyên nhân kỹ thuật cốt lõi giúp Constructor Injection vượt trội:
- Bảo đảm tính bất biến (Immutability) với
final: Java Memory Model (JMM) bảo đảm giá trị của một biếnfinalsẽ hiển thị an toàn và nhất quán tới mọi luồng ngay sau khi hàm khởi tạo hoàn tất mà không cần thêm cơ chế đồng bộ hóa. - Loại bỏ trạng thái khởi tạo dở dang (Partially Initialized Objects): Với Constructor Injection, container không thể tạo ra instance nếu thiếu bất kỳ bean phụ thuộc nào, ném ra lỗi
UnsatisfiedDependencyExceptionngay tại thời điểm khởi động thay vì gây lỗiNullPointerExceptionlúc đang phục vụ người dùng. - Kiểm thử đơn vị thuần túy với tốc độ cao: Bạn có thể khởi tạo đối tượng nghiệp vụ bằng lệnh
newtiêu chuẩn và truyền mock object vào trong vòng 1 mili-giây mà không cần dựng toàn bộ context nặng nề của Spring. - Phát hiện sớm lỗi kiến trúc phụ thuộc vòng (Fail-Fast): Nếu class A cần B và B cần A qua constructor, container sẽ lập tức phát hiện chu trình và ném lỗi
BeanCurrentlyInCreationExceptiontại thời điểm khởi động.
Cơ chế phân giải @Autowired bên dưới của Spring Container hoạt động ra sao?
Khi đối mặt với một điểm tiêm phụ thuộc, lớp AutowiredAnnotationBeanPostProcessor của Spring phối hợp với DefaultListableBeanFactory kích hoạt thuật toán tìm kiếm bean theo cây quyết định nghiêm ngặt:

Giả sử hệ thống có hai bean StripePayment và MoMoPayment cùng thực thi giao diện PaymentGateway:
- Nếu constructor chỉ nhận
PaymentGateway payment, Spring tìm thấy 2 ứng viên hợp lệ. - Container kiểm tra
@Qualifier: Nếu không có, tiếp tục kiểm tra bean nào có@Primary. - Nếu vẫn không có
@Primary, Spring dùng tên tham số (payment) để đối chiếu với tên bean (stripePaymenthoặcmoMoPayment). - Nếu tên tham số không trùng với bean nào, Spring ném ngoại lệ
NoUniqueBeanDefinitionException.
Để khắc phục, bạn có thể chỉ định rõ @Qualifier("stripePayment"), hoặc gắn @Primary lên bean mặc định, hoặc sử dụng cơ chế gom cụm (Collection Injection).
Generic-aware và Collection Injection giải quyết bài toán đa hình như thế nào?
1. Phân giải chính xác qua kiểu Generic (Spring 4+)
Mặc dù Java áp dụng cơ chế xóa kiểu (Type Erasure) tại runtime, thông tin tham số generic vẫn được bảo lưu trong thuộc tính Signature của bytecode. Lớp ResolvableType của Spring phân tích chữ ký này để tiêm chính xác bean mong muốn:
public interface Repository<T> { T findById(long id); }
@Component public class UserRepository implements Repository<User> {}
@Component public class OrderRepository implements Repository<Order> {}
@Service
public class UserService {
private final Repository<User> repo; // Spring tu dong chon UserRepository
public UserService(Repository<User> repo) {
this.repo = repo;
}
}
2. Tiêm tập hợp (Collection Injection) áp dụng Strategy Pattern
Spring hỗ trợ tiêm toàn bộ các bean cùng loại vào một List hoặc Map, giúp loại bỏ hoàn toàn các khối lệnh switch-case rườm rà:
@Service
public class PaymentRouter {
private final Map<String, PaymentGateway> gatewayMap;
// Spring tu dong truyen toan bo bean PaymentGateway vao List
public PaymentRouter(List<PaymentGateway> gateways) {
this.gatewayMap = gateways.stream()
.collect(Collectors.toMap(PaymentGateway::code, g -> g));
}
public void processPayment(String code, long amount) {
PaymentGateway gateway = gatewayMap.get(code);
if (gateway == null) {
throw new IllegalArgumentException("Unsupported payment code: " + code);
}
gateway.charge(amount);
}
}
Khi tích hợp thêm cổng thanh toán mới như VnPayPayment, bạn chỉ cần định nghĩa một class mới có gắn @Component. Lớp điều phối PaymentRouter tự động nhận diện bean mà không cần thay đổi một dòng mã nào.
Những cạm bẫy thực tế nào khiến Field Injection trở thành anti-pattern?
- Chi phí kiểm thử tăng vọt: Khi sử dụng Field Injection, bạn buộc phải khởi chạy
@SpringBootTest(mất hàng giây) hoặc sử dụng Reflection không an toàn (ReflectionTestUtils.setField) dễ gãy vỡ khi tái cấu trúc tên biến. - Dấu hiệu lớp phình to (God Class) bị che đậy: Một class sở hữu 10 trường
@Autowiredvẫn trông có vẻ gọn gàng. Ngược lại, một constructor với 10 tham số sẽ lập tức gióng lên hồi chuông cảnh báo về vi phạm nguyên lý Single Responsibility. - Hiện tượng NullPointerException khi khởi tạo: Trong Field Injection, Spring tạo đối tượng trước rồi mới gán trường dữ liệu sau. Nếu gọi các hàm phụ thuộc ngay trong constructor, ứng dụng sẽ gặp lỗi NPE vì trường đó chưa kịp được tiêm giá trị.
Tóm tắt
- Constructor Injection là giải pháp tối ưu vì đảm bảo tính bất biến, loại bỏ NullPointerException và dễ dàng kiểm thử cô lập.
- Setter Injection chỉ dành riêng cho các thành phần phụ thuộc tùy chọn không ảnh hưởng đến hoạt động cốt lõi của ứng dụng.
- Spring phân giải phụ thuộc theo thứ tự: Kiểu dữ liệu ->
@Qualifier->@Primary-> Tên biến. - Tận dụng Collection Injection kết hợp Generic để triển khai Strategy Pattern linh hoạt mà không cần sửa đổi mã điều phối.
Tự kiểm tra
- Q1Tại sao từ Spring 4.3 trở đi, lập trình viên không cần khai báo chú thích @Autowired trên constructor của bean?
- Q2Điều gì xảy ra khi container phát hiện có 2 bean cùng triển khai giao diện PaymentGateway nhưng không có @Qualifier hay @Primary?
- Q3Lợi thế lớn nhất của việc dùng Collection Injection (List<PaymentGateway>) trong Spring là gì?
- Q4Nguyên nhân nào khiến Constructor Injection phát hiện lỗi phụ thuộc vòng (Circular Dependency) ngay khi ứng dụng khởi động?
- Q5Tại sao Generic-aware Injection (như Repository<User>) vẫn hoạt động chính xác dù Java có cơ chế Type Erasure?
Bài tiếp theo: Circular Dependency: Cơ Chế Three-Level Cache Trong Spring
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Inversion of Control: Đảo Ngược Quyền Kiểm Soát Trong Spring
Bài tiếpCircular Dependency: Cơ Chế Three-Level Cache Trong Spring
Lộ trình Junior Java Backend Developer
Chặng 13/21 · Spring IoC, DI & Bean — bài 4/17 của chặng
Ôn phỏng vấn
Bài này trả lời được các câu phỏng vấn sau — tự trả lời thử trước khi mở đáp án.
Thảo luận & góp ý
Hỏi đáp về bài này