容器扩展点 (Container Extension Points)
通常情况下,应用开发人员不需要子类化 ApplicationContext 的实现类。相反,可以通过插入特殊集成接口的实现来扩展 Spring IoC 容器。接下来的几节将描述这些集成接口。
使用 BeanPostProcessor 定制 Bean
BeanPostProcessor 接口定义了回调方法,你可以实现这些方法来提供自己的(或覆盖容器默认的)实例化逻辑、依赖解析逻辑等。如果你想在 Spring 容器完成 Bean 的实例化、配置和初始化之后实现一些自定义逻辑,可以插入一个或多个自定义的 BeanPostProcessor 实现。
你可以配置多个 BeanPostProcessor 实例,并且可以通过设置 order 属性来控制这些 BeanPostProcessor 实例的运行顺序。仅当 BeanPostProcessor 实现了 Ordered 接口时,你才能设置此属性。如果你编写自己的 BeanPostProcessor,也应该考虑实现 Ordered 接口。有关更多详细信息,请参阅 BeanPostProcessor 和 Ordered 接口的 javadoc。另请参阅关于编程式注册 BeanPostProcessor 实例的说明。
注意
BeanPostProcessor 实例作用于 Bean(或对象)实例。也就是说,Spring IoC 容器实例化一个 Bean 实例,然后 BeanPostProcessor 实例开始工作。
BeanPostProcessor 实例的作用域是按容器的。仅当你使用容器层次结构时,这才有意义。如果你在一个容器中定义了一个 BeanPostProcessor,它仅对该容器中的 Bean 进行后处理。换句话说,在一个容器中定义的 Bean 不会被另一个容器中定义的 BeanPostProcessor 后处理,即使这两个容器属于同一个层次结构。
要更改实际的 Bean 定义(即定义 Bean 的蓝图),你反而需要使用 BeanFactoryPostProcessor,如使用 BeanFactoryPostProcessor 定制配置元数据中所述。
org.springframework.beans.factory.config.BeanPostProcessor 接口包含恰好两个回调方法。当此类作为后置处理器注册到容器时,对于容器创建的每个 Bean 实例,后置处理器都会在容器初始化方法(如 InitializingBean.afterPropertiesSet() 或任何声明的 init 方法)调用之前以及任何 Bean 初始化回调之后收到来自容器的回调。后置处理器可以对 Bean 实例采取任何操作,包括完全忽略回调。Bean 后置处理器通常用于检查回调接口,或者可能用代理包装 Bean。一些 Spring AOP 基础设施类就是作为 Bean 后置处理器实现的,以便提供代理包装逻辑。
ApplicationContext 会自动检测配置元数据中定义的任何实现了 BeanPostProcessor 接口的 Bean。ApplicationContext 将这些 Bean 注册为后置处理器,以便稍后在创建 Bean 时调用它们。Bean 后置处理器可以像任何其他 Bean 一样在容器中部署。
请注意,当在配置类上使用 @Bean 工厂方法声明 BeanPostProcessor 时,工厂方法的返回类型应该是实现类本身,或者至少是 org.springframework.beans.factory.config.BeanPostProcessor 接口,以便清楚地表明该 Bean 的后置处理器性质。否则,ApplicationContext 在完全创建它之前无法按类型自动检测它。由于需要尽早实例化 BeanPostProcessor 以便应用于上下文中其他 Bean 的初始化,因此这种早期类型检测至关重要。
编程式注册 BeanPostProcessor 实例
虽然推荐通过 ApplicationContext 自动检测来注册 BeanPostProcessor(如前所述),但你也可以通过 ConfigurableBeanFactory 的 addBeanPostProcessor 方法显式地编程式注册。当你需要在注册前进行条件逻辑评估,或者甚至在层次结构中的上下文之间复制 Bean 后置处理器时,这很有用。但请注意,编程式添加的 BeanPostProcessor 实例不遵循 Ordered 接口。在这里,注册的顺序决定了执行顺序。还要注意,编程式注册的 BeanPostProcessor 实例总是优先于通过自动检测注册的实例处理,而不管任何显式的排序。
BeanPostProcessor 实例与 AOP 自动代理
实现 BeanPostProcessor 接口的类是特殊的,会被容器区别对待。所有 BeanPostProcessor 实例及其直接引用的 Bean 都会在启动时实例化,作为 ApplicationContext 特殊启动阶段的一部分。接下来,所有 BeanPostProcessor</code> 实例将以排序的方式注册,并应用于容器中后续的所有 Bean。由于 AOP 自动代理本身是作为 BeanPostProcessor实现的,因此BeanPostProcessor` 实例及其直接引用的 Bean 都没有资格进行自动代理,因此不会被织入切面。
对于任何此类 Bean,你应该会看到一条信息日志消息:Bean 'someBean' is not eligible for getting processed by all BeanPostProcessor interfaces (for example: not eligible for auto-proxying)。
如果你通过自动装配或 @Resource(可能会回退到自动装配)将 Bean 注入到 BeanPostProcessor 中,Spring 在搜索类型匹配的依赖候选对象时可能会访问到意料之外的 Bean,从而使它们失去自动代理或其他类型的 Bean 后处理资格。例如,如果你有一个用 @Resource 注解的依赖,其字段或 setter 名称与 Bean 的声明名称不直接对应,且没有使用 name 属性,Spring 将访问其他 Bean 以通过类型进行匹配。
接下来的示例展示了如何在 ApplicationContext 中编写、注册和使用 BeanPostProcessor 实例。
示例:Hello World, BeanPostProcessor 风格
第一个示例说明了基本用法。该示例展示了一个自定义 BeanPostProcessor 实现,它在容器创建每个 Bean 时调用其 toString() 方法,并将结果字符串打印到系统控制台。
以下列表显示了自定义 BeanPostProcessor 实现类的定义:
package scripting;
import org.springframework.beans.factory.config.BeanPostProcessor;
public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {
// 简单地按原样返回实例化的 Bean
public Object postProcessBeforeInitialization(Object bean, String beanName) {
return bean; // 我们在这里可以潜在地返回任何对象引用...
}
public Object postProcessAfterInitialization(Object bean, String beanName) {
System.out.println("Bean '" + beanName + "' created : " + bean.toString());
return bean;
}
}package scripting
import org.springframework.beans.factory.config.BeanPostProcessor
class InstantiationTracingBeanPostProcessor : BeanPostProcessor {
// 简单地按原样返回实例化的 Bean
override fun postProcessBeforeInitialization(bean: Any, beanName: String): Any? {
return bean // 我们在这里可以潜在地返回任何对象引用...
}
override fun postProcessAfterInitialization(bean: Any, beanName: String): Any? {
println("Bean '$beanName' created : $bean")
return bean
}
}以下 beans 元素使用了 InstantiationTracingBeanPostProcessor:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:lang="http://www.springframework.org/schema/lang"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/lang
https://www.springframework.org/schema/lang/spring-lang.xsd">
<lang:groovy id="messenger"
script-source="classpath:org/springframework/scripting/groovy/Messenger.groovy">
<lang:property name="message" value="Fiona Apple Is Just So Dreamy."/>
</lang:groovy>
<!--
当上述 Bean (messenger) 实例化时,
此自定义 BeanPostProcessor 实现将输出信息到系统控制台
-->
<bean class="scripting.InstantiationTracingBeanPostProcessor"/>
</beans>请注意 InstantiationTracingBeanPostProcessor 仅被简定义。它甚至没有名称,并且因为它是 Bean,所以可以像任何其他 Bean 一样进行依赖注入。(前面的配置还定义了一个由 Groovy 脚本支持的 Bean。)
以下 Java 应用程序运行上述代码和配置:
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.scripting.Messenger;
public final class Boot {
public static void main(final String[] args) throws Exception {
ApplicationContext ctx = new ClassPathXmlApplicationContext("scripting/beans.xml");
Messenger messenger = ctx.getBean("messenger", Messenger.class);
System.out.println(messenger);
}
}import org.springframework.beans.factory.getBean
fun main() {
val ctx = ClassPathXmlApplicationContext("scripting/beans.xml")
val messenger = ctx.getBean<Messenger>("messenger")
println(messenger)
}上述应用程序的输出如下:
Bean 'messenger' created : org.springframework.scripting.groovy.GroovyMessenger@272961
org.springframework.scripting.groovy.GroovyMessenger@272961示例:AutowiredAnnotationBeanPostProcessor
将回调接口或注解与自定义 BeanPostProcessor 实现结合使用是扩展 Spring IoC 容器的常用手段。一个例子是 Spring 的 AutowiredAnnotationBeanPostProcessor —— 它是 Spring 发行版附带的一个 BeanPostProcessor 实现,用于自动装配带注解的字段、setter 方法以及任意配置方法。
使用 BeanFactoryPostProcessor 定制配置元数据
我们要查看的下一个扩展点是 org.springframework.beans.factory.config.BeanFactoryPostProcessor。该接口的语义与 BeanPostProcessor 相似,但有一个主要区别:BeanFactoryPostProcessor 作用于 Bean 配置元数据。也就是说,Spring IoC 容器允许 BeanFactoryPostProcessor 读取配置元数据,并可能在容器实例化除 BeanFactoryPostProcessor 实例之外的任何 Bean 之前更改它。
你可以配置多个 BeanFactoryPostProcessor 实例,并且可以通过设置 order 属性来控制这些 BeanFactoryPostProcessor 实例的运行顺序。但是,仅当 BeanFactoryPostProcessor 实现了 Ordered</code> 接口时,你才能设置此属性。如果你编写自己的 BeanFactoryPostProcessor,也应该考虑实现 Ordered 接口。有关更多详细信息,请参阅 [BeanFactoryPostProcessor](https://docs.spring.io/spring-framework/docs/7.0.2/javadoc-api/org/springframework/beans/factory/config/BeanFactoryPostProcessor.html) 和 [Ordered`](https://docs.spring.io/spring-framework/docs/7.0.2/javadoc-api/org/springframework/core/Ordered.html) 接口的 javadoc。
注意
如果你想更改实际的 Bean 实例(即从配置元数据创建的对象),那么你需要使用 BeanPostProcessor(前面在使用 BeanPostProcessor 定制 Bean中进行了描述)。虽然在技术上可以在 BeanFactoryPostProcessor 内部使用 Bean 实例(例如通过使用 BeanFactory.getBean()),但这样做会导致 Bean 过早实例化,从而违反标准容器生命周期。这可能会导致负面副作用,例如绕过 Bean 后处理。
此外,BeanFactoryPostProcessor 实例的作用域是按容器的。仅当你使用容器层次结构时,这才有意义。如果你在一个容器中定义了一个 BeanFactoryPostProcessor,它仅应用于该容器中的 Bean 定义。一个容器中的 Bean 定义不会被另一个容器中的 BeanFactoryPostProcessor 实例处理,即使这两个容器属于同一个层次结构。
当在 ApplicationContext 内部声明 Bean 工厂后置处理器时,它会自动运行,以便对定义容器的配置元数据应用更改。Spring 包含许多预定义的 Bean 工厂后置处理器,如 PropertyOverrideConfigurer 和 PropertySourcesPlaceholderConfigurer。你也可以使用自定义的 BeanFactoryPostProcessor —— 例如,注册自定义属性编辑器。
ApplicationContext 会自动检测部署在其中并实现了 BeanFactoryPostProcessor 接口的任何 Bean。它会在适当的时候将这些 Bean 用作 Bean 工厂后置处理器。你可以像部署任何其他 Bean 一样部署这些后置处理器 Bean。
注意
与 BeanPostProcessor 一样,你通常不希望将 BeanFactoryPostProcessor 配置为延迟初始化。如果没有其他 Bean 引用 Bean(Factory)PostProcessor,该后置处理器将根本不会实例化。因此,标记为延迟初始化将被忽略,即使你在 <beans /> 元素的声明中将 default-lazy-init 属性设置为 true,Bean(Factory)PostProcessor 也会被及早实例化。
示例:使用 PropertySourcesPlaceholderConfigurer 进行属性占位符替换
你可以使用 PropertySourcesPlaceholderConfigurer 通过使用标准 Java Properties 格式将 Bean 定义中的属性值外部化到单独的文件中。这样做使部署应用程序的人员能够定制特定于环境的属性,如数据库 URL 和密码,而无需承担修改容器主要 XML 定义文件的复杂性或风险。
考虑以下基于 XML 的配置元数据片段,其中定义了一个带有占位符值的 DataSource:
<bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
<property name="locations" value="classpath:com/something/jdbc.properties"/>
</bean>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>示例展示了从外部 Properties 文件配置的属性。在运行时,PropertySourcesPlaceholderConfigurer 应用于替换 DataSource 的某些属性的元数据。要替换的值指定为格式为 ${property-name} 的占位符,这遵循 Ant、log4j 和 JSP EL 风格。
实际值来自另一个标准 Java Properties 格式的文件:
jdbc.driverClassName=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:hsql://production:9002
jdbc.username=sa
jdbc.password=root因此,${jdbc.username} 字符串在运行时被替换为值 'sa',同样的替换也适用于与属性文件中的键匹配的其他占位符值。PropertySourcesPlaceholderConfigurer 检查 Bean 定义的大多数属性和属性中的占位符。此外,你还可以自定义占位符前缀、后缀、默认值分隔符和转义字符。此外,可以通过 JVM 系统属性(或通过 SpringProperties 机制)设置 spring.placeholder.escapeCharacter.default 属性来全局更改或禁用默认转义字符。
使用 context 命名空间,你可以使用专用配置元素配置属性占位符。你可以在 location 属性中以逗号分隔列表的形式提供一个或多个位置,如下例所示:
<context:property-placeholder location="classpath:com/something/jdbc.properties"/>PropertySourcesPlaceholderConfigurer 不仅在你指定的 Properties 文件中查找属性。默认情况下,如果它在指定的属性文件中找不到属性,它会检查 Spring Environment 属性和常规 Java System 属性。
警告
给定的应用程序应仅定义一个此类元素以包含其所需的属性。只要配置了不同的占位符语法 (${...}),就可以配置多个属性占位符。
如果你需要将用于替换的属性源模块化,不应创建多个属性占位符。相反,你应该创建自己的 PropertySourcesPlaceholderConfigurer Bean,收集要使用的属性。
提示
你可以使用 PropertySourcesPlaceholderConfigurer 来替换类名,这在运行时必须选择特定的实现类时有时很有用。以下示例展示了如何操作:
<bean class="org.springframework.beans.factory.config.PropertySourcesPlaceholderConfigurer">
<property name="locations">
<value>classpath:com/something/strategy.properties</value>
</property>
<property name="properties">
<value>custom.strategy.class=com.something.DefaultStrategy</value>
</property>
</bean>
<bean id="serviceStrategy" class="${custom.strategy.class}"/>如果类在运行时无法解析为有效的类,则对于非延迟初始化的 Bean,Bean 的解析会在即将创建时失败,也就是在 ApplicationContext 的 preInstantiateSingletons() 阶段。
示例:PropertyOverrideConfigurer
PropertyOverrideConfigurer 是另一个 Bean 工厂后置处理器,类似于 PropertySourcesPlaceholderConfigurer,但与后者不同的是,原始定义可以具有 Bean 属性的默认值或根本没有值。如果重写的 Properties 文件没有某个 Bean 属性的条目,则使用默认的上下文定义。
请注意,Bean 定义并不知道自己被重写了,因此从 XML 定义文件中立即看不出正在使用重写配置器。如果多个 PropertyOverrideConfigurer 实例为同一个 Bean 属性定义了不同的值,则由于重写机制,最后一个获胜。
属性文件配置行采用以下格式:
beanName.property=value以下列表显示了该格式的一个示例:
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql:mydb此示例文件可用于包含名为 dataSource 且具有 driverClassName 和 url 属性的容器定义。
也支持复合属性名称,只要路径中除了被重写的最终属性之外的每个组件都已经非空(大概是由构造函数初始化的)。在以下示例中,tom Bean 的 fred 属性的 bob 属性的 sammy 属性被设置为标量值 123:
tom.fred.bob.sammy=123注意
指定的重写值始终是字面值。它们不会被转换为 Bean 引用。当 XML Bean 定义中的原始值指定 Bean 引用时,此约定也适用。
通过 Spring 2.5 引入的 context 命名空间,可以使用专用的配置元素配置属性重写,如下例所示:
<context:property-override location="classpath:override.properties"/>使用 FactoryBean 定制实例化逻辑
你可以为本身是工厂的对象实现 org.springframework.beans.factory.FactoryBean 接口。
FactoryBean 接口是 Spring IoC 容器实例化逻辑的可插入点。如果你有复杂的初始化代码,用 Java 表达比用(可能)冗长的 XML 更好,你可以创建自己的 FactoryBean,在该类内部编写复杂的初始化逻辑,然后将你的自定义 FactoryBean 插入容器。
FactoryBean<T> 接口提供三个方法:
T getObject(): 返回此工厂创建的对象的实例。根据此工厂返回的是单例还是原型,该实例可能会共享。boolean isSingleton(): 如果此FactoryBean返回单例,则返回true,否则返回false。该方法的默认实现返回true。Class<?> getObjectType(): 返回getObject()方法返回的对象类型,如果事先不知道类型,则返回null。
FactoryBean 概念和接口在 Spring 框架内的许多地方都有使用。Spring 本身附带了 50 多个 FactoryBean 接口的实现。
当你需要向容器请求实际的 FactoryBean 实例本身而不是它产生的 Bean 时,在调用 ApplicationContext 的 getBean() 方法时,在 Bean 的 id 前加上 & 符号。因此,对于 id 为 myBean 的给定 FactoryBean,在容器上调用 getBean("myBean") 返回 FactoryBean 的产品,而调用 getBean("&myBean")</code> 返回 FactoryBean` 实例本身。
补充教学 —— 深入理解 Spring 的“生命周期干扰器”
1. BeanPostProcessor (BPP) vs BeanFactoryPostProcessor (BFPP) 这是面试中最经典的考点,用一句话总结它们的区别:
- BFPP:处理的是 Bean 的菜单 (BeanDefinition)。在 Bean 还没出生前,把菜单改了(比如把占位符
${...}换成实际的值)。 - BPP:处理的是 Bean 的本体 (Object Instance)。在 Bean 出生后、正式上岗前,由它来加工装修(比如变成 AOP 代理对象)。
2. 为什么 BPP 里的 return bean 很重要? 如果你在 BPP 的方法里返回了 null,这个 Bean 就会从容器里“消失”,后面的 BPP 也不会再处理它。你可以利用这个特性返回一个完全不同的对象(比如包装了一个代理对象),这是 AOP 实现的核心。
3. FactoryBean:Spring 里的“合法中介”
- 普通 Bean:你向 Spring 要
A,Spring 直接给你A。 - FactoryBean:你向 Spring 要
A,Spring 发现A是个工厂,于是调用A.getObject(),把结果给你。 - 为什么要它? 很多第三方框架(比如 MyBatis, Feign)的接口并没有实现类,它们是运行时动态生成的类。这时候就需要
FactoryBean这个中介,让 Spring 能管理这些“虚无”的对象。
4. & 符号的妙用 在面试或排查问题时,经常需要获取工厂本身。
context.getBean("myBean")-> 拿工厂造出来的 产品。context.getBean("&myBean")-> 拿 工厂本身。
5. 避坑指南:BFPP 里的 getBean() 文档中提到的那个危险操作:在 BeanFactoryPostProcessor 里调用 getBean()。 原因:此时容器还没初始化完,如果你强行拿一个普通的 Bean,会导致这个 Bean 提前全生命周期初始化,而此时一些重要的 BeanPostProcessor 还没到位。结果就是:这个提前出来的 Bean 可能没有 AOP 功能,或者某些属性没被正确注入,产生莫名其妙的 Bug。