Bean lifecycle — 9 giai đoạn từ instantiate đến destroy
9 giai đoạn cố định của một bean Spring: constructor, populate @Autowired, Aware, init hooks, destroy — vì sao thứ tự không thể đảo và pitfall null field.
TL;DR: Mỗi bean Spring đi qua 9 giai đoạn cố định: instantiate → populate @Autowired → các callback *Aware → BeanPostProcessor before-init → @PostConstruct → InitializingBean.afterPropertiesSet → custom init-method → BeanPostProcessor after-init → ready; rồi destroy khi container shutdown. Thứ tự này không thể đảo vì mỗi bước phụ thuộc bước trước: dependency phải sẵn sàng trước khi init logic chạy. Exception trong @PostConstruct khiến app không khởi động được — đây là fail-fast có chủ đích.
Bài refresh() 12 bước chỉ ra bước 11 là "instantiate all singleton". Câu hỏi phóng to: trong bước 11, mỗi bean cụ thể trải qua giai đoạn nào? Khi bạn ghi @PostConstruct, code đó chạy lúc nào so với lúc @Autowired field được set?
1. 9 giai đoạn — sơ đồ tổng

Bảng mapping từng bước:
| # | Giai đoạn | Code/annotation | Khi nào dùng |
|---|---|---|---|
| 1 | Instantiate | constructor | Bean class được new với DI qua constructor |
| 2 | Populate | @Autowired field/setter | Field/setter inject dependency không qua constructor |
| 3 | *Aware callbacks | BeanNameAware, BeanClassLoaderAware, BeanFactoryAware (bước invokeAwareMethods) | Bean cần biết tên/loader/factory của chính mình |
| 4 | BPP before-init | BeanPostProcessor.postProcessBeforeInitialization | Hook trước init |
| 5 | @PostConstruct | annotation method | Init logic dùng dependency đã inject |
| 6 | afterPropertiesSet | InitializingBean interface | (Hiếm) tương đương @PostConstruct, kế thừa từ Spring 1.x |
| 7 | init-method | @Bean(initMethod = "...") | Init khi bean third-party không annotate được |
| 8 | BPP after-init | BeanPostProcessor.postProcessAfterInitialization | AOP proxy wrap ở đây |
| 9 | Destroy | @PreDestroy, DisposableBean, destroy-method | Cleanup khi container shutdown |
2. Bước 1-2 — instantiate và populate
Đây là cặp bước đầu tiên và cũng là nguồn gốc của một trong những pitfall phổ biến nhất khi mới học Spring.
@Service
public class OrderService {
private final PaymentGateway payment; // constructor inject
@Autowired private NotificationClient notif; // field inject
public OrderService(PaymentGateway payment) {
this.payment = payment;
// Tai day: 'payment' da SET, 'notif' van NULL
System.out.println(notif); // null !!
}
}
Constructor chạy ở bước 1 — Java buộc gọi new trước để có instance. Chỉ sau đó Spring mới có chỗ để set field @Autowired. Đây là ràng buộc của Java, không phải design Spring.
Vì sao thứ tự này? Để tạo object, JVM phải cấp phát vùng nhớ và gọi constructor. Không có cách nào set field của một object trước khi object tồn tại. Spring tuân theo chuỗi này: tạo object → sau đó mới inject dependency ngoài constructor.
// SAI — notif chua duoc inject, NPE tai runtime
public OrderService(PaymentGateway payment) {
this.payment = payment;
notif.send("init"); // NullPointerException
}
// DUNG — tat ca dependency qua constructor, co ngay khi constructor chay
public OrderService(PaymentGateway payment, NotificationClient notif) {
this.payment = payment;
this.notif = notif;
notif.send("init"); // OK
}
Đây là một trong nhiều lý do constructor injection được khuyến nghị: tất cả dependency đã có khi constructor chạy, không thể có trạng thái nửa chừng.
3. Bước 3 — *Aware callbacks
Spring có hơn 10 interface *Aware; bảng dưới liệt kê 7 cái hay gặp. Nhưng chúng không chạy cùng một chỗ — đây là điểm nhiều người hiểu sai. Chỉ BeanNameAware, BeanClassLoaderAware, BeanFactoryAware được gọi trong bước invokeAwareMethods() riêng (ngay sau populate, trước mọi BeanPostProcessor). Năm interface còn lại trong bảng — ApplicationContextAware, EnvironmentAware, ResourceLoaderAware, ApplicationEventPublisherAware, EmbeddedValueResolverAware — không chạy ở bước này; chúng được cấp bởi ApplicationContextAwareProcessor, vốn là một BeanPostProcessor chạy ở bước 4 (BPP before-init).
| Interface | Bean nhận được gì | Khi nào hữu ích |
|---|---|---|
BeanNameAware | setBeanName(String) — tên bean trong context | Log/debug khi bean cần biết identity |
BeanFactoryAware | setBeanFactory(BeanFactory) | Hiếm — thường anti-pattern |
ApplicationContextAware | setApplicationContext(ApplicationContext) | Khi cần lookup động (legacy) |
EnvironmentAware | setEnvironment(Environment) | Đọc property trong custom infrastructure code |
ResourceLoaderAware | setResourceLoader(ResourceLoader) | Load resource động |
ApplicationEventPublisherAware | setApplicationEventPublisher(...) | Publish custom event từ infrastructure code |
EmbeddedValueResolverAware | setEmbeddedValueResolver(StringValueResolver) | Resolve ${...} trong custom logic |
Vì sao thứ tự Aware sau populate? Bean cần dependency đã inject đầy đủ trước khi nhận tham chiếu infrastructure — ngược lại, callback có thể gọi method dùng dependency chưa set.
Khuyến nghị: không implement *Aware trong code business. Constructor injection thay thế hoàn toàn:
// THAY VI implement ApplicationContextAware:
@Service
public class GoodService {
private final ApplicationContext ctx;
public GoodService(ApplicationContext ctx) { this.ctx = ctx; }
}
Spring tự deduce 7+ "infrastructure type" có thể inject như bean thường — ApplicationContext, Environment, ApplicationEventPublisher đều inject được qua constructor. *Aware chỉ nên dùng khi viết infrastructure code (custom BeanFactoryPostProcessor, custom scope). 99% application code không cần.
4. Vì sao thứ tự init (bước 5-7) không thể đảo?
3 cơ chế init chạy theo thứ tự cố định:
@PostConstruct— annotation chuẩn JSR-250.InitializingBean.afterPropertiesSet()— interface Spring 1.x.- Custom init-method — khai báo qua
@Bean(initMethod = "init").
Nếu bean có cả 3, cả 3 đều chạy theo thứ tự trên. Vì sao thứ tự init sau populate và Aware?
Nguyên tắc cốt lõi: init logic dùng dependency — dependency phải sẵn sàng trước. Nếu @PostConstruct chạy trước bước 2 (populate), field @Autowired vẫn null, code init sẽ throw NPE. Spring thiết kế thứ tự này để đảm bảo tại thời điểm init, mọi dependency đã sẵn sàng.
Sơ đồ 9 bước là thứ tự quan sát được (đúng theo trình tự người học thấy). Nhưng dưới nguồn, @PostConstruct không phải một phase độc lập: nó được thực thi bởi một BeanPostProcessor — InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization — tức nằm trong pha BPP before-init (bước 4). Chỉ afterPropertiesSet và init-method mới chạy sau khi mọi postProcessBeforeInitialization xong (trong invokeInitMethods). Điểm cần nhớ: BeanPostProcessor chính là cơ chế cấp nhiều callback mà ta quen gọi là "bước riêng" — @PostConstruct và 5 *Aware họ-context (ApplicationContextAware...) đều đi qua một BPP.
@Service
public class CacheWarmer {
private final DataSource ds;
public CacheWarmer(DataSource ds) { this.ds = ds; }
@PostConstruct
public void init() {
// Chay sau khi tat ca dependency da set — ds bao gio cung non-null tai day
try (var conn = ds.getConnection()) {
conn.createStatement().execute("SELECT 1");
}
System.out.println("CacheWarmer initialized");
}
}
@PostConstruct lý tưởng cho:
- Validate cấu hình tại startup (fail fast nếu config sai).
- Pre-load cache, warm up connection pool.
- Kiểm tra connectivity đến dependency bên ngoài.
Cảnh báo — fail fast là mục tiêu, không phải bug: exception từ @PostConstruct khiến app không khởi động được. Đây là hành vi có chủ đích — nếu init logic fail, container từ chối start thay vì chạy với trạng thái sai. Bài BeanFactory vs ApplicationContext giải thích tại sao eager instantiation + fail-fast là lựa chọn thiết kế của ApplicationContext.
3 cơ chế tồn tại song song do lịch sử: Spring 1-2 chỉ có InitializingBean và XML init-method; Spring 2.5+ thêm @PostConstruct (JSR-250). Khuyến nghị 2026: dùng @PostConstruct/@PreDestroy — chuẩn, không lock vào Spring API.
init-method cho bean third-party
Khi bean là class từ thư viện ngoài (không sửa source được), dùng initMethod:
@Configuration
public class CacheConfig {
@Bean(initMethod = "start", destroyMethod = "stop")
public CacheCluster cacheCluster() {
return new CacheCluster(/* config */);
}
}
Spring gọi cacheCluster.start() sau init, cacheCluster.stop() khi shutdown. Với @Bean, nếu không khai báo destroyMethod, Spring tự suy ra ((inferred) mode mặc định): tìm method public tên close hoặc shutdown rồi gọi nó khi shutdown — không cần bean implement Closeable/AutoCloseable, chỉ cần đúng tên method.
5. Bước 9 — destroy callbacks
Khi container shutdown (ctx.close() hoặc JVM nhận SIGTERM với registerShutdownHook), Spring chạy destroy callback theo thứ tự ngược chiều dependency — bean nào được tạo sau thì bị destroy trước.
3 cơ chế destroy (tương tự init, cùng thứ tự):
@PreDestroymethod.DisposableBean.destroy()— interface Spring 1.x.- Custom destroy-method khai báo qua
@Bean(destroyMethod = "...").
@Service
public class ResourceManager {
private ScheduledExecutorService scheduler;
@PostConstruct
public void start() {
scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(this::refresh, 0, 5, TimeUnit.MINUTES);
}
@PreDestroy
public void stop() {
if (scheduler != null) {
scheduler.shutdown();
try {
scheduler.awaitTermination(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
private void refresh() { /* ... */ }
}
Destroy callback chỉ chạy khi container shutdown đúng cách:
- Spring Boot tự register shutdown hook →
kill <pid>(SIGTERM) trigger graceful shutdown. kill -9 <pid>(SIGKILL) — JVM bị giết trực tiếp, không kịp callback.- Crash JVM (OOM, segfault) cũng không trigger.
Prototype scope — ngoại lệ quan trọng: @PreDestroy trên prototype bean không chạy. Spring docs ghi rõ: Spring không quản lý trọn vòng đời của prototype. Container tạo bean, inject dependency, rồi giao lại cho caller — sau đó không track nữa. Caller phải tự cleanup, hoặc dùng BeanFactory.destroyBean(bean) thủ công.
Xem Singleton và Prototype scopes để hiểu thêm tại sao Spring chọn không track destroy cho prototype.
6. SmartLifecycle — kiểm soát thứ tự start/stop
SmartLifecycle là interface Spring cho phép bean kiểm soát thứ tự start và stop khi container khởi động/shutdown:
@Component
public class KafkaConsumer implements SmartLifecycle {
private volatile boolean running;
public void start() {
running = true;
// bat dau consumer thread
}
public void stop() {
running = false;
// dong consumer
}
public boolean isRunning() { return running; }
// Bean voi phase nho duoc start truoc, stop sau
public int getPhase() { return 0; }
public boolean isAutoStartup() { return true; }
// Async stop voi timeout callback
public void stop(Runnable callback) { stop(); callback.run(); }
}
SmartLifecycle tốt hơn @PostConstruct/@PreDestroy cho các trường hợp cần:
- Graceful shutdown sequence: Tomcat (phase cao) stop trước để ngừng nhận request mới; Kafka consumer (phase thấp) stop sau để xử lý xong message đang chạy.
- Resume sau pause: gọi
start()lại saustop()—@PostConstructchỉ chạy 1 lần duy nhất khi bean init.
Spring Boot dùng SmartLifecycle cho web server, scheduling, Kafka, RabbitMQ. Khi debug thấy log "Stopping bean X with phase 2147483646" — đó là Spring đang chạy SmartLifecycle shutdown sequence theo phase.
7. Graceful shutdown production
Production yêu cầu graceful shutdown — nhận SIGTERM, ngừng accept request mới, xử lý nốt in-flight request, rồi mới shutdown. Spring Boot 2.3+ có sẵn:
# application.yml
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s

Pitfall production:
| Vấn đề | Hệ quả |
|---|---|
Không cấu hình server.shutdown=graceful | Tomcat đóng socket ngay, in-flight request bị cắt, user nhận 502 |
K8s terminationGracePeriodSeconds mặc định 30s quá ngắn | K8s gửi SIGKILL khi quá hạn, callback không chạy |
@PreDestroy chạy lâu hơn timeout | State không được flush trước khi process exit |
Best practice K8s:
spec:
terminationGracePeriodSeconds: 60 # > spring.lifecycle.timeout
containers:
- name: app
readinessProbe:
httpGet: { path: /actuator/health/readiness, port: 8080 }
lifecycle:
preStop:
exec:
command: ["sleep", "5"] # cho service mesh propagate endpoint removal
Rule: terminationGracePeriodSeconds phải lớn hơn timeout-per-shutdown-phase cộng thêm buffer ~20-30s để có thời gian cho pre-stop hook và flush log.
8. Bài toán ví dụ — warm-up cache lúc khởi động, flush an toàn khi shutdown
Một yêu cầu thực tế buộc bạn phải chọn đúng giai đoạn vòng đời; chọn sai sẽ NPE lúc start hoặc mất dữ liệu lúc stop.
Đề bài: PriceCacheService giữ bảng giá trong RAM để request đầu tiên không phải chờ 2 giây query DB. Hai ràng buộc:
- Lúc khởi động, nạp sẵn 50.000 dòng giá từ DB vào cache — chỉ nạp được khi
DataSourceđã sẵn sàng. - Khi pod scale-down (SIGTERM), các giá vừa đổi trong RAM (dirty) phải được ghi về DB trước khi connection pool đóng.
Tự hỏi trước khi đọc lời giải: nếu đặt code nạp cache ngay trong constructor thì DataSource đã sẵn sàng chưa? Khi shutdown, PriceCacheService bị huỷ trước hay sau DataSource?
Lời giải:
@Service
public class PriceCacheService {
private final DataSource ds;
private final Map<String, BigDecimal> cache = new ConcurrentHashMap<>();
private final Set<String> dirty = ConcurrentHashMap.newKeySet();
public PriceCacheService(DataSource ds) { // buoc 1-2: constructor inject -> ds san sang ngay
this.ds = ds;
}
@PostConstruct // buoc 5: chay sau populate, ds chac chan non-null
void warmUp() {
// doc 50.000 gia tu DB vao cache
}
@PreDestroy // buoc 9: chay khi SIGTERM
void flushDirty() {
// ghi cac gia dirty ve DB qua ds
}
}
Vì sao đúng:
- Nạp cache trong
@PostConstruct, không phải constructor. Constructor chạy ở bước 1, trước khi field được inject (bước 2). Ở đâydslà constructor inject nên có ngay, nhưng việc nạp dữ liệu vẫn nên để ở@PostConstruct(bước 5) — nơi mọi dependency chắc chắn đã sẵn sàng, tránh phụ thuộc vào kiểu inject. flushDirty()luôn còn pool để ghi.DataSourcelà dependency củaPriceCacheServicenên được tạo trước. Spring huỷ bean theo thứ tự ngược chiều tạo, nênPriceCacheServicebị huỷ trướcDataSource. Khi@PreDestroychạy, pool vẫn mở.- Giới hạn cần biết: nếu pod bị
kill -9(SIGKILL) hoặc OOM,@PreDestroykhông chạy và dirty mất. Với dữ liệu không được phép mất, đừng dựa vào@PreDestroy— ghi ngay (write-through) hoặc dùng store ngoài.
Liên hệ các bài khác
- refresh() 12 bước: bước 11
finishBeanFactoryInitializationgọicreateBeancho từng singleton — 9 giai đoạn trong bài này là nội dung củacreateBean. Hiểu refresh() cho bức tranh container; bài này phóng to vào một bean đơn. - AOP proxy — JDK vs CGLIB: bước 8 BPP after-init là nơi
AnnotationAwareAspectJAutoProxyCreatorwrap bean trong proxy. Bài này chỉ điểm giai đoạn; bài đó đào sâu cơ chế tạo proxy và pitfall self-call@Transactional. - Singleton và Prototype scopes: 9 giai đoạn trên áp dụng đầy đủ cho singleton. Prototype và request scope có biến thể — đặc biệt destroy không được track cho prototype. Bài đó giải thích vì sao Spring chọn thiết kế này.
Tóm tắt
init-methodcho bean third-party (source không tự thêm annotation);@Beantự suy raclose/shutdownmethod khi không khai báodestroyMethod(inferred mode).- Exception trong
@PostConstruct→ app không khởi động — fail-fast có chủ đích: container từ chối start thay vì chạy với trạng thái sai. - BPP after-init (bước 8) là nơi
AnnotationAwareAspectJAutoProxyCreatorwrap bean trong proxy — AOP proxy tạo sau tất cả init callback. - Production:
server.shutdown=graceful+ K8sterminationGracePeriodSecondsphải lớn hơntimeout-per-shutdown-phase+ buffer (~20–30s); SIGKILL không chạy destroy callback.
Tự kiểm tra
- Q1Đoạn sau in gì? Giải thích dựa trên thứ tự 9 giai đoạn.
@Service public class MyService { @Autowired private Foo foo; public MyService() { System.out.println("ctor: " + foo); } @PostConstruct void init() { System.out.println("init: " + foo); } } - Q2Vì sao Spring thiết kế thứ tự: populate
@Autowired(bước 2) rồi mới@Awarecallbacks (bước 3), rồi mới@PostConstruct(bước 5)? Không thể đảo thứ tự được không? - Q3Bạn viết
@PostConstructmethod validate kết nối DB. Method này throwSQLException. App xử lý thế nào? Đây có phải hành vi bug không? - Q4App Spring Boot deploy K8s. DevOps thấy khi pod terminate, log không có dòng "shutdown complete" dù bean có
@PreDestroy. Liệt kê 3 nguyên nhân khả thi và cách kiểm tra từng cái. - Q5Bạn có bean third-party
CacheClusterkhông có source code, cần gọicluster.start()sau khi inject xong vàcluster.stop()khi shutdown. Dùng cơ chế nào? Viết code config.
Bài tiếp theo: AOP proxy — JDK vs CGLIB
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Ôn phỏng vấn
Bài này trả lời được các câu phỏng vấn sau — tự trả lời thử trước khi mở đáp án.
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên