Backend

99 Ngày Spring — Ngày 04: các kiểu Dependency Injection

SSite Admin
31 tháng 07, 2026 4 phút đọc 241 lượt xem
99 Ngày Spring — Ngày 04: các kiểu Dependency Injection

Hôm qua bạn đã thấy container tạo và nuôi bean. Nhưng còn mảnh ghép cuối: bean này cần bean kia thì container đưa vào bằng cách nào? Đó là dependency injection — và Spring cho bạn 3 cửa: constructor, setter và field. Hôm nay ta xem từng cửa, và lý do cả cộng đồng (lẫn chính Spring team) khuyên đi cửa số một.

Sketchnote Ngày 04: Ba kiểu dependency injection trong Spring

Bài toán: bean cần bean

@Servicepublic class OrderService {
    // OrderService cần PaymentService để làm việc.
    // Làm sao container ĐƯA PaymentService vào đây?
}

Container biết cả OrderService lẫn PaymentService (nhờ @Service — Ngày 03). Việc còn lại là chỉ chỗ cho container đặt PaymentService vào: qua tham số constructor, qua method setter, hay ghi thẳng vào field.

Kiểu 1: Constructor injection — khuyên dùng

@Servicepublic class OrderService {
    private final PaymentService paymentService;

    // 1 constructor duy nhất → không cần @Autowired
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}
  • Field khai báo final được — object bất biến sau khi tạo, không ai thay được phụ thuộc giữa chừng.

  • Bean không bao giờ tồn tại ở trạng thái thiếu đồ nghề: thiếu phụ thuộc là fail ngay lúc khởi động, không phải NPE lúc nửa đêm.

  • Từ Spring 4.3: class chỉ có 1 constructor thì không cần @Autowired — code sạch như POJO thuần.

Kiểu 2: Setter injection — cho phụ thuộc tùy chọn

@Servicepublic class NotificationService {
    private SmsSender smsSender;   // phụ thuộc TÙY CHỌN

    @Autowired(required = false)
    public void setSmsSender(SmsSender smsSender) {
        this.smsSender = smsSender;
    }
}

Setter injection cho phép tạo bean trước, tiêm sau — hợp với phụ thuộc không bắt buộc (không có SmsSender thì thôi, chỉ gửi email) hoặc phụ thuộc muốn thay đổi lại được. Nhược điểm là object khả biến: ai đó có thể gọi setter lần nữa giữa vòng đời, và bạn phải tự kiểm tra null trước khi dùng.

Kiểu 3: Field injection — gọn mà đắt

@Servicepublic class ReportService {
    @Autowired
    private UserRepository userRepository;  // tiêm thẳng vào field

    // Nhìn gọn... nhưng là kiểu bị khuyên TRÁNH nhiều nhất!
}

Nhìn ngắn nhất — không constructor, không setter — nên từng là kiểu phổ biến nhất trong tutorial cũ. Nhưng cái giá không nhỏ:

  • Khó test — muốn đặt phụ thuộc giả phải dùng reflection hoặc kéo cả Spring context vào unit test.

  • Che giấu phụ thuộc — class 15 field @Autowired nhìn vẫn "gọn", trong khi constructor 15 tham số sẽ hét lên: class này ôm việc quá nhiều!

  • Không dùng được final — phụ thuộc có thể bị đổi bằng reflection bất cứ lúc nào.

  • Tách khỏi Spring là chết: không container thì không có cách chính thống nào đặt phụ thuộc vào.

Vì sao chọn constructor ?

// Constructor injection → test không cần Spring:PaymentService fake = new FakePaymentService();
OrderService service = new OrderService(fake);   // xong!

// Field injection → new xong field vẫn null,// phải nhờ reflection hoặc bật cả Spring context để test.

Gom lại một câu: constructor injection cho bạn bất biến (final), an toàn (fail sớm), trung thực (lộ rõ mọi phụ thuộc) và dễ test (chỉ cần new). Tài liệu chính thức của Spring khuyến nghị: constructor cho phụ thuộc bắt buộc, setter cho phụ thuộc tùy chọn — còn field injection nên để dành cho... code demo. Mẹo thực dụng: constructor bắt đầu đông tham số chính là "chuông báo" class đang phình — tách nhỏ nó thay vì đổi sang field injection để giấu bụng.

Bài tập nhỏ

  • Tạo PaymentService và OrderService với constructor injection, in một dòng log trong constructor để thấy thứ tự container tạo bean.

  • Thử xóa @Service khỏi PaymentService và chạy lại — đọc kỹ lỗi required a bean of type: đây là fail-fast đang cứu bạn.

  • Viết unit test cho OrderService bằng cách new nó với một PaymentService giả — không cần Spring.

  • Đổi tạm sang field injection và thử viết lại test đó — cảm nhận sự khác biệt rồi... đổi về.

Kết luận

Bạn đã biết 3 con đường đưa phụ thuộc vào bean và lý do constructor injection là mặc định đúng đắn: final, fail sớm, trung thực, dễ test. Ngày 05, ta trả lời câu hỏi ngược lại: làm sao container tìm thấy các bean ngay từ đầu — @Component và component scan, cùng lỗi kinh điển "bean ngoài package không được quét". Hẹn gặp lại!

S

Site Admin

Engineer and writer. Building things with TypeScript and distributed systems.

Bình luận (0)

Bạn cần đăng nhập bằng Google để bình luận.

Hãy là người bình luận đầu tiên.

Bài viết liên quan

99 Ngày Java — Ngày 58: Optional

Optional là công cụ cho kiểu trả về — không field, không tham số, không bọc collection; chuỗi map/flatMap/filter thay kim tự tháp if; orElse luôn tính tham số còn orElseGet lười, orElseThrow cho “không có là lỗi” — và các anti-pattern isPresent + get, orElse(null).

23 thg 9, 20269 phút9
99 Ngày Spring — Ngày 58: Spring Boot Actuator

Boot 3 mặc định chỉ lộ health; bản đồ endpoint an toàn và nguy hiểm; HealthIndicator tự viết với liveness tách khỏi readiness cho Kubernetes, info từ build và git, metrics Micrometer với tag hữu hạn — và ba lớp khóa: cổng riêng, SecurityFilterChain với EndpointRequest, exclude env/heapdump/threaddump.

23 thg 9, 202610 phút5
So sánh EasyExcel và Apache Fesod: khác biệt thật nằm ở đâu?

Fesod là hậu duệ của EasyExcel, cùng engine SAX/SXSSF. Khác biệt kiểm chứng được: vòng đời dự án, POI 5.2.5 vs 5.5.1, các API mới (numRows, csv(), headerMergeStrategy) và độ bền với file xấu — không phải hiệu năng gấp nhiều lần.

23 thg 9, 20268 phút1