Backend

99 Ngày Spring — Ngày 07: Bean scope & lifecycle

SSite Admin
4 tháng 08, 2026 5 phút đọc 95 lượt xem
99 Ngày Spring — Ngày 07: Bean scope & lifecycle

Sáu ngày qua ta nhắc đi nhắc lại chữ singleton như một điều hiển nhiên — hôm nay trả nợ: scope quyết định container tạo bao nhiêu instance cho một bean và chúng sống bao lâu. Kèm theo là vòng đời của bean — móc @PostConstruct/@PreDestroy để chen việc của mình vào lúc sinh và lúc mất — và @Lazy cho những bean nặng nề chưa cần vội.

Sketchnote Ngày 07: bean scope và lifecycle trong Spring

Scope — một bean, bao nhiêu instance?

@Component            // mặc định: SINGLETON — cả app dùng chung 1 instancepublic class CounterService { }

@Component@Scope("prototype")   // PROTOTYPE — MỖI lần xin container là một instance MỚIpublic class ShoppingCart { }

// Web scope (cần ứng dụng web):// @RequestScope  — mỗi HTTP request một instance// @SessionScope  — mỗi phiên người dùng một instance
  • singleton (mặc định): container tạo đúng một instance lúc khởi động, ai xin cũng nhận nó — tiết kiệm và đủ dùng cho service/repository không giữ state.

  • prototype: mỗi lời gọi getBean() / mỗi điểm tiêm là một instance mới — cho object có state riêng theo người dùng.

  • request / session (ứng dụng web): instance sống theo một HTTP request / một phiên — sẽ gặp lại ở giai đoạn REST API.

Con số nói thay: một app thực tế có hàng trăm bean singleton, prototype đếm trên đầu ngón tay. Mặc định cứ để singleton — chỉ đổi khi có lý do rõ ràng.

Cái bẫy lớn nhất: state trong singleton

@Service   // singleton — MỘT instance phục vụ MỌI request đồng thờipublic class OrderService {

    private int soDonHang = 0;          // ⚠️ state ghi được trong singleton!

    public void datHang() {
        soDonHang++;                    // nhiều request cùng ghi → race condition
    }
}
// Quy tắc vàng: bean singleton nên STATELESS —// field chỉ giữ dependency (final), dữ liệu đi theo tham số & biến cục bộ

Một instance phục vụ mọi request cùng lúc nghĩa là field ghi được trở thành tài nguyên tranh chấp: hai request cùng soDonHang++ có thể mất một lượt đếm. Quy tắc vàng: bean singleton stateless — field chỉ giữ dependency final (đúng kiểu constructor injection Ngày 04), dữ liệu chảy qua tham số và biến cục bộ. Đây cũng là câu phỏng vấn kinh điển: "Singleton bean có thread-safe không?" — không, Spring chỉ bảo đảm một instance, thread-safety là việc của bạn.

Còn một bẫy tinh vi: tiêm prototype vào singleton — prototype chỉ được tạo một lần lúc tiêm, rồi "đóng băng" trong singleton, thành ra cũng như singleton. Gặp ca này cần ObjectProvider để xin instance mới mỗi lần dùng — biết tên giải pháp là đủ cho hôm nay.

Vòng đời — chen việc vào lúc sinh, lúc mất

Hành trình một bean: container tạo instancetiêm dependency@PostConstructphục vụ → (container đóng) → @PreDestroy → kết thúc. Hai móc đó là chỗ đặt code khởi tạo và dọn dẹp:

@Componentpublic class CacheManager {

    private final Map<String, String> cache = new HashMap<>();

    @PostConstruct        // chạy SAU khi bean tạo xong & tiêm đủ dependency
    public void warmUp() {
        cache.put("config", taiTuDatabase());   // nạp sẵn dữ liệu
    }

    @PreDestroy           // chạy TRƯỚC khi container đóng — dọn dẹp
    public void flush() {
        ghiXuongDisk(cache);
    }
}
  • Vì sao không làm trong constructor? Lúc constructor chạy, dependency có thể chưa tiêm đủ@PostConstruct bảo đảm bean đã "đủ đồ nghề".

  • @PreDestroy chạy khi container đóng êm đềm (Ctrl+C, close()) — kill cứng thì không kịp chạy.

  • Lưu ý: bean prototype container tạo xong là "buông tay" — @PreDestroy không được gọi cho chúng.

@Lazy — khoan hãy tạo, chờ dùng mới tạo

Singleton mặc định là eager: tất cả được tạo lúc khởi động — lỗi cấu hình lộ ngay từ đầu, đổi lại app nặng khởi động chậm. Bean nào nặng và hiếm dùng thì đánh dấu @Lazy:

@Component@Lazy                 // đừng tạo lúc khởi động — chờ tới lần dùng ĐẦU TIÊNpublic class HeavyReportEngine {
    public HeavyReportEngine() {
        // nạp model 2GB — chậm 10 giây
    }
}

@Servicepublic class ReportService {
    private final HeavyReportEngine engine;

    // Nơi tiêm cũng phải @Lazy — không thì engine vẫn bị tạo sớm
    public ReportService(@Lazy HeavyReportEngine engine) {
        this.engine = engine;    // nhận proxy; engine thật tạo khi gọi method
    }
}

Đánh đổi công bằng: khởi động nhanh hơn, nhưng lỗi khởi tạo lộ muộn — thay vì chết ngay lúc boot, app chết ở lần dùng đầu tiên. Vì thế đừng rải @Lazy khắp nơi; eager-by-default chính là một tính năng an toàn của Spring.

Bài tập nhỏ

  • Tạo bean @Scope("prototype"), gọi getBean() 2 lần, so sánh == — rồi làm lại với singleton.

  • Thêm @PostConstruct@PreDestroy in log vào một bean, chạy app rồi tắt bằng Ctrl+C — quan sát thứ tự log lúc khởi động và lúc đóng.

  • Tái hiện bẫy đếm: field int trong singleton, gọi ++ từ 2 thread chạy song song 10.000 lần — kết quả có đủ 20.000 không?

  • Đánh dấu một bean @Lazy với constructor in log — xác nhận log chỉ hiện ở lần dùng đầu tiên, không phải lúc khởi động.

Kết luận

Giờ chữ "singleton" đã có nội dung thật: scope quyết định số instance, vòng đời có hai móc @PostConstruct/@PreDestroy, singleton phải stateless, và @Lazy dành cho bean nặng hiếm dùng. Ngày 08 ta rời thế giới bean để cấu hình ứng dụng: application.yml, @Value@ConfigurationProperties — đưa những con số hard-code ra khỏi code. 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