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 169 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, setterfield. 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 PaymentServiceOrderService 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 Spring — Ngày 14: Bean Validation — @Valid & bộ constraint chuẩn

Ngày 14 của 99 Ngày Spring: Bean Validation khai báo luật ngay trên DTO — @Valid kích hoạt, bộ constraint chuẩn (@NotBlank vs @NotEmpty vs @NotNull, @Size, @Email, @Min/@Max), bẫy int vs Integer, validate object lồng nhau không tự lan, và lỗi 400 gom một lượt.

10 thg 8, 20265 phút0
99 Ngày Java — Ngày 14: Kế thừa (Inheritance)

Ngày 14 của 99 Ngày Java: extends trao gia tài từ lớp cha, override với @Override và super., thứ tự khai sinh cha trước con sau qua super(...), protected trả nợ Ngày 13, và phép thử is-a vs has-a — khi nào nên composition thay vì kế thừa.

10 thg 8, 20266 phút0
99 Ngày Spring — Ngày 13: @RequestBody & DTO — đừng công khai entity ra API

Ngày 13 của 99 Ngày Spring: @RequestBody + Jackson deserialize JSON thành object, DTO là hợp đồng API — chặn lỗ hổng mass assignment chiều vào và rò rỉ dữ liệu chiều ra, request/response DTO khác nhau là bình thường, và Jackson annotations tinh chỉnh hợp đồng JSON.

9 thg 8, 20266 phút9