Scope quyết định Spring tạo bao nhiêu thực thể của một bean. Bài này về sáu scope và một cái bẫy.

Singleton là mặc định

  singleton: hai lần getBean cùng đối tượng?  true
  prototype: hai lần getBean cùng đối tượng?  false

Mặc định, mỗi bean chỉ có một thực thể cho cả ứng dụng.

Chú ý đây là "một thực thể mỗi container", không phải mẫu Singleton của GoF — không có getInstance() tĩnh, không có hàm khởi tạo private, và bạn vẫn new được bình thường.

Hệ quả đã nói ở bài 3 và đáng nhắc lại: bean singleton phục vụ mọi request đồng thời, nên không được có trạng thái thay đổi được.

Cái bẫy: prototype tiêm vào singleton

@Component @Scope("prototype")
class KhoPrototype { }

@Service
class DichVu {
    private final KhoPrototype kho;
    DichVu(KhoPrototype kho) { this.kho = kho; }
    int lay() { return System.identityHashCode(kho); }
}
  gọi 3 lần, hashCode của prototype được tiêm:
    863366099
    863366099
    863366099

Cùng một đối tượng.

Lý do rất đơn giản khi nghĩ ra: DichVu là singleton, nên hàm khởi tạo của nó chạy đúng một lần. Tại thời điểm đó Spring tạo một KhoPrototype và tiêm vào. Từ đó về sau không có gì kích hoạt việc tạo thêm.

Scope của bean được tiêm bị "nuốt" bởi scope của bean chứa nó. Tiêm prototype vào singleton là bạn có một singleton — và không có cảnh báo nào.

Đây là lỗi rất khó phát hiện vì mã trông hoàn toàn đúng, và nó chỉ lộ ra khi bạn để ý trạng thái bị chia sẻ giữa các request.

Bốn cách chữa

ObjectProvider — cách tôi khuyên:

@Service
class DichVu {
    private final ObjectProvider<KhoPrototype> nhaCungCap;
    DichVu(ObjectProvider<KhoPrototype> p) { this.nhaCungCap = p; }
    void lam() {
        KhoPrototype k = nhaCungCap.getObject();   // MỚI mỗi lần gọi
    }
}

Rõ ràng, không cần proxy, và ObjectProvider còn có getIfAvailable() cho phụ thuộc tuỳ chọn.

@Lookup — Spring ghi đè phương thức bằng CGLib:

@Lookup
protected KhoPrototype tao() { return null; }   // Spring viết lại thân

Hoạt động nhưng khó hiểu với người đọc, và không dùng được với lớp final.

Scoped proxy:

@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
class KhoPrototype { }

Spring tiêm một proxy, và mỗi lời gọi method tạo thực thể mới. Tiện nhưng che giấu việc gì đang xảy ra — người đọc thấy một trường bình thường.

Đừng dùng prototype. Đây là câu trả lời đúng trong đa số trường hợp: nếu bạn cần một đối tượng mới mỗi lần, hãy new nó. Không phải mọi thứ đều cần là bean.

Sáu scope

singleton — mặc định, một thực thể cho container.

prototype — thực thể mới mỗi lần yêu cầu. Chú ý Spring không quản vòng đời của prototype: @PreDestroy không bao giờ chạy. Bạn tự chịu trách nhiệm dọn dẹp.

request — một thực thể mỗi HTTP request.

session — một thực thể mỗi phiên người dùng.

application — một thực thể mỗi ServletContext.

websocket — một thực thể mỗi phiên WebSocket.

Bốn scope cuối chỉ có trong ứng dụng web, và đều cần scoped proxy khi tiêm vào singleton:

@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
class NguoiDungHienTai { }

Nên dùng scope web không

Lời khuyên thẳng: hiếm khi.

request scope nghe hợp lý cho "người dùng hiện tại", nhưng nó tạo ra trạng thái ẩn — phương thức nhận được dữ liệu mà không có tham số nào nói điều đó. Đây đúng vấn đề của ThreadLocal ở bài 78 sê-ri Java, và scope request của Spring cài đặt bằng ThreadLocal nên mang theo mọi cảnh báo ở đó.

Với @Async hay luồng ảo, bean scope request không lan sang luồng khác, và bạn nhận lỗi lúc chạy.

Cách rõ ràng hơn: truyền dữ liệu qua tham số phương thức. Dài hơn vài ký tự, nhưng luồng dữ liệu nhìn thấy được.

@PostConstruct@PreDestroy

Bài 3 đã đo thứ tự. Vài lưu ý thêm:

Đừng làm việc nặng trong @PostConstruct. Nó chạy lúc khởi động, nên mọi thứ chậm ở đây đều cộng vào thời gian khởi động. Kết nối mạng, nạp dữ liệu lớn — nên làm lười hoặc bất đồng bộ.

Đừng dựa vào thứ tự giữa các bean. Spring bảo đảm phụ thuộc của bạn sẵn sàng trước, nhưng không bảo đảm thứ tự giữa hai bean độc lập. Cần thứ tự thì dùng @DependsOn, hoặc tốt hơn là làm cho nó không cần thứ tự.

InitializingBeanDisposableBean là hai interface cũ làm cùng việc. Đừng dùng — chúng buộc lớp của bạn phụ thuộc vào Spring, còn chú thích thì không.

Thử ba mươi giây

grep -rn '@Scope' --include='*.java' src/main

Với mỗi kết quả không phải singleton, kiểm xem nó được tiêm vào đâu. Nếu bean chứa nó là singleton và không có ObjectProvider hay proxy, bạn vừa tìm ra một bean prototype chỉ được tạo một lần.

Ngày mai: cấu hình ứng dụng — application.yml, thứ tự ưu tiên và profile.