Vòng Đời Khởi Động: Giải Mã 12 Bước Trong refresh()
Mổ xẻ 12 bước trong refresh() của AbstractApplicationContext, phân biệt BFPP và BPP, ánh xạ lỗi startup vào từng bước cụ thể.
TL;DR: Khi thực thi SpringApplication.run(), container nạp toàn bộ cấu hình thông qua phương thức cốt lõi refresh() gồm 12 bước tuần tự trong AbstractApplicationContext. Chu trình này được phân thành 3 giai đoạn: Chuẩn bị BeanFactory và nạp siêu dữ liệu (bước 1–4), Biến đổi siêu dữ liệu qua BFPP và đăng ký BPP (bước 5–6), Cấp phát toàn bộ bean Singleton non-lazy và phát sự kiện (bước 7–12). Bước 11 chiếm 99% thời gian khởi động; nắm vững 12 bước này giúp bạn định vị chính xác nguyên nhân của mọi ngoại lệ startup.
Trong bài BeanFactory vs ApplicationContext: Bản Chất Hai Container, chúng ta đã biết container duy trì hai bản đồ chính: beanDefinitionMap (siêu dữ liệu) và singletonObjects (instance thực tế). Bài học này sẽ giải mã câu hỏi then chốt: Quá trình lấp đầy hai bản đồ này diễn ra theo trình tự 12 bước nào?
Phương thức refresh() trong AbstractApplicationContext gồm những bước nào?
Toàn bộ quá trình khởi động của Spring ApplicationContext được đồng bộ hóa thông qua một phương thức duy nhất:
// File: org/springframework/context/support/AbstractApplicationContext.java
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// --- PHASE 1: Prepare BeanFactory and load metadata ---
prepareRefresh(); // 1
ConfigurableListableBeanFactory bf = obtainFreshBeanFactory(); // 2
prepareBeanFactory(bf); // 3
postProcessBeanFactory(bf); // 4
try {
// --- PHASE 2: Transform metadata and register extensions ---
invokeBeanFactoryPostProcessors(bf); // 5
registerBeanPostProcessors(bf); // 6
// --- PHASE 3: Instantiate singletons and publish events ---
initMessageSource(); // 7
initApplicationEventMulticaster(); // 8
onRefresh(); // 9
registerListeners(); // 10
finishBeanFactoryInitialization(bf); // 11 (takes 99% startup time)
finishRefresh(); // 12
} catch (BeansException ex) {
destroyBeans();
cancelRefresh(ex);
throw ex;
}
}
}

Giai đoạn 1: Container chuẩn bị BeanFactory và nạp siêu dữ liệu ra sao?
Giai đoạn này tập trung vào việc đọc cấu hình để điền đầy bản đồ beanDefinitionMap (chưa có bất kỳ bean nghiệp vụ nào được tạo ra trên bộ nhớ):
- Bước 1 —
prepareRefresh(): Khởi tạo trạng thái, xác thực các thuộc tính môi trường bắt buộc (validateRequiredProperties()). Nếu thiếu biến cấu hình trọng yếu, lỗi sẽ ném ra ngay lập tức. - Bước 2 —
obtainFreshBeanFactory(): Quét các class mang chú thích@Configuration,@Component, đọc XML/Java config và chuyển đổi thành các thực thểBeanDefinitionlưu vàobeanDefinitionMap. - Bước 3 —
prepareBeanFactory(bf): Cài đặt ClassLoader, SpEL resolver (#{...}), và đăng kýApplicationContextAwareProcessorđể hỗ trợ tiêm context vào bean. - Bước 4 —
postProcessBeanFactory(bf): Hook cho các lớp con mở rộng (ví dụ Spring Boot thêm các scope cho ứng dụng Web nhưrequesthaysession).
Giai đoạn 2: Cơ chế biến đổi siêu dữ liệu của BFPP và BPP hoạt động như thế nào?
Đây là cửa sổ can thiệp duy nhất vào siêu dữ liệu trước khi các bean thực sự được sinh ra:
- Bước 5 —
invokeBeanFactoryPostProcessors(bf): Kích hoạt toàn bộBeanFactoryPostProcessor(BFPP). Tiêu biểu làConfigurationClassPostProcessor(phân tích sâu@ComponentScan,@Import) vàPropertySourcesPlaceholderConfigurer(giải mã${...}trong@Value). BFPP có toàn quyền sửa đổi cấu hình trongBeanDefinition. - Bước 6 —
registerBeanPostProcessors(bf): Tìm kiếm và đăng ký cácBeanPostProcessor(BPP) vào danh sách quản lý của container (nhưAutowiredAnnotationBeanPostProcessorxử lý@Autowired). Lưu ý: Các BPP chỉ được đăng ký chứ chưa chạy ở bước này.
BFPP tác động lên siêu dữ liệu (bản vẽ kỹ thuật) ở bước 5 trước khi có object. BPP tác động lên instance thực tế (ngôi nhà đã xây) quanh thời điểm tạo bean ở bước 11.
Tại sao bước 11 chiếm tới 99% thời gian khởi động của ứng dụng?
Tại Bước 11 (finishBeanFactoryInitialization), container duyệt qua toàn bộ beanDefinitionMap, lọc ra tất cả các bean Singleton không đánh dấu @Lazy, và kích hoạt chuỗi xử lý tạo thực thể:
- Phân giải đệ quy và khởi tạo các bean phụ thuộc (Dependency Resolution).
- Gọi Constructor phù hợp thông qua Reflection.
- Chạy BPP tiền khởi tạo (
postProcessBeforeInitialization). - Kích hoạt callback khởi tạo
@PostConstructhoặcInitializingBean.afterPropertiesSet(). - Chạy BPP hậu khởi tạo (
postProcessAfterInitialization) — đây là nơi Dynamic Proxy của AOP (@Transactional, @Async) được bọc ngoài bean. - Lưu bean hoàn chỉnh vào bảng băm
singletonObjects.
Với một ứng dụng Spring Boot doanh nghiệp chứa từ 300 đến 600 bean cùng nhiều thao tác kết nối cơ sở dữ liệu hay kiểm tra hạ tầng lúc khởi động, bước 11 chiếm gần như trọn vẹn thời gian khởi động ứng dụng.
Các sự cố khởi động (Startup Errors) ánh xạ tương ứng vào những bước nào?
Khi ứng dụng gặp sự cố không thể khởi động, việc đối chiếu lớp ngoại lệ đầu tiên trong stack trace với các bước của refresh() sẽ giúp khoanh vùng lỗi trong vài giây:
| Ngoại lệ phát sinh | Bước tương ứng | Nguyên nhân cốt lõi |
|---|---|---|
MissingRequiredPropertiesException | Bước 1 | Biến môi trường bắt buộc chưa được cấu hình |
BeanDefinitionParsingException | Bước 2 | Lỗi cú pháp trong class @Configuration hoặc file XML |
BeanDefinitionOverrideException | Bước 2 hoặc 5 | Hai bean bị trùng tên định danh trong context |
UnsatisfiedDependencyException | Bước 11 | Thiếu bean phụ thuộc hoặc có nhiều bean gây xung đột |
BeanCreationException | Bước 11 | Hàm Constructor hoặc @PostConstruct ném lỗi logic |
BeanCurrentlyInCreationException | Bước 11 | Xảy ra phụ thuộc vòng giữa các constructor |
Sự kiện Application Event nào an toàn nhất để thực thi tác vụ sau khởi động?
Spring phát đi nhiều sự kiện trong và sau chu trình refresh():
refresh() hoàn tất (Bước 12)
└─ ContextRefreshedEvent (Container xong, nhưng server chưa chắc đã sẵn sàng)
└─ ApplicationStartedEvent (Trước khi CommandLineRunner chạy)
└─ ApplicationReadyEvent (Sau khi mọi Runner hoàn tất, Port đã mở)
Sử dụng ApplicationReadyEvent là giải pháp an toàn và đáng tin cậy nhất cho các tác vụ khởi chạy (như nạp bộ nhớ đệm cache hoặc gửi tín hiệu sẵn sàng):
@Component
public class CacheWarmer {
@EventListener(ApplicationReadyEvent.class)
public void onReady() {
// Safe: all beans initialized and server listening on port
log.info("System is ready. Warming up cache...");
}
}
Tại sao gọi getBean() bên trong BeanFactoryPostProcessor là cạm bẫy nguy hiểm?
Nếu trong phương thức postProcessBeanFactory của một BFPP tùy biến, bạn vô tình gọi bf.getBean("someService"):
@Component
public class BadBFPP implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory bf) {
// Anti-pattern: Calling getBean during Phase 2
SomeService svc = (SomeService) bf.getBean("someService");
}
}
Tại bước 5, danh sách BPP chưa hề được đăng ký (bước 6 chưa chạy). Lệnh gọi getBean() ép container phải khởi tạo someService sớm. Hậu quả là bean này bị tước bỏ toàn bộ các bộ xử lý BPP:
- Các trường
@Autowiredkhông được tiêm giá trị (null). - Phương thức
@PostConstructkhông được kích hoạt. - Không được bọc AOP Proxy, khiến
@Transactionalhay@Asynchoàn toàn vô hiệu.
Quy tắc bắt buộc: BFPP chỉ được phép đọc và biến đổi BeanDefinition, tuyệt đối không được gọi getBean().
Tóm tắt
- Phương thức
refresh()điều phối 12 bước khởi tạo container qua 3 giai đoạn chặt chẽ. - Giai đoạn 1 nạp siêu dữ liệu vào
beanDefinitionMap; Giai đoạn 2 cho phép BFPP tinh chỉnh siêu dữ liệu; Giai đoạn 3 cấp phát bean. - Bước 11 tạo toàn bộ Singleton bean non-lazy và thực thi nguyên lý Fail-Fast để bảo vệ hệ thống.
- Hãy lắng nghe sự kiện
ApplicationReadyEventđể thực thi logic sau khởi động nhằm tránh hiện tượng chạy sớm khi server chưa sẵn sàng.
Tự kiểm tra
- Q1Lỗi UnsatisfiedDependencyException thường phát sinh ở bước nào trong chu trình 12 bước của phương thức refresh()?
- Q2Hậu quả nghiêm trọng nhất khi gọi getBean() bên trong phương thức của một BeanFactoryPostProcessor (BFPP) là gì?
- Q3Khác biệt căn bản giữa BeanFactoryPostProcessor (BFPP) và BeanPostProcessor (BPP) về thời điểm và đối tượng tác động là gì?
- Q4Tại sao bước 11 (finishBeanFactoryInitialization) lại chiếm tới 99% tổng thời gian khởi động của ứng dụng Spring?
- Q5Để thực hiện một tác vụ sau khi toàn bộ ứng dụng Spring Boot đã khởi động hoàn tất và máy chủ sẵn sàng nhận request, sự kiện nào là an toàn nhất?
Bài tiếp theo: BeanDefinition & BFPP: Thao Tác Siêu Dữ Liệu Trước Khi Tạo Bean
Bài này có giúp bạn hiểu bản chất không?
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.
BeanFactory vs ApplicationContext: Bản Chất Hai Container
Bài tiếpBeanDefinition & BFPP: Thao Tác Siêu Dữ Liệu Trước Khi Tạo Bean
Ô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.
Thảo luận & góp ý
Hỏi đáp về bài này