1.8 容器扩展点

通常,应用程序开发人员不需要子类化ApplicationContext实现类。相反,可以通过插入特殊集成接口的实现来扩展Spring IOC容器。接下来的几节将描述这些集成接口。

1.8.1 使用BeanPostProcessor自定义Beans

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

你可以配置多个BeanPostProcessor实例,并且可以通过设置Order属性来控制这些BeanPostProcessor实例的执行顺序。只有在BeanPostProcessor实现Ordered接口时,才能设置此属性。如果你编写自己的BeanPostProcessor,那么也应该考虑实现Ordered的接口。有关进一步的详细信息,请参阅BeanPostProcessor和有序接口的JavaDoc。另请参见BeanPostProcessor实例的编程注册说明。

BeanPostProcessor实例对bean(或对象)实例进行操作。也就是说,Spring IOC容器实例化一个bean实例,然后beanPostProcessor实例再继续完成它们的工作。

BeanPostProcessor实例的作用域是每个容器。这仅在使用容器层次结构时才相关。如果你在一个容器中定义了beanPostProcessor,那么它只post-processes该容器中的bean。换句话说,在一个容器中定义的bean不会由在另一个容器中定义的beanPostProcessor进行后处理,即使这两个容器是同一层次结构的一部分。

要更改实际的bean定义(即定义bean的蓝图),你需要使用beanFactoryPostProcessor。

org.springframework.beans.factory.config.BeanPostProcessor接口正好由两个回调方法组成。当此类注册为容器的post-processor时,对于容器创建的每个bean实例,post-processor在调用容器初始化方法(如InitializingBean.afterPropertiesSet()或任何声明的init方法)之前,以及在任何bean初始化之后,都会从容器中调用回调。post-processor可以对bean实例执行任何操作,包括完全忽略回调。bean post-processor通常检查回调接口,或者它可以用代理包装bean。为了提供代理包装逻辑,一些Spring AOP基础结构类被实现为bean post-processors。

ApplicationContext自动检测在实现BeanPostProcessor接口的配置元数据中定义的任何bean。ApplicationContext将这些bean注册为后处理器,以便在bean创建之后可以调用它们。bean post-processors 可以与任何其他bean相同的方式部署在容器中。

注意,当通过在configuration类上使用@Bean factory方法声明BeanPostProcessor时,factory方法的返回类型应该是实现类本身,或者至少是org.springframework.beans.factory.config.BeanPostProcessor接口,并清楚地表面该bean是post-processor。否则,ApplicationContext无法在完全创建它之前按类型自动检测到它。由于需要尽早实例化BeanPostProcessor以应用于上下文中其他bean的初始化,因此这种早期类型检测非常关键。

以编程方式注册BeanPostProcessor实例

虽然推荐的BeanPostProcessor注册方法是通过ApplicationContext自动检测(如前所述),但你可以使用addBeanPostProcessor方法在ConfigurableBeanFactory中以编程方式注册它们。 当你需要在注册之前评估条件逻辑,或者甚至需要跨层次结构中的上下文复制bean post processors时,这可能非常有用。但是,请注意,以编程方式添加的BeanPostProcessor实例并不尊重有序的接口。在这里,登记的顺序决定执行的顺序。还要注意,以编程方式注册的BeanPostProcessor实例总是在注册为自动检测的实例之前进行处理,而不接收任何显式排序。

BeanPostProcessor实例和AOP自动代理

实现BeanPostProcessor接口的类是特殊的,由容器进行不同的处理。作为ApplicationContext特殊启动阶段的一部分,所有BeanPostProcessor实例和这些实例直接引用的bean都在启动时实例化。

接下来,所有beanPostProcessor实例都以排序的方式注册,并应用于容器中所有其他bean。因为AOP自动代理是作为BeanPostProcessor本身实现的,所以BeanPostProcessor实例和它们直接引用的bean都不符合自动代理的条件,因此它们中没有aspects woven。

对于任何这样的bean,你都应该看到一条日志消息:bean somebean不适合被所有的BeanPostProcessor接口处理(例如:不适合自动代理)。

如果你使用autowiring或@Resource将bean连接到BeanPostProcessor,那么在搜索类型匹配的依赖项候选项时,Spring可能会访问意外的bean,因此,它们不适合自动代理或选用其他类型的post-processing方式。例如,如果你有一个用@Resource注解的依赖项,其中字段或setter名称与bean的声明名称不直接匹配,并且没有使用name属性,那么spring将通过类型来访问其他bean。

下面的例子展示了怎么在ApplicationContext中写,注册,和使用BeanPostProcessor。

例子Hello World, BeanPostProcessor-style

第一个例子是基本应用,这个例子展示了自定义的BeanPostProcessor实现,调用toString()方法并打印到系统输出。

下面是自定义BeanPostProcessor:

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;
}
}

下面的bean使用了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应用程序:

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 = (Messenger) ctx.getBean("messenger");
System.out.println(messenger);
}
}

程序的输出如下:

Bean 'messenger' created : [email protected]

例子:RequiredAnnotationBeanPostProcessor

将回调接口或注解与自定义BeanPostProcessor实现结合使用是扩展Spring IOC容器的常用方法。一个例子是Spring的RequiredAnnotationBeanPostProcessor-一个随Spring发行版一起提供的BeanPostProcessor实现,它要求用(任意)注解标记的bean上的JavaBean属性都需要一个真实值。

1.8.2 使用BeanFactoryPostProcessor自定义配置元数据

我们看到的下一个扩展点是org.springframework.beans.factory.config.BeanFactoryPostProcessor。此接口的语义与BeanPostProcessor的语义相似,但有一个主要区别:BeanFactoryPostProcessor对Bean配置元数据进行操作。也就是说,Spring IOC容器允许BeanFactoryPostProcessor读取配置元数据,并可能在容器实例化BeanFactoryPostProcessor实例以外的任何bean之前对其进行更改。

你可以配置多个BeanFactoryPostProcessor实例,并且可以通过设置Order属性来控制这些BeanFactoryPostProcessor实例的运行顺序。但是,只能在BeanFactoryPostProcessor实现Ordered接口时设置此属性。如果你编写自己的BeanFactoryPostProcessor,那么也应该考虑实现Ordered的接口。有关更多详细信息,请参阅BeanFactoryPostProcessor和Ordered接口的JavaDoc。

如果要更改实际的bean实例(即从配置元数据创建的对象),则需要使用BeanPostProcessor(如前面使用BeanPostProcessor自定义bean中所述)。虽然在技术上可以在BeanFactoryPostProcessor中使用bean实例(例如,通过使用BeanFactory.getBean()),但这样做会导致过早的bean实例化,违反标准容器生命周期。这可能会导致负面影响,例如绕过bean post处理。

此外,BeanFactoryPostProcessor实例的作用域是每个容器。这仅在使用容器层次结构时才相关。如果在一个容器中定义BeanFactoryPostProcessor,那么它只应用于该容器中的bean定义。一个容器中的bean定义不会由另一个容器中的BeanFactoryPostProcessor实例进行后处理,即使这两个容器属于同一层次结构。

bean工厂后处理器在ApplicationContext中声明时自动执行,以便将更改应用于定义容器的配置元数据。Spring包含许多预定义的bean工厂后处理器,例如PropertyOverrideConfigurer和PropertyPlaceholderConfigurer。你还可以使用自定义BeanFactoryPostProcessor-来注册自定义属性编辑器。

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

与BeanPostProcessors一样,你通常不希望为延迟初始化配置BeanFactoryPostProcessors。如果没有其他bean引用bean(工厂)后处理器,则该后处理器将不会被实例化。因此,将其标记为lazy初始化将被忽略,并且即使在你的元素的声明中将default-lazy-init属性设置为true,Bean(Factory)PostProcessor也将被优先实例化。

PropertyPlaceholderConfigurer类名替换

通过使用标准Java Properties格式,可以使用PropertyPlaceholderConfigurer从一个单独的文件中的bean定义外部化属性值。这样做可以使部署应用程序的人员自定义特定于环境的属性,如数据库URL和密码,而不必为容器修改一个或多个XML定义主文件从而增加复杂性或风险。

考虑以下基于XML的配置元数据片段,其中定义了具有占位符值的数据源:

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations" value="classpath:com/something/jdbc.properties"/>
</bean>
<bean id="dataSource" destroy-method="close"
class="org.apache.commons.dbcp.BasicDataSource">
<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文件中。在运行时,使用PropertyPlaceholderConfigurer将元数据替换成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',其他的占位符也会被替换成相应的值。PropertyPlaceholderConfigurer检查bean定义的大多数属性和属性中的占位符。此外,还可以自定义占位符前缀和后缀。

使用Spring2.5引入的context命名空间,可以使用专用配置元素配置属性占位符。可以在“位置”属性中以逗号分隔的列表形式提供一个或多个位置,如下示例所示:

<context:property-placeholder location="classpath:com/something/jdbc.properties"/>

PropertyPlaceholderConfigurer不仅在指定的属性文件中查找属性。默认情况下,如果它在指定的属性文件中找不到属性,它也会检查Java系统属性。可以通过使用以下三个支持的整数值之一设置配置器的systemPropertiesMode属性来自定义此行为:

  • never (0): 从不检查系统属性。

  • fallback (1): 如果属性在声明的文件里面找不到,则去检查系统属性。这个是默认的模式。

  • override (2): 先检查系统属性,这会让系统属性覆盖其他的来源。

你可以使用PropertyPlaceholderConfigurer来替换类的名称,这个在运行时需要类的特殊实现时很有用,如下例子所示:

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<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的解析将失败,这是在非lazy init bean的ApplicationContext的preInstantiateSingletons()阶段进行的。

例子PropertyOverrideConfigurer

PropertyOverrideConfigurer是另外一个post-processor bean工程,和PropertyPlaceholderConfigurer相似,但是和它不同的是,对bean的定义来说原始定义可以有默认值或者没有任何值。 如果overriding Properties没有bean某个特定属性,那么会使用默认的上下文定义。

注意,bean定义不知道被覆盖,所以从XML定义文件中看,并不知道使用的是 override configurer。如果多个PropertyOverrideConfigurer实例为同一个bean属性定义不同的值,则最后一个实例将因覆盖机制而获胜。

属性文件配置行如下格式:

beanName.property=value

下面是例子:

dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql:mydb

此示例文件可以与包含名为dataSource的bean的容器定义一起使用,该bean具有driver和url属性。

也支持复合属性名,只要路径的每个组件(被重写的最终属性除外)已经非空(可能由构造函数初始化)。在下面的示例中,tom bean的fred属性的bob属性的sammy属性设置为标量值123:

tom.fred.bob.sammy=123

指定的重写值始终是文本值。它们不会转换为bean引用。当XML bean定义中的原始值指定bean引用时,此约定也适用。

使用Spring2.5引入的context命名空间,可以使用专用配置元素配置属性占位符。可以在“位置”属性中以逗号分隔的列表形式提供一个或多个位置,如下示例所示:

<context:property-override location="classpath:override.properties"/>

1.8.3 使用FactoryBean自定义实例化逻辑

你可以为本身是工厂的对象实现org.springframework.beans.factory.factorybean接口。

FactoryBean接口是Spring IOC容器实例化逻辑的一个可插入点。如果需要在java中实现初始化代码,而不是一个(潜在的)冗长的XML中,那么你可以创建自己的FactoryBean,在该类中编写复杂的初始化,然后将自定义的FactoryBean插入容器中。

FactoryBean接口提供3个方法:

  • Object getObject(): 返回工厂创建的实例,该实例可能是被共享的, 取决于该实例是单例还是多例模式。

  • boolean isSingleton():判断FactoryBean返回的是单例还是多例。

  • Class getObjectType():返回getObject() 方法返回的类型,如果提前不知道类型,那么返回null。

factrybean概念和接口在Spring框架中的许多地方使用。超过50个FactoryBean接口实现与Spring本身一起提供。

当需要向容器请求实际的FactoryBean实例本身而不是它生成的bean时,在调用ApplicationContext的getbean()方法时,在bean的ID前面加上符号(&)。因此,对于ID为myBean的给定FactoryBean,在容器上调用getBean(“myBean”)返回FactoryBean生成的bean,而调用getBean(“&myBean”)则返回FactoryBean实例本身。