Spring Core & Boot/refresh() — 12 bước biến config thành runtime container
9/41
Bài 9 / 41~12 phútContainer InternalsMiễn phí lượt xem

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 paymentGatewayemailService". 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 @Configuration class đầy đủ, xử lý @Import, @ComponentScan nested, @Bean method. Đâ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 --> R

Bướ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, @PostConstruct né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ý do ApplicationContext chọn eager thay vì lazy như BeanFactory trần. Xem thêm ở BeanFactory vs ApplicationContext.

Mỗi startup error ứng với một bước

Loại lỗiBước xảy raNguyên nhân điển hình
MissingRequiredPropertiesException1Environment thiếu property bắt buộc
BeanDefinitionParsingException2Syntax @Configuration sai, XML malformed
BeanDefinitionOverrideException2 hoặc 5Hai bean cùng name (Boot 2.1+ cấm override mặc định)
BeanCreationException11Constructor/@PostConstruct ném exception
UnsatisfiedDependencyException11Thiếu bean dependency, không resolve được @Autowired
BeanCurrentlyInCreationException11Circular 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:#dbeafe

Hai 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ỗ:

EventKhi nàoBước tương ứng
ContextRefreshedEventNgay khi bước 12 chạy xongBước 12
ApplicationStartedEventSau refresh(), trước runnerSau bước 12
ApplicationReadyEventSau tất cả CommandLineRunner/ApplicationRunnerSau 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.

Dấu hiệu nhận biết lỗi BFPP sớm

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

📚 Source để đọc kèm

Chỉ cần đọc tên method + Javadoc, không cần hiểu hết implementation:

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ên DefaultListableBeanFactory đã được giải phẫu ở bài đó — hai cái map beanDefinitionMap/singletonObjects được lấp đầy bởi chính refresh(). Đọ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úc BeanDefinition và 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() trong AbstractApplicationContext có 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 beanDefinitionMap từ annotation/XML. Bước 11 lấp đầy singletonObjects bằ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 getBean trong 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

Tự kiểm tra
Q1
App 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ì?

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 @ComponentScan scope).
  • Có nhiều bean cùng type nhưng không có @Primary hoặ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.

Q2
Vì 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().

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.

Q3
Mộ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.

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 @Autowired của reportService không được inject — nhận giá trị null.
  • @PostConstruct không được gọi.
  • AOP proxy không được tạo — @Transactional, @Async, @PreAuthorize khô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.

Q4
Sự khác nhau về thời điểm chạy giữa BeanFactoryPostProcessorBeanPostProcessor 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".

Q5
Bạn cần warm up cache sau khi app fully ready. Có 2 lựa chọn: @EventListener(ContextRefreshedEvent.class)@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

Đặt 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