Spring Core & Boot/BeanDefinition & BeanFactoryPostProcessor — metadata bean và pha can thiệp trước instantiate
10/41
Bài 10 / 41~12 phútContainer InternalsMiễn phí lượt xem

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

BeanDefinition là bản mô tả metadata của bean (class, scope, dependencies) — chưa phải object thật. BeanFactoryPostProcessor (BFPP) có thể chỉnh sửa metadata này trước khi container instantiate bất kỳ bean nào. Bài này đào sâu hai khái niệm đó, phân biệt BFPP vs BPP, và giải thích vì sao tách pha metadata khỏi instance là nền móng của autoconfig Spring.

TL;DR: Khi bạn khai báo @Component hay @Bean, container chưa tạo object — nó chỉ ghi một BeanDefinition (metadata: class, scope, dependencies) vào beanDefinitionMap. Object thật chỉ ra đời ở bước 11 của refresh(). Trước bước đó, BeanFactoryPostProcessor (BFPP) có thể chỉnh sửa metadata — đổi scope, resolve ${...} placeholder, đăng ký thêm definition. Phân biệt BFPP với BeanPostProcessor (BPP): BFPP can thiệp definition trước instantiate, BPP can thiệp instance sau instantiate. Hai pha tách biệt này là lý do Spring Boot autoconfig và Spring Cloud có thể tweak bean mà không sửa annotation.

Bài trước (BeanFactory vs ApplicationContext) đã cho thấy DefaultListableBeanFactory giữ hai map: beanDefinitionMap (metadata) và singletonObjects (instance). Bài này đào sâu vào map đầu tiên — BeanDefinition là gì bên trong, ai được đọc/ghi nó, và cơ chế can thiệp trước khi bean ra đời.

1. Kịch bản: bean có behavior bất ngờ

Bạn khai báo @Service trên ReportService với scope mặc định (singleton). Nhưng khi debug, mỗi lần gọi getBean bạn lại nhận instance khác — scope trông như prototype. Không ai trong team sửa annotation. Chuyện gì đã xảy ra?

Câu trả lời: một BeanFactoryPostProcessor đâu đó trong app đã gọi def.setScope("prototype") trên definition của ReportService trước khi container tạo bean. Đây là cơ chế hợp lệ, nhưng cần biết để debug.

Để hiểu tại sao điều này có thể xảy ra, cần biết BeanDefinition là gì.

2. BeanDefinition — bản mô tả metadata của bean

BeanDefinition là interface trong Spring, đại diện cho toàn bộ thông tin container cần để tạo một bean — nhưng bản thân nó không phải bean, không phải object. Đây là điểm dễ nhầm nhất.

Khi container scan thấy:

@Service
@Scope("singleton")
public class OrderService {
    private final PaymentGateway paymentGateway;

    public OrderService(PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }
}

Container tạo một entry trong beanDefinitionMap như sau (pseudo-code):

// Minh hoa noi dung BeanDefinition cho OrderService
BeanDefinition def = new RootBeanDefinition();
def.setBeanClassName("com.olhub.OrderService");
def.setScope("singleton");
def.setLazyInit(false);
// Constructor dependency: can PaymentGateway
def.getConstructorArgumentValues()
   .addGenericArgumentValue(new RuntimeBeanReference("paymentGateway"));

Interface BeanDefinition giữ những thông tin chính:

Thuộc tínhMethodVí dụ giá trị
Tên classgetBeanClassName()"com.olhub.OrderService"
ScopegetScope()"singleton", "prototype"
Lazy initisLazyInit()false
Dependencies khai báogetDependsOn()["paymentGateway"]
Init methodgetInitMethodName()"init"
Destroy methodgetDestroyMethodName()"cleanup"
RolegetRole()ROLE_APPLICATION, ROLE_INFRASTRUCTURE

Điều cốt lõi cần nhớ: BeanDefinition là metadata, không phải instance. Nó tồn tại trong beanDefinitionMap ngay sau khi scan/parse config xong (giai đoạn 1-2 của refresh()). Object OrderService thật sự chỉ ra đời ở bước 11.

flowchart TB
  Scan["@Component scan<br/>@Bean parse"]
  BDMap["beanDefinitionMap<br/>name -> BeanDefinition<br/>(METADATA ONLY)"]
  BFPP["BeanFactoryPostProcessor<br/>modify definition"]
  Inst["finishBeanFactoryInitialization<br/>buoc 11: tao object that"]
  SOMap["singletonObjects<br/>name -> Object INSTANCE"]

  Scan -->|"ghi metadata"| BDMap
  BDMap --> BFPP
  BFPP -->|"definition sau sua"| Inst
  Inst -->|"cache instance"| SOMap

Cơ chế bên dưới — beanDefinitionMap trong DefaultListableBeanFactory

beanDefinitionMap là một LinkedHashMap<String, BeanDefinition> trong DefaultListableBeanFactory. Nó giữ thứ tự đăng ký — quan trọng khi thứ tự tạo bean phụ thuộc khai báo.

Khi container chạy đến bước 2 của refresh() (obtainFreshBeanFactory), mọi definition được load:

  • @ComponentScan → scanner duyệt bytecode, tạo ScannedGenericBeanDefinition cho mỗi class có stereotype annotation.
  • @Bean method → ConfigurationClassParser tạo ConfigurationClassBeanDefinition cho mỗi method.
  • XML → XmlBeanDefinitionReader tạo GenericBeanDefinition.

Tất cả đều lưu vào cùng map beanDefinitionMap với key là bean name. Ở thời điểm này, không có object nào trong heap ngoài metadata struct.

Điểm quan trọng: vì definition là object Java thông thường (implements BeanDefinition), bất kỳ code nào giữ reference đến ConfigurableListableBeanFactory đều có thể đọc và ghi definition. Đây chính là cửa để BeanFactoryPostProcessor can thiệp.

3. BeanFactoryPostProcessor — can thiệp metadata trước instantiate

BeanFactoryPostProcessor là interface chỉ có một method:

@FunctionalInterface
public interface BeanFactoryPostProcessor {
    void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
        throws BeansException;
}

Tham số beanFactory cho phép đọc toàn bộ beanDefinitionMap và ghi lại definition. BFPP chạy ở bước 5 của refresh() (invokeBeanFactoryPostProcessors) — sau khi mọi definition đã được load, trước khi bất kỳ bean nào được instantiate.

Ví dụ viết BFPP tùy chỉnh:

@Component
public class ForceLazyDefinitions implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory bf)
            throws BeansException {
        // Doc toan bo ten bean trong map
        for (String name : bf.getBeanDefinitionNames()) {
            BeanDefinition def = bf.getBeanDefinition(name);
            // Chi su ly bean cua app (bo qua bean ROLE_INFRASTRUCTURE cua Spring)
            if (def.getRole() == BeanDefinition.ROLE_APPLICATION) {
                def.setLazyInit(true); // set lazy cho moi bean application
            }
        }
    }
}

Đây là code hợp lệ — sau khi chạy, mọi application bean sẽ bị lazy init. Container sẽ không tạo chúng tại bước 11, mà chờ đến khi getBean đầu tiên gọi tới.

BFPP nổi bật trong Spring core

BFPPLàm gìKhi nào gặp
ConfigurationClassPostProcessorParse @Configuration class, đăng ký @Bean method vào beanDefinitionMapMọi app Spring Boot
PropertySourcesPlaceholderConfigurerResolve ${...} trong @Value trên definition (trước khi bean ra đời)Khi dùng @Value("${...}")
CustomScopeConfigurerĐăng ký scope tùy chỉnh vào registryKhi cần scope "thread" hay custom
MapperScannerConfigurer (MyBatis)Tạo definition cho mỗi interface @MapperApp dùng MyBatis

ConfigurationClassPostProcessor đặc biệt quan trọng vì nó chạy đầu tiên trong bước 5 — nếu không có nó, @Bean method không được parse → mọi bean khai báo trong @Configuration không tồn tại trong beanDefinitionMap khi bước 11 chạy.

4. BeanPostProcessor — can thiệp instance sau instantiate

Để phân biệt rõ, cần hiểu BeanPostProcessor (BPP) — tên gần giống nhưng hoàn toàn khác vai trò:

public interface BeanPostProcessor {
    // Goi truoc init method (@PostConstruct)
    default Object postProcessBeforeInitialization(Object bean, String beanName) {
        return bean;
    }
    // Goi sau init method — day la cho tao proxy AOP
    default Object postProcessAfterInitialization(Object bean, String beanName) {
        return bean;
    }
}

BPP nhận object instance đã tạo, có thể trả về object khác (thường là proxy). BPP đăng ký ở bước 6 của refresh(), nhưng thực sự chạy quanh từng bean tại bước 11 khi bean được instantiate.

Bảng đối chiếu đầy đủ:

BeanFactoryPostProcessorBeanPostProcessor
Đối tượng can thiệpBeanDefinition (metadata)Bean instance (object)
Chạy lúc nàoBước 5 — trước bất kỳ instantiateBước 11 — quanh từng bean khi tạo
InputConfigurableListableBeanFactoryObject bean + bean name
Có thể trả về object khác?Không (ghi vào definition)Có — thay bằng proxy
Ví dụ Spring coreConfigurationClassPostProcessor, PropertySourcesPlaceholderConfigurerAutowiredAnnotationBeanPostProcessor, CommonAnnotationBeanPostProcessor, AnnotationAwareAspectJAutoProxyCreator
Khi nào cần tự viết?Tweak scope/lazy/class của definitionWrap bean với decorator/proxy tùy chỉnh

Ví dụ BPP nổi bật:

  • AutowiredAnnotationBeanPostProcessor: chạy postProcessBeforeInitialization, dùng reflection inject field/method có @Autowired.
  • CommonAnnotationBeanPostProcessor: gọi method có @PostConstruct sau init, gọi @PreDestroy khi destroy.
  • AnnotationAwareAspectJAutoProxyCreator: chạy postProcessAfterInitialization, nếu bean match pointcut → wrap bằng JDK dynamic proxy hoặc CGLIB proxy. Đây là nền của @Transactional, @Cacheable.

5. Vì sao tách pha metadata khỏi instance?

Đây là câu hỏi thiết kế quan trọng — và câu trả lời giải thích nhiều thứ trong Spring.

Vấn đề nếu không tách: nếu container tạo object ngay khi scan thấy @Component, thì mọi "tweak" phải xảy ra trước khi scan hoặc bằng annotation. Không có cách nào để một module bên ngoài (autoconfig, Spring Cloud) thay đổi behavior của bean mà không sửa code annotation.

Giải pháp tách pha: bằng cách tạo metadata trước, giữ nó trong map, rồi để BFPP chạy trên map đó, Spring tạo ra một "cửa sổ" cho phép mọi module đăng ký BFPP và can thiệp definition. Các hệ quả thực tế:

  1. Spring Boot autoconfig: mỗi AutoConfiguration class thực ra là config class khai báo conditional bean — nhưng cũng có thể là BFPP. Ví dụ: DataSourceAutoConfiguration kiểm tra classpath có driver không, rồi mới đăng ký DataSource definition vào map.

  2. Spring Cloud Config: khi app khởi động, Spring Cloud bootstrap context load property từ config server trước khi main context refresh. Main context BFPP sau đó dùng các property đó để resolve ${...} trong definition.

  3. Debug bean behavior bất ngờ: nếu một bean có scope/lazy/class khác với khai báo annotation, đó là dấu hiệu một BFPP nào đó đã modify definition. Cách debug: đặt breakpoint trong invokeBeanFactoryPostProcessors hoặc log beanFactory.getBeanDefinition("beanName") ở đầu app.

Pitfall — Nhầm BFPP với BPP

Hai interface tên gần giống nhau nhưng thời điểm và đối tượng can thiệp khác nhau hoàn toàn. Nhầm lẫn phổ biến là viết logic "modify bean instance" vào BeanFactoryPostProcessor — nhưng tại bước 5 chưa có instance nào để modify. Ngược lại, viết logic "đọc/ghi scope hay class" vào BeanPostProcessor — thì đã quá muộn, bean đã tạo từ definition không đổi được nữa.

// SAI — co gang lay instance trong BFPP, se throw exception
@Component
public class WrongPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory bf) {
        // getBean() o day throw BeanCurrentlyInCreationException
        // vi buoc 11 chua chay
        OrderService svc = bf.getBean(OrderService.class); // THROW
    }
}
// DUNG — doc va sua definition (metadata), khong lay instance
@Component
public class CorrectPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory bf) {
        BeanDefinition def = bf.getBeanDefinition("orderService");
        def.setLazyInit(true); // OK — sua metadata
    }
}

Tra lỗi nhanh — lỗi nào thuộc pha nào

Gặp startup error liên quan definition/post-processor, map vào bảng này trước khi đọc full stack trace (đánh số bước theo refresh() 12 bước):

Lỗi thường gặpXảy ra ở bước nàoNguyên nhân
BeanDefinitionParsingException2 hoặc 5 — load/parse definition@Configuration class final, syntax sai, XML malformed
BeanDefinitionStoreException5 — load definitionPlaceholder ${...} không resolve được, resource import thiếu
BeanDefinitionOverrideException5 — đăng ký vào beanDefinitionMapHai bean cùng tên — Boot 2.1+ cấm override mặc định
BeanCurrentlyInCreationException ngay trong BFPP5 — BFPP gọi getBean()BFPP cố lấy instance khi bước 11 chưa chạy (pitfall ở trên)
NotWritablePropertyException11 — populate propertyDefinition trỏ tới property không tồn tại trên class
Bean chạy được nhưng scope/lazy/class khác khai báosau 5 — definition đã bị sửaMột BFPP đã modify definition — log getBeanDefinition("beanName") để xác nhận

Liên hệ các bài khác

Bài này là một mảnh trong bức tranh container. Ghép với các bài khác để thấy toàn bộ luồng:

  • BeanFactory vs ApplicationContextbeanDefinitionMap là field của DefaultListableBeanFactory, nơi metadata sống. Bài đó giải thích hai map cốt lõi và tại sao singleton scope = "một entry trong singletonObjects map".
  • refresh() 12 bước (bài tiếp theo) — BFPP chạy ở bước 5 (invokeBeanFactoryPostProcessors), BPP đăng ký ở bước 6, instantiate ở bước 11. Biết thứ tự này mới debug đúng startup error.
  • @Bean & @ConfigurationConfigurationClassPostProcessor là BFPP parse @Configuration class. Đây là lý do @Bean method hoạt động — nếu không có BFPP này, method chỉ là method Java, container không biết gì.

Tóm tắt

  • BeanDefinition là metadata (class, scope, dependencies) lưu trong beanDefinitionMapchưa phải object instance. Object chỉ ra đời ở bước 11 của refresh().
  • BeanFactoryPostProcessor chạy ở bước 5 — sau khi load mọi definition, trước khi instantiate. Nhận ConfigurableListableBeanFactory, có thể đọc và ghi toàn bộ beanDefinitionMap.
  • BeanPostProcessor chạy quanh từng bean tại bước 11 — nhận instance đã tạo, có thể trả về proxy thay thế.
  • Tách pha metadata khỏi instance = cho phép BFPP (autoconfig, Spring Cloud) tweak definition mà không sửa annotation.
  • Debug bean có behavior bất ngờ: kiểm tra BFPP nào đã modify definition trước instantiate.

Tự kiểm tra

Tự kiểm tra
Q1
Khi bạn khai báo @Service trên OrderService, điều gì xảy ra ngay tại bước scan? Object OrderService có được tạo chưa?

Chưa. Container scan thấy @Service → tạo một BeanDefinition (metadata: class name, scope singleton, lazy false, constructor dependencies) → lưu vào beanDefinitionMap với key là bean name.

Object OrderService thật sự chỉ được tạo ở bước 11 của refresh() (finishBeanFactoryInitialization), khi container gọi createBean cho từng entry trong beanDefinitionMap.

Ý nghĩa thực tế: trong khoảng thời gian từ bước 2 đến bước 10, beanDefinitionMap đã đầy đủ nhưng singletonObjects còn trống. Đây là khoảng thời gian BFPP có thể can thiệp an toàn.

Q2
Sự khác biệt cốt lõi giữa BeanFactoryPostProcessorBeanPostProcessor là gì? Cho một ví dụ thực tế từ Spring core cho mỗi loại.

Khác biệt nằm ở thời điểmđối tượng can thiệp:

  • BFPP nhận ConfigurableListableBeanFactory, can thiệp definition (metadata) ở bước 5 — trước khi bất kỳ bean nào instantiate.
  • BPP nhận object instance đã tạo, có thể trả về object khác (proxy) — chạy quanh từng bean tại bước 11.

Ví dụ BFPP trong Spring core: PropertySourcesPlaceholderConfigurer — đọc toàn bộ definition, replace ${...} placeholder trong @Value bằng giá trị property thực tế. ConfigurationClassPostProcessor — parse @Configuration class, đăng ký @Bean method vào map.

Ví dụ BPP trong Spring core: AutowiredAnnotationBeanPostProcessor — inject dependency vào field/method có @Autowired sau khi bean được tạo. AnnotationAwareAspectJAutoProxyCreator — wrap bean bằng AOP proxy nếu có aspect match (nền của @Transactional).

Q3
Bạn viết một BeanFactoryPostProcessor và bên trong gọi beanFactory.getBean(OrderService.class). Điều gì xảy ra? Vì sao?

Spring sẽ throw lỗi hoặc cảnh báo nghiêm trọng. Lý do: BFPP chạy ở bước 5, trước bước 11 (finishBeanFactoryInitialization) — chưa có instance nào trong singletonObjects.

Khi getBean gọi vào thời điểm này, container sẽ cố tạo bean ngay lập tức (outside normal lifecycle). Điều này có thể dẫn đến BeanCurrentlyInCreationException hoặc bean được tạo trước khi các BFPP khác chạy xong — nghĩa là bean đó sẽ không nhận được modification từ BFPP sau.

Spring ghi log cảnh báo rõ ràng khi phát hiện pattern này: "Bean ... is not eligible for getting processed by all BeanFactoryPostProcessors". Đây là dấu hiệu để refactor logic đó sang BPP hoặc ApplicationListener<ContextRefreshedEvent>.

Q4
Vì sao tách pha metadata (BeanDefinition) khỏi pha instantiate cho phép Spring Boot autoconfig hoạt động mà không cần sửa annotation trong code của bạn?

Nếu container tạo object ngay khi scan thấy @Component, không có "cửa sổ" nào để module bên ngoài can thiệp. Mọi customization phải xảy ra ở annotation time — tức là phải sửa code nguồn.

Nhờ tách pha: sau khi scan xong, toàn bộ definition nằm trong beanDefinitionMap dưới dạng object Java thông thường. Autoconfig starter chỉ cần đăng ký BFPP — BFPP đó nhận toàn bộ map, kiểm tra điều kiện (classpath, property, missing bean), rồi thêm/sửa/xóa definition.

Ví dụ: DataSourceAutoConfiguration kiểm tra "có driver JDBC trong classpath không, có spring.datasource.url không, đã có bean DataSource chưa" — nếu thỏa mãn, đăng ký HikariDataSource definition. Tất cả xảy ra ở bước 5, trong BFPP, không đụng đến annotation nào của bạn.

Q5
ConfigurationClassPostProcessor là loại gì (BFPP hay BPP)? Nếu nó không tồn tại, điều gì xảy ra với @Bean method trong @Configuration class?

ConfigurationClassPostProcessorBFPP — chạy ở bước 5, trước instantiate.

Nhiệm vụ của nó: scan tất cả @Configuration class đã được đăng ký trong beanDefinitionMap, parse từng @Bean method, rồi tạo thêm BeanDefinition cho mỗi method đó vào map.

Nếu không có ConfigurationClassPostProcessor: container chỉ biết về class AppConfig (vì nó có @Configuration nên được scan). Nhưng các method dataSource(), orderService() trong đó chỉ là method Java thông thường — container không biết chúng khai báo bean. Kết quả: mọi bean định nghĩa qua @Bean biến mất khỏi context, startup throw NoSuchBeanDefinitionException cho các dependency đó.

Đây là lý do ConfigurationClassPostProcessor là BFPP ưu tiên cao nhất, chạy trước mọi BFPP custom.

Bài tiếp theo: Environment & PropertySource

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