Spring có ba cách tiêm phụ thuộc. Bài này giải thích vì sao chỉ nên dùng một.
Tiêm qua hàm khởi tạo
@Service
class TiemHamKhoiTao {
private final Kho kho;
TiemHamKhoiTao(Kho kho) { this.kho = kho; }
}
kho = KhoDB | trường có final? có
Từ Spring 4.3, không cần @Autowired khi lớp chỉ có một hàm khởi tạo. Spring tự hiểu.
Bốn tính chất, và chúng là lý do đây là cách duy nhất nên dùng:
Trường là final — bất biến sau khi tạo, an toàn luồng, và trình biên dịch bảo đảm nó được gán.
Đối tượng luôn hợp lệ. Không tồn tại trạng thái "đã tạo nhưng chưa có phụ thuộc".
Test được không cần Spring — new TiemHamKhoiTao(khoGia) là xong. Không cần @SpringBootTest, không cần phản chiếu.
Phụ thuộc hiện rõ. Hàm khởi tạo tám tham số là tín hiệu lớp làm quá nhiều việc — và đó là thông tin hữu ích, không phải phiền toái.
Tiêm qua trường: cái bẫy
@Service
class TiemTruong {
@Autowired Kho kho; // không final được
}
qua Spring -> kho = KhoDB
new TiemTruong() -> kho = null
Dòng thứ hai là vấn đề. Tạo bằng new — trong test, hoặc do nhầm lẫn — cho một đối tượng trông hợp lệ nhưng NPE ở lần dùng đầu tiên.
Ba vấn đề khác:
Không final được, nên trường thay đổi được và không có bảo đảm nào về an toàn luồng.
Che giấu phụ thuộc. Lớp mười lăm @Autowired trông vẫn gọn, nên không ai nhận ra nó đã quá tải.
Che giấu phụ thuộc vòng. Với tiêm qua hàm khởi tạo, vòng phụ thuộc là lỗi lúc khởi động. Với tiêm qua trường, Spring giải được vòng bằng cách tiêm sau — nên vấn đề thiết kế bị giấu đi.
Spring Boot 2.6 trở đi cấm phụ thuộc vòng theo mặc định, và đó là quyết định đúng.
Tiêm qua setter
@Autowired void setKho(Kho kho) { this.kho = kho; }
Chỉ dùng cho phụ thuộc tuỳ chọn hoặc cần thay lúc chạy. Hiếm khi cần trong mã ứng dụng.
Nhiều cài đặt cùng interface
Đây là tình huống bạn sẽ gặp thường xuyên:
interface Thongbao { }
@Component class Email implements Thongbao { }
@Component class SMS implements Thongbao { }
Tiêm Thongbao sẽ lỗi: expected single matching bean but found 2.
Bốn cách xử lý:
Tiêm tất cả vào List:
GomTatCa(List<Thongbao> ds) { this.ds = ds; }
[Email, SMS, Zalo]
Rất hay khi bạn muốn chạy mọi cài đặt — gửi thông báo qua mọi kênh, chạy mọi bộ kiểm tra. Thêm một @Component mới là nó tự vào danh sách, không sửa mã cũ.
Cũng tiêm được vào Map<String, Thongbao> với khoá là tên bean.
@Qualifier để chọn đích danh:
ChonMot(@Qualifier("zalo") Thongbao t) { }
Zalo
@Primary để đặt mặc định:
@Component @Primary class SMS implements Thongbao { }
SMS
@Primary thắng khi không có @Qualifier. Hữu ích khi có một cài đặt "thường dùng" và vài cái đặc biệt.
@Profile để chọn theo môi trường — bài 6.
Thứ tự ưu tiên: @Qualifier > @Primary > khớp theo tên tham số > lỗi.
Phụ thuộc tuỳ chọn
DichVu(Optional<Cache> cache) { }
DichVu(@Autowired(required = false) Cache cache) { }
Optional là cách rõ ràng hơn. Không có bean Cache thì bạn nhận Optional.empty() thay vì lỗi khởi động.
Đừng dùng @Autowired trên hàm khởi tạo nữa
Mã cũ hay có:
@Autowired
DichVu(Kho kho) { }
Từ Spring 4.3, chú thích này thừa khi chỉ có một hàm khởi tạo. Xoá đi cho gọn.
Chỉ cần khi lớp có nhiều hàm khởi tạo và bạn muốn chỉ định dùng cái nào.
Lombok
@Service
@RequiredArgsConstructor
class DichVu {
private final Kho kho;
private final CongThanhToan ct;
}
@RequiredArgsConstructor sinh hàm khởi tạo cho mọi trường final. Đây là mẫu phổ biến nhất trong dự án Spring hiện đại, và nó giữ nguyên mọi ưu điểm của tiêm qua hàm khởi tạo mà không phải gõ.
Cân nhắc: Lombok là annotation processor, nên nó cần cấu hình IDE và làm dấu vết ngăn xếp khó đọc hơn một chút. Nhiều đội chấp nhận đánh đổi đó; một số đội không.
Thử ba mươi giây
grep -rn '@Autowired' --include='*.java' src/main | grep -v 'void set' | wc -l
Mỗi kết quả nằm trên một trường là một lớp không test được bằng new, và một chỗ phụ thuộc vòng có thể đang ẩn. Đổi sang tiêm qua hàm khởi tạo là việc cơ học, và IDE làm hộ được.
Ngày mai: vòng đời bean và scope — cùng cái bẫy tiêm prototype vào singleton.