refresh() — 12 bước biến config thành runtime container
refresh() là method quan trọng nhất của ApplicationContext. Bài này đi sâu 12 bước trong AbstractApplicationContext.refresh(): từ load BeanDefinition đến eager instantiate singleton, chia 3 giai đoạn rõ ràng. Hiểu cơ chế này là chìa khoá debug mọi startup error — BeanCreationException, NoSuchBeanDefinitionException, BeanCurrentlyInCreationException — vì mỗi loại lỗi ứng với đúng một bước.
TL;DR: Khi bạn gọi SpringApplication.run(), container không tức thì "sẵn sàng" — nó phải trải qua refresh(): một method 12 bước cố định trong AbstractApplicationContext. Ba giai đoạn lần lượt là: chuẩn bị BeanDefinition (bước 1–4), biến đổi metadata qua BFPP/BPP (bước 5–6), rồi instantiate toàn bộ singleton non-lazy (bước 11) và publish ContextRefreshedEvent (bước 12). Bước 11 chiếm 99% thời gian startup — đó là lý do Spring mất 3–15 giây khởi động chứ không phải millisecond. Mỗi loại startup error (BeanCreationException, UnsatisfiedDependencyException, BeanCurrentlyInCreationException) đều ứng với đúng một bước trong chuỗi này.
Bài BeanFactory vs ApplicationContext đã giải thích container là hai cái map (beanDefinitionMap + singletonObjects) và ApplicationContext bọc một DefaultListableBeanFactory. Câu hỏi tiếp theo: hai cái map đó được lấp đầy bằng cơ chế gì, theo thứ tự nào? Đó chính là refresh().
1. Source gốc — đọc method 12 bước
refresh() nằm trong AbstractApplicationContext — class cha chung của mọi implementation (AnnotationConfigApplicationContext, AnnotationConfigServletWebServerApplicationContext, …). Source rút gọn, đã annotate giai đoạn:
// File: org/springframework/context/support/AbstractApplicationContext.java
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// --- GIAI DOAN 1: Chuan bi BeanFactory ---
prepareRefresh(); // 1
ConfigurableListableBeanFactory bf =
obtainFreshBeanFactory(); // 2
prepareBeanFactory(bf); // 3
postProcessBeanFactory(bf); // 4
try {
// --- GIAI DOAN 2: Bien doi metadata ---
invokeBeanFactoryPostProcessors(bf); // 5
registerBeanPostProcessors(bf); // 6
// --- GIAI DOAN 3: Instantiate + publish ---
initMessageSource(); // 7
initApplicationEventMulticaster(); // 8
onRefresh(); // 9
registerListeners(); // 10
finishBeanFactoryInitialization(bf); // 11 -- nang nhat
finishRefresh(); // 12
} catch (BeansException ex) {
destroyBeans();
cancelRefresh(ex);
throw ex;
}
}
}
12 dòng đó là toàn bộ "phép thuật" khởi động container. Phần dưới mổ từng giai đoạn.
2. Giai đoạn 1 — chuẩn bị BeanFactory (bước 1–4)
Giai đoạn này đọc config và xây dựng beanDefinitionMap — sau bước 4, container biết tất cả bean sẽ có, nhưng chưa tạo bất kỳ instance nào.
Bước 1 — prepareRefresh(): xoá cache cũ, validate required environment property (qua getEnvironment().validateRequiredProperties()). Nếu property bắt buộc thiếu, exception ném tại đây — trước khi bất kỳ bean nào được load.
Bước 2 — obtainFreshBeanFactory(): với AnnotationConfigApplicationContext, bước này gọi refreshBeanFactory() — nó scan class có @Configuration/@Component, parse @Bean method, đọc @Import, đọc XML nếu có. Mỗi class/method tìm thấy được convert thành một BeanDefinition và đăng ký vào beanDefinitionMap. Đây là lúc beanDefinitionMap được lấp đầy.
Bước 3 — prepareBeanFactory(bf): cài đặt ClassLoader, BeanExpressionResolver (để SpEL #{...} hoạt động), register ApplicationContextAwareProcessor — một BeanPostProcessor nội bộ để inject ApplicationContext vào bean implement ApplicationContextAware.
Bước 4 — postProcessBeanFactory(bf): hook dành cho subclass. Spring Boot web context dùng bước này để register ServletContextAwareProcessor và scope request/session.
Sau bước 4, beanDefinitionMap đầy đủ. Ví dụ: "có bean tên orderService, class com.olhub.OrderService, scope singleton, cần paymentGateway và emailService". Nhưng singletonObjects vẫn rỗng.
3. Giai đoạn 2 — biến đổi metadata (bước 5–6)
Hai bước này là cửa sổ can thiệp duy nhất vào container trước khi bean được tạo.
Bước 5 — invokeBeanFactoryPostProcessors(bf): chạy tất cả BeanFactoryPostProcessor (BFPP) đã register. BFPP nhận ConfigurableListableBeanFactory và có thể đọc, sửa, thêm BeanDefinition. Hai BFPP quan trọng nhất:
ConfigurationClassPostProcessor— BFPP đầu tiên chạy, parse@Configurationclass đầy đủ, xử lý@Import,@ComponentScannested,@Beanmethod. Đây là nơi@SpringBootApplicationđược "mở ra".PropertySourcesPlaceholderConfigurer— resolve${...}trong@Value, thay placeholder bằng giá trị thực từEnvironment.
Bước 6 — registerBeanPostProcessors(bf): không chạy BeanPostProcessor (BPP) ngay, chỉ đăng ký chúng. BPP sẽ được gọi sau ở bước 11. Hai BPP cốt lõi: AutowiredAnnotationBeanPostProcessor (xử lý @Autowired) và CommonAnnotationBeanPostProcessor (xử lý @PostConstruct/@PreDestroy).
Phân biệt then chốt: BFPP can thiệp vào metadata (chạy ở bước 5, trước khi bất kỳ bean nào được tạo), BPP can thiệp vào instance (đăng ký ở bước 6, chạy ở bước 11 quanh từng bean). Tên gần giống nhau nhưng thời điểm và đối tượng hoàn toàn khác — xem chi tiết ở BeanDefinition & BeanFactoryPostProcessor.
Cơ chế bên dưới
Tại sao bước 11 chiếm 99% startup time?
flowchart LR
subgraph B11["Buoc 11: finishBeanFactoryInitialization"]
direction TB
P["Duyet beanDefinitionMap<br/>tim tat ca singleton non-lazy"]
CB["Voi moi bean:<br/>createBean(name, def)"]
R["Chon constructor<br/>Resolve dependency (getBean de quy)<br/>Inject field/setter<br/>BPP.postProcessBeforeInit<br/>@PostConstruct<br/>BPP.postProcessAfterInit<br/>Dat vao singletonObjects"]
end
P --> CB --> RBước 11 (finishBeanFactoryInitialization) duyệt toàn bộ beanDefinitionMap, tìm mọi bean singleton non-lazy, rồi gọi createBean cho từng bean. Với mỗi bean, luồng đầy đủ là: chọn constructor, đệ quy getBean cho từng dependency (tạo dependency trước nếu chưa có), inject, chạy BPP trước init, chạy @PostConstruct, chạy BPP sau init (ở đây AOP proxy có thể được tạo), cuối cùng đặt vào singletonObjects.
Một app Spring Boot thực tế có thể có 300–600 singleton bean. Mỗi bean cần resolve dependency, chạy @PostConstruct (đôi khi kết nối DB, ping cache), và có thể bị wrap proxy. Tổng cộng đó là lý do startup mất 3–15 giây.
Fail-fast là lựa chọn thiết kế có chủ đích: Bởi vì bước 11 tạo eager toàn bộ singleton ngay lúc khởi động, mọi lỗi cấu hình (thiếu dependency, sai
@Value,@PostConstructném exception) lộ ra ngay khi deploy — không phải lúc user gửi request lúc 2 giờ sáng. Đây là lý doApplicationContextchọn eager thay vì lazy nhưBeanFactorytrần. Xem thêm ở BeanFactory vs ApplicationContext.
Mỗi startup error ứng với một bước
| Loại lỗi | Bước xảy ra | Nguyên nhân điển hình |
|---|---|---|
MissingRequiredPropertiesException | 1 | Environment thiếu property bắt buộc |
BeanDefinitionParsingException | 2 | Syntax @Configuration sai, XML malformed |
BeanDefinitionOverrideException | 2 hoặc 5 | Hai bean cùng name (Boot 2.1+ cấm override mặc định) |
BeanCreationException | 11 | Constructor/@PostConstruct ném exception |
UnsatisfiedDependencyException | 11 | Thiếu bean dependency, không resolve được @Autowired |
BeanCurrentlyInCreationException | 11 | Circular dependency qua constructor injection |
Khi thấy stack trace khởi động, dòng đầu tiên trong trace chứa tên class lỗi — map nó vào bảng trên để biết đang ở bước nào. Từ đó thu hẹp nguyên nhân nhanh hơn nhiều so với đọc toàn bộ log.
Flow tổng 12 bước
flowchart TB
Run["SpringApplication.run()"]
S1["1. prepareRefresh<br/>validate env property"]
S2["2. obtainFreshBeanFactory<br/>scan + load BeanDefinition"]
S3["3. prepareBeanFactory<br/>set ClassLoader, SpEL, Aware"]
S4["4. postProcessBeanFactory<br/>hook subclass (web scope...)"]
S5["5. invokeBFPP<br/>BFPP modify definition"]
S6["6. registerBPP<br/>dang ky BPP (chua chay)"]
S7["7. initMessageSource<br/>i18n bean"]
S8["8. initEventMulticaster<br/>event bus"]
S9["9. onRefresh<br/>Boot: start Tomcat o day"]
S10["10. registerListeners<br/>dang ky ApplicationListener"]
S11["11. finishBFInit<br/>instantiate moi singleton non-lazy<br/>(99% startup time)"]
S12["12. finishRefresh<br/>publish ContextRefreshedEvent"]
Done["Container san sang"]
Run --> S1 --> S2 --> S3 --> S4 --> S5 --> S6
S6 --> S7 --> S8 --> S9 --> S10 --> S11 --> S12 --> Done
style S11 fill:#fef3c7
style S5 fill:#dbeafe
style S6 fill:#dbeafeHai bước màu xanh (5–6) là cửa sổ can thiệp metadata. Bước màu vàng (11) là bước nặng nhất.
4. Application events trong vòng đời refresh
Spring publish một số event trong và sau refresh(). Hiểu thứ tự giúp bạn đặt logic "after startup" đúng chỗ:
| Event | Khi nào | Bước tương ứng |
|---|---|---|
ContextRefreshedEvent | Ngay khi bước 12 chạy xong | Bước 12 |
ApplicationStartedEvent | Sau refresh(), trước runner | Sau bước 12 |
ApplicationReadyEvent | Sau tất cả CommandLineRunner/ApplicationRunner | Sau bước 12 |
ContextRefreshedEvent là sớm nhất — container đã ready nhưng chưa chạy runner. Trong test dùng @DirtiesContext, event này có thể fire nhiều lần. ApplicationReadyEvent là muộn nhất và đáng tin cậy nhất cho "after app start" logic:
@Component
public class CacheWarmer {
@EventListener(ApplicationReadyEvent.class)
public void warmCache(ApplicationReadyEvent event) {
// An toan: moi bean da init, Tomcat da listen port
// App ready fire 1 lan duy nhat per instance
log.info("Warming up cache...");
}
}
5. Pitfall — gọi getBean bên trong BFPP
Đây là lỗi phổ biến nhất khi viết BeanFactoryPostProcessor custom:
// SAI -- goi getBean trong BFPP
@Component
public class MyBFPP implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory bf) {
BeanDefinition def = bf.getBeanDefinition("orderService");
def.setLazyInit(true);
// Sai: goi getBean de lay bean khac
SomeService svc = (SomeService) bf.getBean("someService"); // DUNG getBean
}
}
BFPP chạy ở bước 5 — tại thời điểm này singletonObjects gần như rỗng (chưa qua bước 11). Gọi getBean("someService") tại đây ép container phải tạo someService sớm, trước khi tất cả BPP được đăng ký (bước 6 chưa chạy). Hệ quả: bean someService được tạo thiếu BPP — không có @Autowired, không có AOP proxy, không có @PostConstruct. Bean hoạt động không đúng, và lỗi này rất khó debug vì bean trông "có vẻ tồn tại".
// DUNG -- chi modify metadata, khong getBean
@Component
public class MyBFPP implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory bf) {
BeanDefinition def = bf.getBeanDefinition("orderService");
def.setLazyInit(true);
// Chi doc/sua BeanDefinition -- khong tao bean
}
}
Quy tắc: BFPP chỉ được đọc và sửa BeanDefinition, không được tạo bean bằng getBean. Nếu cần truy cập bean khác, dùng BeanPostProcessor hoặc ApplicationListener<ContextRefreshedEvent> — lúc đó bước 11 đã xong.
Khi thấy một bean bị inject null mặc dù bean đó tồn tại, hoặc @Transactional/@Async không có hiệu lực trên một bean cụ thể — nghi ngờ bean đó được khởi tạo sớm trong BFPP (trước bước 6), nên thiếu AOP proxy.
📚 Deep Dive
Chỉ cần đọc tên method + Javadoc, không cần hiểu hết implementation:
AbstractApplicationContext.refresh()— searchpublic void refresh(). Đây là 12 dòng source đứng sau mọi startup Spring.AbstractApplicationContext.finishBeanFactoryInitialization()— đọc đoạn cuối: gọibeanFactory.preInstantiateSingletons()— đây là nơi bước 11 thực sự xảy ra.DefaultListableBeanFactory.preInstantiateSingletons()— vòng lặp quabeanDefinitionNames, skip lazy, gọigetBean(beanName)cho từng cái. Cực kỳ ngắn và rõ ràng.- Spring Reference — Container Extension Points — mô tả BFPP và BPP chính thức, với Javadoc contract và ví dụ.
Liên hệ các bài khác
Bài này là mảnh trung tâm — nối với toàn bộ module container:
- BeanFactory vs ApplicationContext:
refresh()chạy trênDefaultListableBeanFactoryđã được giải phẫu ở bài đó — hai cái mapbeanDefinitionMap/singletonObjectsđược lấp đầy bởi chínhrefresh(). Đọc bài đó trước để hiểu data structure, đọc bài này để hiểu quá trình điền dữ liệu. - BeanDefinition & BeanFactoryPostProcessor: bước 2 tạo ra
BeanDefinition, bước 5 cho BFPP can thiệp vào chúng. Bài đó đào sâu cấu trúcBeanDefinitionvà tất cả field có thể sửa. - Bean lifecycle: bước 11 (
createBean) chỉ là điểm vào — bên trong nó là vòng đời đầy đủ của một bean (instantiate → inject → BPP before init →@PostConstruct→ BPP after init → proxy wrap). Bài đó mổ chi tiết từng callback.
Tóm tắt
refresh()trongAbstractApplicationContextcó 12 bước cố định, chia 3 giai đoạn: chuẩn bị BeanFactory (1–4) → biến đổi metadata BFPP/BPP (5–6) → instantiate + publish (7–12).- Bước 2 lấp đầy
beanDefinitionMaptừ annotation/XML. Bước 11 lấp đầysingletonObjectsbằng cách tạo thực sự từng singleton. - Bước 11 (
finishBeanFactoryInitialization) chiếm 99% startup time — nơi tất cả singleton được tạo, inject, proxy wrap. - Eager instantiate ở bước 11 = fail-fast: lỗi cấu hình lộ ra ngay lúc deploy.
- BFPP (bước 5) can thiệp vào metadata; BPP (đăng ký bước 6, chạy bước 11) can thiệp vào instance.
- Gọi
getBeantrong BFPP là pitfall nghiêm trọng — bean được tạo thiếu BPP, thiếu proxy. - Mỗi loại startup error (
BeanCreationException,UnsatisfiedDependencyException,BeanCurrentlyInCreationException) ứng với đúng một bước — dùng bảng map để debug nhanh.
Tự kiểm tra
Q1App Spring Boot của bạn throw UnsatisfiedDependencyException lúc startup. Dựa vào 12 bước refresh(), lỗi này xảy ra ở bước nào? Nguyên nhân điển hình là gì?▸
UnsatisfiedDependencyException lúc startup. Dựa vào 12 bước refresh(), lỗi này xảy ra ở bước nào? Nguyên nhân điển hình là gì?Lỗi xảy ra ở bước 11 — finishBeanFactoryInitialization. Đây là bước instantiate tất cả singleton non-lazy. Khi container cố tạo bean A, nó cần resolve dependency của A bằng cách gọi getBean(depName). Nếu không tìm thấy bean phù hợp (không có bean match type, có nhiều bean cùng type mà không có qualifier, hoặc bean đó tự thân cũng fail), container ném UnsatisfiedDependencyException.
Nguyên nhân thường gặp:
- Bean dependency chưa được scan (class thiếu
@Component/@Service, hoặc nằm ngoài@ComponentScanscope). - Có nhiều bean cùng type nhưng không có
@Primaryhoặc@Qualifierđể phân biệt. - Bean dependency bản thân cũng fail instantiate (chuỗi lỗi nested).
Cách đọc stack trace: tìm dòng required a bean of type '...' that could not be found — đó là dependency bị thiếu. Kiểm tra class đó có @Component và nằm trong package được scan không.
Q2Vì sao Spring Boot mất 5–15 giây khởi động trong khi một plain Java app main() khởi động ngay lập tức? Giải thích theo cơ chế bước 11 của refresh().▸
main() khởi động ngay lập tức? Giải thích theo cơ chế bước 11 của refresh().Plain Java app chỉ chạy main() — không có container, không có bean nào được tạo sẵn. Spring Boot phải chạy refresh() và tại bước 11, container tạo eager tất cả singleton non-lazy.
Với một app Spring Boot thực tế, đó có thể là 200–600 bean. Với mỗi bean, container phải: resolve dependency (đệ quy getBean), gọi constructor, inject field, chạy BPP trước/sau init (trong đó có tạo AOP proxy), gọi @PostConstruct. Một số @PostConstruct thực sự kết nối DB, ping Redis, load schema — những thao tác I/O này cộng dồn nhanh.
Đây là đánh đổi có chủ đích: startup chậm hơn để đổi lấy fail-fast (lỗi cấu hình lộ ngay lúc deploy) và request đầu tiên nhanh (không phải tạo bean lần đầu). Spring Boot 3.3+ có CDS (Class Data Sharing) giúp giảm bước 11 xuống khoảng 50% bằng cách cache bytecode đã parse.
Q3Một đồng nghiệp viết BeanFactoryPostProcessor và gọi beanFactory.getBean("reportService") bên trong postProcessBeanFactory(). Hậu quả tiềm ẩn là gì? Giải thích theo thứ tự bước 5 và bước 6.▸
BeanFactoryPostProcessor và gọi beanFactory.getBean("reportService") bên trong postProcessBeanFactory(). Hậu quả tiềm ẩn là gì? Giải thích theo thứ tự bước 5 và bước 6.BFPP chạy ở bước 5. Tại thời điểm đó, bước 6 — registerBeanPostProcessors — chưa chạy, nghĩa là tất cả BPP (trong đó có AutowiredAnnotationBeanPostProcessor, CommonAnnotationBeanPostProcessor, AnnotationAwareAspectJAutoProxyCreator) chưa được đăng ký.
Khi getBean("reportService") được gọi ở bước 5, container tạo reportService mà không có BPP nào. Hậu quả:
- Các field
@AutowiredcủareportServicekhông được inject — nhận giá trịnull. @PostConstructkhông được gọi.- AOP proxy không được tạo —
@Transactional,@Async,@PreAuthorizekhông hoạt động trên bean đó.
Bean vẫn tồn tại trong singletonObjects, nên lần sau gọi getBean("reportService") trả về đúng object này (đã broken). Lỗi cực khó debug vì không có exception — chỉ thấy behavior sai (null, transaction không rollback, security bypass).
Fix: chỉ đọc/sửa BeanDefinition trong BFPP. Nếu cần truy cập bean khác, dùng @EventListener(ContextRefreshedEvent.class) — lúc đó bước 11 đã xong.
Q4Sự khác nhau về thời điểm chạy giữa BeanFactoryPostProcessor và BeanPostProcessor là gì? Cho một ví dụ cụ thể từ Spring core cho mỗi loại.▸
BeanFactoryPostProcessor và BeanPostProcessor là gì? Cho một ví dụ cụ thể từ Spring core cho mỗi loại.BFPP — bước 5: chạy trước khi bất kỳ bean nào được instantiate. Nhận ConfigurableListableBeanFactory, có thể đọc và sửa BeanDefinition. Đối tượng tác động: metadata.
BPP — đăng ký bước 6, chạy bước 11: chạy quanh từng bean khi bean được tạo — postProcessBeforeInitialization trước @PostConstruct, postProcessAfterInitialization sau. Đối tượng tác động: instance.
Ví dụ từ Spring core:
- BFPP:
PropertySourcesPlaceholderConfigurer— scan tất cảBeanDefinition, tìm@Value("${...}"), replace placeholder bằng giá trị từEnvironment. Làm việc trên metadata, không tạo instance. - BPP:
AnnotationAwareAspectJAutoProxyCreator— sau khi bean được tạo (postProcessAfterInitialization), kiểm tra xem có aspect nào match không; nếu có, wrap bean trong CGLIB/JDK proxy. Làm việc trên instance.
Memo đơn giản: BFPP = "chỉnh bản vẽ trước khi xây", BPP = "sửa nhà sau khi xây xong".
Q5Bạn cần warm up cache sau khi app fully ready. Có 2 lựa chọn: @EventListener(ContextRefreshedEvent.class) và @EventListener(ApplicationReadyEvent.class). Cái nào an toàn hơn, vì sao — đặc biệt liên quan đến bước 12 của refresh()?▸
@EventListener(ContextRefreshedEvent.class) và @EventListener(ApplicationReadyEvent.class). Cái nào an toàn hơn, vì sao — đặc biệt liên quan đến bước 12 của refresh()?ApplicationReadyEvent an toàn hơn cho warm-up cache.
ContextRefreshedEvent được publish tại bước 12 của refresh() — ngay khi container xong, nhưng với Spring Boot, embedded server (Tomcat) chưa chắc đã bind port xong, và các CommandLineRunner/ApplicationRunner chưa chạy. Nếu logic warm cache phụ thuộc vào port đã mở hoặc runner đã chạy, dùng event này có thể gặp race condition.
Ngoài ra, ContextRefreshedEvent có thể fire nhiều lần trong một process nếu dùng parent-child context hoặc @DirtiesContext trong test — warm cache nhiều lần có thể gây bug.
ApplicationReadyEvent fire một lần duy nhất, sau khi toàn bộ runner đã xong và server đã listen. Đây là điểm "app chính thức sống" — safe cho bất kỳ "after start" logic nào. Chọn ApplicationReadyEvent trừ khi bạn có lý do cụ thể cần sớm hơn.
Bài tiếp theo: BeanDefinition & BeanFactoryPostProcessor
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