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?

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

Mười hai bước của refresh gom thành ba giai đoạn: chuẩn bị BeanFactory bước 1 đến 4, cửa sổ sửa metadata bước 5 và 6, tạo object và công bố bước 7 đến 12; bước 11 được mở rộng sang bên phải liệt kê toàn bộ việc làm cho mỗi singleton

Giai đoạn 2 (bước 5–6) là cửa sổ can thiệp metadata. Bước 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
0/5 câu đã trả lời
  1. 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ì?
  2. 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().
  3. 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.
  4. 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.
  5. 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()?

Bài tiếp theo: BeanDefinition & BeanFactoryPostProcessor

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

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

Bài tiếp theo

BeanDefinition & BeanFactoryPostProcessor — metadata bean và pha can thiệp trước instantiate