Bean lifecycle phases — 9 giai đoạn từ instantiate đến destroy
Mỗi bean Spring đi qua 9 giai đoạn có thứ tự cố định — từ constructor, populate @Autowired, Aware callbacks, init hooks, đến destroy. Bài này bóc tách cơ chế từng bước, lý giải vì sao thứ tự này không thể đảo, và pitfall null field trong constructor.
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
flowchart TB
A["1. Instantiate<br/>(goi constructor)"]
B["2. Populate properties<br/>(@Autowired field/setter)"]
C["3. Aware callbacks<br/>(BeanNameAware, ApplicationContextAware...)"]
D["4. BPP before-init<br/>(BeanPostProcessor.postProcessBefore)"]
E["5. @PostConstruct"]
F["6. InitializingBean.afterPropertiesSet"]
G["7. Custom init-method"]
H["8. BPP after-init<br/>(BeanPostProcessor.postProcessAfter - AOP proxy wrap day)"]
I["Bean ready - phuc vu request"]
J["9. @PreDestroy + DisposableBean.destroy + custom destroy-method"]
A --> B --> C --> D --> E --> F --> G --> H --> I --> J
style E fill:#d1fae5
style H fill:#fef3c7
style J fill:#fee2e2Bả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 | implement BeanNameAware, ApplicationContextAware... | Bean cần biết tên/context 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ó khoảng 10 interface *Aware mà bean có thể implement để nhận tham chiếu đến các thành phần infrastructure. Spring gọi từng callback theo thứ tự ngay sau khi populate field xong.
| 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. Bước 5-7 — init callbacks: vì sao thứ tự này
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.
@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. Nếu bean có method close() (implements Closeable/AutoCloseable), Spring tự gọi nó tại shutdown — không cần khai báo destroyMethod. Đây là (inferred) mode mặc định từ Spring 4.
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 manage complete lifecycle 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
sequenceDiagram
participant K8s
participant App
participant Tomcat
participant InFlight as In-flight requests
K8s->>App: SIGTERM
App->>Tomcat: stop accepting new connections
Note over Tomcat: 503 cho request moi
Tomcat->>InFlight: wait for completion (max 30s)
InFlight-->>Tomcat: all done
App->>App: @PreDestroy callbacks
App->>K8s: process exit 0Pitfall 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.
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
- Bean trải qua 9 giai đoạn có thứ tự cố định: instantiate → populate →
*Aware→ BPP-before →@PostConstruct→afterPropertiesSet→ init-method → BPP-after → ready, rồi destroy khi shutdown. - Thứ tự này không thể đảo: dependency phải sẵn sàng (bước 2) trước khi init logic chạy (bước 5+). Constructor chạy trước populate — không dùng
@Autowiredfield trong constructor. - 3 cơ chế init (
@PostConstruct,InitializingBean, init-method) tồn tại do lịch sử. Khuyến nghị:@PostConstructcho bean tự viết,init-methodcho bean third-party. - Exception trong
@PostConstruct→ app không khởi động — fail-fast có chủ đích, không phải bug. - 9
*Awareinterface — chỉ dùng cho infrastructure code. Constructor inject thay thế hoàn toàn cho business code. - BPP after-init (bước 8) là nơi AOP proxy được tạo — bài AOP proxy đào sâu cơ chế này.
- Destroy callback chỉ chạy khi SIGTERM (không chạy với SIGKILL). Prototype bean không có destroy — Spring không track.
- Production:
server.shutdown=graceful+ K8sterminationGracePeriodSeconds=60+ readiness probe.
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);
}
}
▸
@Service
public class MyService {
@Autowired private Foo foo;
public MyService() {
System.out.println("ctor: " + foo);
}
@PostConstruct
void init() {
System.out.println("init: " + foo);
}
}In:
ctor: null
init: <Foo bean instance>Vì sao: constructor chạy ở bước 1 (instantiate) — Java buộc gọi new trước, lúc này chưa có instance để set field. @Autowired field được populate ở bước 2 sau khi object đã tồn tại. @PostConstruct chạy ở bước 5 — tất cả field đã được inject đầy đủ.
Bài học: không dùng @Autowired field trong constructor. Chuyển sang constructor injection — mọi dependency có ngay khi constructor chạy.
Q2Vì sao Spring thiết kế thứ tự: populate @Autowired (bước 2) rồi mới @Aware callbacks (bước 3), rồi mới @PostConstruct (bước 5)? Không thể đảo thứ tự được không?▸
@Autowired (bước 2) rồi mới @Aware callbacks (bước 3), rồi mới @PostConstruct (bước 5)? Không thể đảo thứ tự được không?Không thể đảo vì mỗi bước phụ thuộc bước trước:
- Bước 2 phải sau bước 1: Spring cần object đã tồn tại mới set field được.
- Bước 3 nên sau bước 2:
*Awarecallback có thể gọi method dùng dependency đã inject — nếu chạy trước populate, dependency vẫn null. - Bước 5 phải sau bước 2: init logic dùng dependency (
@PostConstruct void init() { this.dependency.doStuff(); }) — dependency phải sẵn sàng trước.
Nếu @PostConstruct chạy trước populate, mọi field @Autowired đều null → NPE tại init. Thứ tự này là invariant của Spring bean lifecycle, được đảm bảo bởi source AbstractAutowireCapableBeanFactory.initializeBean().
Q3Bạn viết @PostConstruct method validate kết nối DB. Method này throw SQLException. App xử lý thế nào? Đây có phải hành vi bug không?▸
@PostConstruct method validate kết nối DB. Method này throw SQLException. App xử lý thế nào? Đây có phải hành vi bug không?App không khởi động được. Spring wrap exception thành BeanCreationException và fail container startup. Đây là hành vi có chủ đích — fail fast.
Lý do: ApplicationContext instantiate eager toàn bộ singleton lúc startup. Nếu bean init fail, container từ chối start thay vì chạy với trạng thái không hợp lệ. Lỗi lộ ra ngay khi deploy, không phải lúc user gửi request đầu tiên.
Nếu muốn graceful: catch exception trong @PostConstruct, log warning, continue. Nhưng đừng swallow lỗi config nghiêm trọng — fail fast giúp phát hiện vấn đề sớm hơn nhiều.
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.▸
@PreDestroy. Liệt kê 3 nguyên nhân khả thi và cách kiểm tra từng cái.- 1. K8s gửi SIGKILL thay SIGTERM: nếu container không thoát trong
terminationGracePeriodSeconds(default 30s), K8s gửi SIGKILL — JVM bị giết ngay, shutdown hook không chạy. Kiểm tra: log "SIGTERM received" ở đầu shutdown. Nếu không thấy log này → SIGKILL. Fix: tăng grace period, đảm bảo app thoát trong timeout. - 2.
server.shutdown=gracefulchưa bật: thiếu config này, Tomcat đóng socket ngay khi nhận SIGTERM nhưng Spring shutdown hook vẫn chạy. Tuy nhiên nếu@PreDestroyphụ thuộc resource đang đóng, có thể throw và log bị mất. Kiểm tra:grep server.shutdown application.yml. - 3. Bean là prototype scope: Spring không track lifecycle prototype,
@PreDestroykhông được gọi tự động. Kiểm tra: xem bean declaration có@Scope("prototype")không.
Q5Bạn có bean third-party CacheCluster không có source code, cần gọi cluster.start() sau khi inject xong và cluster.stop() khi shutdown. Dùng cơ chế nào? Viết code config.▸
CacheCluster không có source code, cần gọi cluster.start() sau khi inject xong và cluster.stop() khi shutdown. Dùng cơ chế nào? Viết code config.Dùng @Bean(initMethod = "start", destroyMethod = "stop") — đây đúng là use case của init-method: bean third-party không annotate được.
@Configuration
public class CacheConfig {
@Bean(initMethod = "start", destroyMethod = "stop")
public CacheCluster cacheCluster() {
return new CacheCluster(/* config */);
}
}Spring gọi cacheCluster.start() tại bước 7 (sau @PostConstruct và afterPropertiesSet), và cacheCluster.stop() khi container shutdown.
Bonus: nếu CacheCluster implements Closeable hoặc AutoCloseable, Spring tự detect close() làm destroy method — không cần khai báo destroyMethod. Đây là (inferred) mode mặc định từ Spring 4.
Bài tiếp theo: AOP proxy — JDK vs CGLIB
Bài này có giúp bạn hiểu bản chất không?
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