容器扩展点

通常,应用开发者无需继承 ApplicationContext 实现类。相反,可以通过插入特殊的集成接口的实现来扩展 Spring IoC 容器。接下来的几节将介绍这些集成接口。

使用 BeanPostProcessor 定制 Bean

BeanPostProcessor 接口定义了回调方法,你可以实现这些方法来提供你自己的(或覆盖容器默认的)实例化逻辑、依赖解析逻辑等。如果你想在 Spring 容器完成实例化、配置和初始化 Bean 之后实现一些自定义逻辑,你可以插入一个或多个自定义的 BeanPostProcessor 实现。

你可以配置多个 BeanPostProcessor 实例,并通过设置 order 属性来控制这些 BeanPostProcessor 实例的运行顺序。只有当 BeanPostProcessor 实现 Ordered 接口时,才能设置此属性。如果你编写自己的 BeanPostProcessor,也应考虑实现 Ordered 接口。更多详细信息,请参阅 BeanPostProcessorOrdered 接口的 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 实例
虽然推荐的 BeanPostProcessor 注册方法是通过 ApplicationContext 自动检测(如前所述),但你可以通过调用 ConfigurableBeanFactoryaddBeanPostProcessor 方法以编程方式注册它们。这在你需要在注册前评估条件逻辑或甚至在层级结构中的上下文之间复制 Bean 后处理器时非常有用。但是请注意,以编程方式添加的 BeanPostProcessor 实例不遵循 Ordered 接口。在这里,注册的顺序决定了执行顺序。另请注意,以编程方式注册的 BeanPostProcessor 实例总是先于通过自动检测注册的实例处理,无论是否有任何明确的排序。
BeanPostProcessor 实例和 AOP 自动代理

实现 BeanPostProcessor 接口的类是特殊的,容器会区别对待它们。所有 BeanPostProcessor 实例以及它们直接引用的 Bean 都会在启动时实例化,这是 ApplicationContext 特殊启动阶段的一部分。接着,所有 BeanPostProcessor 实例按排序方式注册,并应用于容器中的所有后续 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 实现类的定义

  • Java

  • Kotlin

package scripting;

import org.springframework.beans.factory.config.BeanPostProcessor;

public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {

	// simply return the instantiated bean as-is
	public Object postProcessBeforeInitialization(Object bean, String beanName) {
		return bean; // we could potentially return any object reference here...
	}

	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 {

	// simply return the instantiated bean as-is
	override fun postProcessBeforeInitialization(bean: Any, beanName: String): Any? {
		return bean // we could potentially return any object reference here...
	}

	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>

	<!--
	when the above bean (messenger) is instantiated, this custom
	BeanPostProcessor implementation will output the fact to the system console
	-->
	<bean class="scripting.InstantiationTracingBeanPostProcessor"/>

</beans>

注意 InstantiationTracingBeanPostProcessor 是如何仅被定义的。它甚至没有名字,而且因为它是一个 Bean,所以可以像其他任何 Bean 一样进行依赖注入。(前面的配置还定义了一个由 Groovy 脚本支持的 Bean。Spring 动态语言支持在题为动态语言支持的章节中有详细介绍。)

以下 Java 应用运行前面的代码和配置

  • Java

  • Kotlin

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 接口时,才能设置此属性。如果你编写自己的 BeanFactoryPostProcessor,也应考虑实现 Ordered 接口。更多详细信息,请参阅 BeanFactoryPostProcessorOrdered 接口的 javadoc。

如果你想修改实际的 Bean 实例(即从配置元数据创建的对象),那么你需要使用 BeanPostProcessor(如前面使用 BeanPostProcessor 定制 Bean中所述)。虽然在 BeanFactoryPostProcessor 中操作 Bean 实例在技术上是可能的(例如,使用 BeanFactory.getBean()),但这会导致 Bean 过早实例化,从而违反标准的容器生命周期。这可能会产生负面影响,例如绕过 Bean 的后处理过程。

此外,BeanFactoryPostProcessor 实例的作用域是按容器划分的。这只在使用容器层级结构时才相关。如果你在一个容器中定义一个 BeanFactoryPostProcessor,它只应用于该容器中的 Bean 定义。一个容器中的 Bean 定义不会被另一个容器中的 BeanFactoryPostProcessor 实例后处理,即使两个容器属于同一层级结构。

Bean 工厂后处理器在 ApplicationContext 中声明时会自动运行,以便对定义容器的配置元数据应用更改。Spring 包含一些预定义的 Bean 工厂后处理器,例如 PropertyOverrideConfigurerPropertySourcesPlaceholderConfigurer。你也可以使用自定义的 BeanFactoryPostProcessor,例如注册自定义的属性编辑器。

ApplicationContext 会自动检测部署到其中的、实现 BeanFactoryPostProcessor 接口的任何 Bean。它在适当的时候将这些 Bean 用作 Bean 工厂后处理器。你可以像部署任何其他 Bean 一样部署这些后处理器 Bean。

BeanPostProcessor 类似,你通常不希望为 BeanFactoryPostProcessor 配置延迟初始化。如果没有其他 Bean 引用 Bean(Factory)PostProcessor,该后处理器根本不会被实例化。因此,将其标记为延迟初始化将被忽略,即使你在 <beans /> 元素的声明中将 default-lazy-init 属性设置为 trueBean(Factory)PostProcessor 也会被立即实例化。

示例:类名替换 PropertySourcesPlaceholderConfigurer

你可以使用 PropertySourcesPlaceholderConfigurer 将 Bean 定义中的属性值使用标准的 Java Properties 格式外部化到单独的文件中。这样做可以使部署应用程序的人员定制环境特定的属性(例如数据库 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 定义的大多数属性和特性中的占位符。此外,你可以自定义占位符前缀和后缀。

随着 Spring 2.5 引入的 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,这发生在 ApplicationContextpreInstantiateSingletons() 阶段)bean 的解析会失败。

示例: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 的 bean,该 bean 具有 driverClassNameurl 属性。

还支持复合属性名称,前提是除了最终要被覆盖的属性之外,路径中的每个组件都已非空(presumably 通常由构造函数初始化)。在下面的示例中,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 Framework 中许多地方都有使用。Spring 自身就包含了 50 多个 FactoryBean 接口的实现。

当你需要容器返回实际的 FactoryBean 实例本身而不是它产生的 bean 时,在调用 ApplicationContextgetBean() 方法时,在 bean 的 id 前加上和号符号 (&)。因此,对于 idmyBean 的给定 FactoryBean,在容器上调用 getBean("myBean") 会返回 FactoryBean 的产品(即由 FactoryBean 创建的 bean),而调用 getBean("&myBean") 则返回 FactoryBean 实例本身。