Dependency Injection trong Spring: hiểu bean, @Autowired và constructor injection

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Khái niệm khiến người mới Spring bối rối nhất thường là: ai tạo ra các đối tượng, và vì sao tôi không cần gõ new? Câu trả lời nằm ở Inversion of Control và Dependency Injection. Hiểu chúng, phần lớn "phép màu" của Spring sẽ trở nên rõ ràng.

Vấn đề khi tự tạo phụ thuộc

Giả sử OrderService cần gửi thông báo. Cách ngây thơ là tự tạo bên trong:

public class OrderService {    private final EmailSender sender = new EmailSender(); // dính chặt vào cài đặt cụ thể    public void place(Order o) {        // ...        sender.send(o.getUserEmail(), "Đã đặt hàng");    }}

Cách viết này khiến OrderService gắn chặt vào EmailSender. Muốn đổi sang gửi SMS, hoặc dùng bản giả khi kiểm thử, bạn phải sửa chính class đó.

Đảo ngược quyền kiểm soát

Với DI, class chỉ khai báo nó cần gì, còn việc tạo và cung cấp được giao cho bên ngoài (container của Spring):

public interface Notifier {    void notify(String to, String message);}@Servicepublic class EmailNotifier implements Notifier {    public void notify(String to, String message) { /* gửi email */ }}@Servicepublic class OrderService {    private final Notifier notifier;    public OrderService(Notifier notifier) { // Spring tự tiêm        this.notifier = notifier;    }    public void place(Order o) {        notifier.notify(o.getUserEmail(), "Đã đặt hàng");    }}

Giờ OrderService chỉ biết giao diện Notifier. Đổi cài đặt hay thay bản giả khi test không cần đụng đến nó.

Bean và container

Một bean là đối tượng do Spring tạo và quản lý. Container quét code tìm các class có chú thích như @Component, @Service, @Repository, @Controller, tạo bean tương ứng và nối chúng với nhau theo kiểu phụ thuộc. Bạn cũng có thể khai báo bean thủ công:

@Configurationpublic class AppConfig {    @Bean    public Clock clock() {        return Clock.systemUTC();    }}

Mặc định bean có phạm vi singleton: mỗi ứng dụng chỉ có một thể hiện. Vì vậy bean không nên giữ trạng thái riêng của từng người dùng trong các trường của nó.

Ba cách tiêm phụ thuộc

  1. Constructor injection (khuyên dùng): phụ thuộc truyền qua hàm khởi tạo, có thể khai báo final, dễ test, không thể tạo đối tượng thiếu phụ thuộc.
  2. Setter injection: dùng cho phụ thuộc tùy chọn.
  3. Field injection với @Autowired trên trường: ngắn gọn nhưng khó test và che giấu phụ thuộc, nên tránh trong code mới.

Từ các phiên bản Spring gần đây, nếu class chỉ có một constructor thì không cần ghi @Autowired, như ví dụ ở trên.

Khi có nhiều bean cùng kiểu

Nếu có cả EmailNotifier và SmsNotifier cùng cài Notifier, Spring không biết chọn cái nào và báo lỗi. Bạn giải quyết bằng @Primary để đánh dấu mặc định, hoặc @Qualifier để chỉ rõ:

public OrderService(@Qualifier("smsNotifier") Notifier notifier) {    this.notifier = notifier;}

Lợi ích khi kiểm thử

Vì phụ thuộc đi qua constructor, bạn có thể kiểm thử mà không cần khởi động Spring:

@Testvoid placeSendsNotification() {    Notifier fake = mock(Notifier.class);    OrderService service = new OrderService(fake);    service.place(new Order("a@example.com"));    verify(fake).notify("a@example.com", "Đã đặt hàng");}

Test chạy trong mili giây, không cần database hay mạng. Đây chính là lý do thiết kế hướng DI được đánh giá cao.

Những lỗi thường gặp

  • Phụ thuộc vòng tròn: hai bean cần nhau; hãy xem lại thiết kế, tách một phần ra bean thứ ba.
  • Quên chú thích nên class không được quét thành bean, dẫn đến lỗi "No qualifying bean".
  • Đặt class ngoài package gốc của ứng dụng nên không được quét.

Vòng đời và phạm vi của bean

Ngoài singleton, Spring còn các phạm vi khác như prototype (mỗi lần xin một thể hiện mới), request và session trong ứng dụng web. Bạn hiếm khi cần chúng, nhưng nên biết để hiểu khi nào một đối tượng được dùng chung. Bean cũng có vòng đời: được tạo, tiêm phụ thuộc, khởi tạo (có thể dùng @PostConstruct), sử dụng rồi hủy khi ứng dụng tắt (@PreDestroy). Hãy tránh đặt logic nặng hoặc gọi mạng trong lúc khởi tạo bean, vì lỗi ở đó sẽ khiến cả ứng dụng không khởi động được.

Một mẹo khi gỡ lỗi: bật log cấp DEBUG cho gói org.springframework hoặc dùng endpoint /actuator/beans để xem danh sách bean và quan hệ giữa chúng. Khi gặp lỗi "No qualifying bean" hay "circular dependency", nhìn vào danh sách này thường giúp bạn tìm ra nguyên nhân nhanh hơn nhiều so với đoán mò.

Kết luận

DI là cách thiết kế để các thành phần lỏng lẻo, dễ thay thế và dễ kiểm thử. Hãy ưu tiên constructor injection, phụ thuộc vào interface khi hợp lý, và để Spring lo phần lắp ráp. Một khi nắm được tư duy này, đọc code Spring sẽ dễ chịu hơn nhiều.

Muốn học bài bản và có lộ trình rõ ràng? Bạn có thể xem các khóa học trên codebem.com.

author_photo
Nguyễn Đình Cường

0 comment