Backend

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

SSite Admin
4 tháng 08, 2026 5 phút đọc 197 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 Java — Ngày 58: Optional

Optional là công cụ cho kiểu trả về — không field, không tham số, không bọc collection; chuỗi map/flatMap/filter thay kim tự tháp if; orElse luôn tính tham số còn orElseGet lười, orElseThrow cho “không có là lỗi” — và các anti-pattern isPresent + get, orElse(null).

23 thg 9, 20269 phút9
99 Ngày Spring — Ngày 58: Spring Boot Actuator

Boot 3 mặc định chỉ lộ health; bản đồ endpoint an toàn và nguy hiểm; HealthIndicator tự viết với liveness tách khỏi readiness cho Kubernetes, info từ build và git, metrics Micrometer với tag hữu hạn — và ba lớp khóa: cổng riêng, SecurityFilterChain với EndpointRequest, exclude env/heapdump/threaddump.

23 thg 9, 202610 phút5
So sánh EasyExcel và Apache Fesod: khác biệt thật nằm ở đâu?

Fesod là hậu duệ của EasyExcel, cùng engine SAX/SXSSF. Khác biệt kiểm chứng được: vòng đời dự án, POI 5.2.5 vs 5.5.1, các API mới (numRows, csv(), headerMergeStrategy) và độ bền với file xấu — không phải hiệu năng gấp nhiều lần.

23 thg 9, 20268 phút1