乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 Spring概述与IoC容器

    • Spring概述与IoC容器
    • Spring Framework 概述
    • IoC 与 DI 核心概念
    • @Configuration 详解
    • @Component 详解
    • @ComponentScan 详解
    • @Import 详解
    • @Profile 详解
    • @PropertySource 详解
    • @Service 详解
    • @Repository 详解
  • 第2章 Bean的定义与依赖注入

    • Bean的定义与依赖注入
    • @Bean 详解
    • @Autowired 详解
    • @Qualifier 详解
    • @Primary 详解
    • @Resource 详解
    • @Inject 详解
    • @Named 详解
    • @Value 详解
    • @Scope 详解
    • @Lazy 详解
  • 第3章 Bean生命周期与作用域

    • Bean生命周期与作用域
    • Bean生命周期概述
    • @PostConstruct
    • @PreDestroy
    • InitializingBean
    • DisposableBean
    • BeanPostProcessor
    • BeanFactoryPostProcessor
  • 第4章 AOP面向切面编程

    • AOP面向切面编程
    • AOP核心概念
    • @EnableAspectJAutoProxy
    • @Aspect
    • @Pointcut
    • @Before
    • @After
    • @AfterReturning
    • @AfterThrowing
    • @Around
  • 第5章 数据访问与事务管理

    • 数据访问与事务管理
    • 数据访问概述
    • @EnableTransactionManagement
    • @Transactional
    • @Transactional 的传播行为
    • @Transactional 的隔离级别
    • @Transactional 的回滚规则
    • @Transactional 的超时与只读属性
    • @TransactionalEventListener
  • 第6章 Spring Boot自动配置基础

    • Spring Boot自动配置基础
    • @SpringBootApplication 注解
    • @EnableAutoConfiguration 注解
    • @ConfigurationProperties 注解
    • @ConditionalOnClass 注解
    • @ConditionalOnMissingBean 注解
    • @ConditionalOnProperty 注解
  • 第7章 从容器到Web: Spring MVC导引

    • Spring MVC 导引
  • 第8章 扩展阅读

    • 扩展阅读
    • Spring 事件机制 — ApplicationEvent / ApplicationListener
    • @EventListener
    • SpEL — Spring 表达式语言
    • 校验 Validation — JSR-303 / JSR-380 Bean Validation
    • 类型转换与数据绑定 — Converter / DataBinder
  • 附录

    • Spring Framework 专业术语
    • Spring 核心知识点
    • Spring 面试高频考点
    • Spring 核心注解速查表

BeanFactoryPostProcessor

定位:Spring 容器提供的容器级扩展接口,在 BeanDefinition 加载完成后、Bean 实例化之前对配置元数据进行修改。它是 Spring 属性占位符解析、@Configuration 配置类增强、自定义作用域注册等机制的核心基础设施。


定义与作用

BeanFactoryPostProcessor 是 org.springframework.beans.factory.config 包下的接口,定义如下:

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

其核心语义是:在所有 BeanDefinition 加载到容器后、任何 Bean 实例化之前,对 BeanFactory 中的配置元数据进行修改。开发者可在该方法中执行:

  • 解析属性占位符(${jdbc.url} → 实际值)
  • 修改 BeanDefinition 的属性(作用域、懒加载、依赖关系)
  • 注册新的 BeanDefinition 到容器
  • 移除或替换已有的 BeanDefinition
  • 对 @Configuration 配置类进行 CGLIB 增强处理

与 BeanPostProcessor 的关键区别在于:BeanFactoryPostProcessor 操作的是 BeanDefinition(配置元数据),而 BeanPostProcessor 操作的是 Bean 实例(已创建的对象)。


适用位置与常用方法

项目说明
实现方式类实现 BeanFactoryPostProcessor 接口,并注册为 Spring Bean
作用范围容器内所有 BeanDefinition,除非在方法内部通过 beanName 过滤
执行次数容器刷新阶段仅执行一次(refresh() 的 invokeBeanFactoryPostProcessors() 阶段)
操作对象ConfigurableListableBeanFactory,可获取和修改 BeanDefinition
执行时机BeanDefinition 加载完成后、任何 Bean 实例化之前

关键特性:BeanFactoryPostProcessor 可以修改 Bean 的类名、作用域、属性值、依赖关系,甚至注册全新的 BeanDefinition。但它无法直接操作 Bean 实例,因为此时所有 Bean 都尚未创建。


核心原理

Spring 容器在 AbstractApplicationContext.refresh() 的 invokeBeanFactoryPostProcessors() 阶段,遍历所有注册的 BeanFactoryPostProcessor 并调用其 postProcessBeanFactory() 方法。

BFPP 与 BPP 的时序对比

以下大图清晰展示了 BeanFactoryPostProcessor 和 BeanPostProcessor 在容器生命周期中的位置差异:


完整示例:飞翔科技学生管理系统的动态数据源切换

场景简述

飞翔科技架构师白歌在设计学生管理系统时,要求系统支持"开发环境"和"测试环境"两套数据库配置。但小崔发现,现有的 application.properties 中数据库配置写死,每次切换环境都要改文件并重新打包,效率极低。

白歌提出需求:在容器启动时,根据系统环境变量动态修改数据源 Bean 的 URL,无需改动配置文件。这正好适合用 BeanFactoryPostProcessor 在 Bean 实例化前修改 DataSource 的 BeanDefinition。

操作前:硬编码配置的代码

@Configuration
public class DataSourceConfig {

    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        // 硬编码,切换环境必须改代码重新打包
        config.setJdbcUrl("jdbc:mysql://localhost:3306/student_dev");
        config.setUsername("root");
        config.setPassword("dev_password");
        return new HikariDataSource(config);
    }
}

问题分析:

  • 开发环境(student_dev)和生产环境(student_prod)的 URL 写死在代码中
  • 每次部署到不同环境需要修改源码、重新编译打包
  • 运维人员无法通过环境变量或启动参数动态调整
  • 不符合"一次构建,多处部署"的 DevOps 原则

操作后:使用 BeanFactoryPostProcessor 的完整代码

白歌设计了一个 BeanFactoryPostProcessor,在 Bean 实例化前根据环境变量动态替换数据源 URL:

1. 动态数据源配置修改器

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;

@Component
public class DynamicDataSourceConfigurer implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        System.out.println("[BFPP] DynamicDataSourceConfigurer 开始执行...");

        // 读取环境变量
        String env = System.getenv("STUDENT_DB_ENV");
        if (env == null || env.isEmpty()) {
            env = "dev"; // 默认开发环境
        }
        System.out.println("[BFPP] 检测到环境标识:" + env);

        // 根据环境确定数据库配置
        String jdbcUrl;
        String password;

        switch (env) {
            case "prod":
                jdbcUrl = "jdbc:mysql://mysql.learnto.cn:3306/student_prod";
                password = "prod_secret_2024";
                break;
            case "test":
                jdbcUrl = "jdbc:mysql://test-mysql.learnto.cn:3306/student_test";
                password = "test_password";
                break;
            case "dev":
            default:
                jdbcUrl = "jdbc:mysql://localhost:3306/student_dev";
                password = "dev_password";
                break;
        }

        // 修改 dataSource BeanDefinition 的属性值
        if (beanFactory.containsBeanDefinition("dataSource")) {
            BeanDefinition beanDef = beanFactory.getBeanDefinition("dataSource");

            // 获取当前的构造器参数值(假设是通过 @Bean 方法返回的 DataSource)
            // 对于 @Bean 方法定义的 Bean,我们修改其工厂方法的属性
            // 这里演示通过修改 BeanDefinition 的 propertyValues 来影响实例化

            // 注意:对于 @Bean 方法,更实际的做法是修改配置类中的 @Value 占位符
            // 以下演示修改一个通过 XML 或 @Component 定义的 DataSource 的 propertyValues
            if (beanDef.getPropertyValues().contains("jdbcUrl")) {
                beanDef.getPropertyValues().add("jdbcUrl", jdbcUrl);
                System.out.println("[BFPP] 已修改 dataSource.jdbcUrl = " + jdbcUrl);
            }

            // 打印所有 BeanDefinition 信息用于演示
            System.out.println("[BFPP] dataSource BeanDefinition:" + beanDef.getBeanClassName());
        }

        // 更实际的场景:修改配置类中 @Value 占位符的解析结果
        // Spring 的 PropertySourcesPlaceholderConfigurer 本身就是一个 BFPP
        // 这里演示注册一个新的 BeanDefinition
        System.out.println("[BFPP] 环境配置完成,当前环境:" + env);
    }
}

2. 使用 @Value 占位符的数据源配置(配合 BFPP 思想)

在实际项目中,PropertySourcesPlaceholderConfigurer(Spring 内置的 BFPP)已经处理了 @Value 占位符的解析。白歌向团队展示的是如何自定义这一行为:

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {

    @Value("${student.db.url:jdbc:mysql://localhost:3306/student_dev}")
    private String jdbcUrl;

    @Value("${student.db.username:root}")
    private String username;

    @Value("${student.db.password:dev_password}")
    private String password;

    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(jdbcUrl);
        config.setUsername(username);
        config.setPassword(password);
        config.setMaximumPoolSize(10);
        System.out.println("[DataSourceConfig] 创建数据源,URL:" + jdbcUrl);
        return new HikariDataSource(config);
    }
}

3. 自定义属性源 BFPP(更贴近实际生产)

小崔最终实现了一个更实用的版本:在 BFPP 中向 Environment 添加自定义属性源,从而影响 @Value 的解析结果:

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Component
public class EnvironmentPropertyConfigurer implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        ConfigurableEnvironment env = (ConfigurableEnvironment) beanFactory.getEnvironment();

        // 从外部系统读取动态配置
        Map<String, Object> dynamicProps = loadDynamicProperties();

        // 添加到 Environment 的属性源列表中(高优先级)
        MapPropertySource propertySource = new MapPropertySource("dynamic-props", dynamicProps);
        env.getPropertySources().addFirst(propertySource);

        System.out.println("[BFPP] 已加载 " + dynamicProps.size() + " 条动态属性");
        dynamicProps.forEach((k, v) -> System.out.println("[BFPP]   " + k + " = " + v));
    }

    private Map<String, Object> loadDynamicProperties() {
        Map<String, Object> props = new HashMap<>();

        // 模拟从配置中心(如 Nacos、Apollo)读取配置
        String env = System.getenv("STUDENT_DB_ENV");
        if ("prod".equals(env)) {
            props.put("student.db.url", "jdbc:mysql://mysql.learnto.cn:3306/student_prod");
            props.put("student.db.password", "prod_secret_2024");
        } else if ("test".equals(env)) {
            props.put("student.db.url", "jdbc:mysql://test-mysql.learnto.cn:3306/student_test");
            props.put("student.db.password", "test_password");
        } else {
            props.put("student.db.url", "jdbc:mysql://localhost:3306/student_dev");
            props.put("student.db.password", "dev_password");
        }

        // 模拟从配置中心读取的其他动态配置
        props.put("student.cache.enabled", "true");
        props.put("student.cache.ttl", "3600");

        return props;
    }
}

Spring Boot 启动验证(设置环境变量为 test):

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import javax.sql.DataSource;

@SpringBootApplication
public class StudentManagementApplication {
    public static void main(String[] args) {
        // 模拟设置环境变量(实际通过 -Dstudent.db.env=test 或容器环境变量设置)
        // System.setProperty("student.db.env", "test"); // 仅用于演示

        ConfigurableApplicationContext context =
            SpringApplication.run(StudentManagementApplication.class, args);

        DataSource dataSource = context.getBean(DataSource.class);
        System.out.println("[Main] 数据源 Bean 类型:" + dataSource.getClass().getSimpleName());

        context.close();
    }
}

运行结果及分析:

[BFPP] 已加载 4 条动态属性
[BFPP]   student.db.url = jdbc:mysql://localhost:3306/student_dev
[BFPP]   student.db.password = dev_password
[BFPP]   student.cache.enabled = true
[BFPP]   student.cache.ttl = 3600
[DataSourceConfig] 创建数据源,URL:jdbc:mysql://localhost:3306/student_dev
[Main] 数据源 Bean 类型:HikariDataSource

关键差异:

  • 操作前:数据库配置硬编码在 Java 类中,切换环境必须修改源码、重新编译打包
  • 操作后:BeanFactoryPostProcessor 在 Bean 实例化前将动态配置注入 Environment,@Value 解析时自动使用最新值。运维人员只需修改环境变量或配置中心值,无需重新打包

易错场景与面试考点

易错场景一:在 BFPP 中尝试获取 Bean 实例

@Component
public class BrokenConfigurer implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // 错误!此时所有 Bean 都尚未实例化,getBean() 会触发提前实例化
        // 导致部分 BPP 未注册、部分 BeanDefinition 未处理
        DataSource dataSource = beanFactory.getBean(DataSource.class);
        System.out.println("[BFPP] 获取到 DataSource:" + dataSource);
    }
}

问题:BeanFactoryPostProcessor 的执行阶段早于所有 Bean 的实例化。此时调用 getBean() 会强制提前实例化该 Bean,但此时其他 BeanFactoryPostProcessor 可能尚未执行,其他 BeanPostProcessor 也尚未注册,导致该 Bean 的初始化不完整(如 @Value 占位符未解析、AOP 代理未生成)。

正确做法:BFPP 中只操作 BeanDefinition 和 Environment,绝不调用 getBean()。若必须访问其他 Bean,应使用 BeanPostProcessor 而非 BeanFactoryPostProcessor。

易错场景二:BFPP 中修改 BeanDefinition 后未意识到不影响已解析的注解

@Component
public class ScopeModifier implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        BeanDefinition bd = beanFactory.getBeanDefinition("studentService");
        bd.setScope("prototype"); // 修改作用域
        System.out.println("[BFPP] 已将 studentService 作用域改为 prototype");
    }
}

问题:虽然修改 BeanDefinition 的作用域是合法的,但若 studentService 已经被其他 Bean 通过 @Autowired 注入为依赖,Spring 在解析依赖时可能已根据原始作用域做了优化(如直接引用 singleton 实例)。修改作用域后,这些已解析的依赖关系不会自动更新。

正确做法:BeanFactoryPostProcessor 适合修改配置属性、注册新 BeanDefinition,但修改已有 Bean 的核心定义(如类名、作用域)应谨慎,最好在容器设计阶段完成,而非通过 BFPP 动态修改。

易错场景三:自定义 BFPP 与 Spring 内置 BFPP 的优先级冲突

@Component
public class CustomPropertyConfigurer implements BeanFactoryPostProcessor, Ordered {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // 尝试修改已被 PropertySourcesPlaceholderConfigurer 解析过的值
        // 但此时解析可能已完成或尚未完成,取决于优先级
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE; // 最低优先级,最后执行
    }
}

问题:Spring 内置的 PropertySourcesPlaceholderConfigurer 也是一个 BeanFactoryPostProcessor,负责解析 @Value 中的 ${...} 占位符。若自定义 BFPP 优先级设置不当,可能导致:

  • 自定义 BFPP 先执行,但此时占位符尚未解析,修改的是原始占位符字符串
  • 自定义 BFPP 后执行,但此时占位符已解析,修改不影响已解析的值

正确做法:若自定义 BFPP 需要影响 @Value 解析结果,应实现 PriorityOrdered 并设置高优先级(HIGHEST_PRECEDENCE),确保在 PropertySourcesPlaceholderConfigurer 之前执行。更推荐的做法是直接操作 Environment 的 PropertySources,如示例中的 addFirst() 方式。

面试考点

Q1:BeanFactoryPostProcessor 和 BeanPostProcessor 的核心区别?

BeanFactoryPostProcessor 在 BeanDefinition 加载后、实例化前 执行,操作的是配置元数据(BeanDefinition),可以修改类名、作用域、属性值等;BeanPostProcessor 在 Bean 实例化后 执行,操作的是已创建的对象实例,可以包装或替换 Bean 实例。前者影响"如何创建",后者影响"创建后加工"。

Q2:PropertySourcesPlaceholderConfigurer 是什么?

它是 Spring 内置的 BeanFactoryPostProcessor,负责解析 @Value 注解和 XML 配置中的 ${...} 占位符,将其替换为 Environment 中对应的属性值。它通过操作 BeanDefinition 中的属性值实现解析,在 Bean 实例化前完成。

Q3:为什么 BFPP 中不能调用 getBean()?

因为 BFPP 的执行阶段早于所有 Bean 的实例化。强制 getBean() 会导致该 Bean 提前实例化,但此时其他 BFPP 可能尚未执行(部分 BeanDefinition 未处理),其他 BPP 也尚未注册(该 Bean 的 AOP 代理、注解处理等将缺失),导致 Bean 处于不完整状态。

Q4:@Configuration 配置类是如何被增强的?

Spring 通过 ConfigurationClassPostProcessor(一个特殊的 BeanFactoryPostProcessor)处理 @Configuration 类。它使用 CGLIB 生成配置类的子类,确保 @Bean 方法之间的互相调用返回的是容器中的单例 Bean,而非多次调用创建多个实例。这个增强过程发生在 BFPP 阶段。


本文边界说明

本文档仅讲解 BeanFactoryPostProcessor 接口。关于 JSR-250 注解 @PostConstruct 和 @PreDestroy、Spring 接口 InitializingBean / DisposableBean、以及 Bean 级扩展点 BeanPostProcessor,请分别参阅本章其他独立文档。严禁在讲解 BeanFactoryPostProcessor 时混入 AOP 切面定义(@Aspect、@Pointcut 等)或 Web 层注解,以保持知识点的原子性和教学清晰度。

上一页
BeanPostProcessor