译 - Spring 核心技术之 Spring 容器扩展点

3,586 阅读13分钟

前言

本文内容选自 Spring Framework 5.1.6.RELEASE 官方文档中 core 部分的 1.8 小节,简单介绍了如何利用 Spring 容器扩展点进行定制化扩展,以及注意点。若有任何问题,欢迎交流。

原文地址:docs.spring.io/spring/docs…

正文 1.8. Spring 容器扩展点

通常,一位应用开发者不需要继承 ApplicationContext 实现类。相反,Spring IoC 容器能够通过插入特殊的集成接口来实现扩展。下面一些章部分内容描述了这些集成接口。

1.8.1 用 BeanPostProcessor 定制 Beans

BeanPostProcessor 接口定义了允许实现的回调方法,来用于提供自己(或者覆盖容器默认)的初始化逻辑,依赖处理逻辑等等。如果你想要在 Spring 容器完成容器初始化,配置和初始化 Bean 之后实现一些定制的逻辑,你可以通过插入一个或者多个定制的 BeanPostProcessor实现。

你可以配置多个 BeanPostProcessor 实例,并且通过设置 order 属性来控制这些 BeanPostProcessor 实例的执行顺序。只有当你的 BeanPostProcessor 实现了 Ordered 接口才能设置这个属性。如果你要实现自己 BeanPostProcessor ,你也应该考虑实现 Ordered 接口。有关详细信息,可见 BeanPostProcessorOrdered 的javadoc 。也可以参考 programmatic registration of BeanPostProcessor instances上的注释。

BeanPostProcessor 实例作用于 Bean(或者对象)实例上。也就是说,Spring IoC 容器初始化一个 Bean 实例,然后BeanPostProcessor 实例完成它们的工作。

BeanPostProcessor 实例作用范围于每个容器。这仅当你使用到容器的层次结构才有关。如果你在一个容器里定义了一个 BeanPostProcessor,它只会后置处理这个容器下的 beans。换句话说,定义在一个容器的 beans 不能被定义在另个容器里的 BeanPostProcessor 对象执行后置处理,即使这些容器在同一个层级下。

想要改变 Bean 定义(也就是说,定义 Bean 的蓝图),你需要使用 BeanFactoryPostProcessor,如 Customizing Configuration Metadata with a BeanFactoryPostProcessor 所描述的。

org.springframework.beans.factory.config.BeanPostProcessor 接口由两个回调方法组成,当一个类在容器里作为后置处理器注册时,对于每个由这个容器创建的 bean 实例,后置处理器会在容器初始化方法(例如 InitializingBean.afterPropertiesSet() 或者任何声明 init 方法)调用前得到回调,并且在任何 bean 初始化之后得到回调。一个 Bean 后置处理器通常在回调接口用于检查,或者它可能使用一个代理对一个 bean 进行包装。一些 SpringAOP 基础结构的类就是用通过 bean 后置处理器实现的,以便提供代理包装的逻辑。

ApplicationContext 自动检测在配置元信息里那些实现了 BeanPostProcessor 接口的 beans。ApplicationContext 会将这些 beans 注册为后置处理器,以便于后面在 bean 创建时被调用。Bean 后置处理器可以像采用其他 beans一样的方法部署在容器中。

注意的是,但在一个配置类通过 @Bean 工厂方法声明一个 BeanPostProcessor 时,这个工厂方法的返回类型应该是这个实现类本身,或者 org.springframework.beans.factory.config.BeanPostProcessor 接口,明确指明这个 bean拥有 后置处理器的性质。否则,ApplicationContext 无法在完全创建它之前,通过类型自动检测到它。由于BeanPostProcessor 需要过早实例化以便于作用于在同个上下文的其他 bean 实例化,因此这种前期的类型检测至关重要。

编程方式注册 BeanPostProcessor 实例

虽然 BeanPostProcessor 注册的推荐方式为让 ApplicationContext 自动检测(如之前描述的一样),你可以注册他们通过编程方式,通过 ConfigurableBeanFactory 使用 addBeanPostProcessor方法。当你需要在注册前处理条件逻辑,或者在一个层次里跨上下文拷贝bean后置处理器时所有帮助。然而要注意的是,以编程方式添加的BeanPostProcessor实例不遵循Ordered接口。这里,注册的顺序确定了执行的顺序。也要注意的是,通过编程方式注册的 BeanPostProcessor 实例总是在通过自动检测 注册的实例之前处理,任何显式的排序不会起作用。

BeanPostProcessor 实例和 AOP 自动代理

在容器中实现了 BeanPostProcessor 接口的类是特殊的,且被区别对待。作为特殊启动阶段的一部分,所有 BeanPostProcessor 实例以及他们所直接引用的 beans 都在启动时实例化。接下来,所有 BeanPostProcessor 实例将以有序的方式注册,并作用到当前容器中所有其他 beans。因为 AOP 自动代理是基于 BeanPostProcessor 实现的,BeanPostProcessor 实例以及他们直接引用的beans不符合自动代理的条件,因此这些 bean无法被切面织入。

对于这样的 bean,你应该会看到一个信息日志消息:Bean someBean is not eligible for getting processed by all BeanPostProcessor interfaces (for example: not eligible for auto-proxying).

如果你通过自动注入或者 @Resource方式在你的 BeanPostProcessor 注入beans,当 Spring 基于类型匹配的依赖候选时,Spring 可能会访问到非所期望的 beans ,因此,他们不适合自动代理或者其他类型的 bean 后置处理器。例如,你有一个依赖标记了@Resource,,而这个字段或者 setter 方法名没有直接对应 bean 的声明名称,也没有使用到名称属性,Spring 会按照类型匹配他们访问其他 beans

接下来的示例展示了在 ApplicationContext 中如何写,注册以及使用 BeanPostProcessor 实例。

示例:Hello World,BeanPostProcessor-style

第一个实例演示了基本用法,这个示例展示了一个定制的 BeanPostProcessor 实现,其调用了每个通过这个容器创建的 bean 的 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;
    }
}

下面使用 InstantiationTracingBeanPostProcessor 的 beans 元素

<?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 动态语言支持在 Dynamic Language Support一章中详细介绍。

下面 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 : org.springframework.scripting.groovy.GroovyMessenger@272961
org.springframework.scripting.groovy.GroovyMessenger@272961
示例:RequiredAnnotationBeanPostProcessor

一种常用扩展 Spring IoC 容器的方法就是将回调接口或注解与定制的BeanPostProcessor实现结合使用。Spring的RequiredAnnotationBeanPostProcessor就是这样的例子,一个 BeanPostProcessor 实现,在Spring 运行阶段确保 beans 上被特定注解标记的JavaBean 属性能用值进行依赖注入。

1.8.2 用 BeanFactoryPostProcessor定制配置的元数据

下个扩展点我们来看下 org.springframework.beans.factory.config.BeanFactoryPostProcessor.这个接口的语义与 BeanPostProcessor 类似,主要的不同在于:BeanFactoryPostProcessor 操作于 Bean 的配置元数据。也就是说,Spring IoC 容器让 BeanFactoryPostProcessor 读取配置元数据,在容器实例化除了 BeanFactoryPostProcessor 实例之外的 Beans 之前,改变其配置元数据。

你可以配置多个BeanFactoryPostProcessor 实例,并且通过设置 order 属性,来控制这些 BeanFactoryPostProcessor 实例的运行顺序。但只有 BeanFactorPostProcessor 实现了 Ordered 接口,才能设置这个属性。如果你实现了自己的 BeanFactoryPostProcessor,你也需要考虑实现Ordered 接口。有关详细信息,可见 BeanFactoryPostProcessorOrdered 的 javadoc 。

如果你想要改变 Bean 实例,那么你应该使用 BeanPostProcessor (描述于之前的 Customizing Beans by Using a BeanPostProcessor)虽然在技术上是可以用 BeanFactoryPostProcessor (例如,使用 BeanFactory.getBean())实现,但这样做会造成让 bean 过早的实例化,违背了标准的容器生命周期。这样可能会产生负面作用,如绕过 Bean 常规的后置处理。

除此之外,BeanFactoryPostProcessor实例作用范围于每个容器。仅当你使用到容器的层次结构时才相关。如果你在一个容器里定义了一个 BeanFactoryPostProcessor,它只能作用于在这个容器里的 bean 定义。即使这些容器在同一个层次结构里,一个容器的Bean 定义不会被定义在另一个容器里的 BeanFactoryPostProcessor 实例进行后置处理。

为了将定义容器的配置元数据的改变生效,当bean 工厂后置处理器声明在 ApplicationContext 中,就会自动执行。Spring 包含了许多预定义的 bean 工厂后置处理器,例如 PropertyOverrideConfigurerPropertyPlaceholderConfigurer。你也可以使用定制的 BeanFactoryPostProcessor - 例如,注册定制的属性编辑器。

ApplicationContext 会自动检测到声明在自己内部实现了 BeanFactoryPostProcessor 接口的的 beans。它会在合适的时机,将这些 beans 作为 bean 工厂后置处理器。你可以像其他 beans 一样声明这些后置处理器 beans。

BeanPostProcessor 一样,你通常不想配置 BeanFactoryPostProcessor 后被延时初始化。如果没有其他 bean 引用BeanFactoryPostProcessor,那么这个后置处理器根本不会被实例化。因此,延迟加载的标记会被忽略,即使你在元素的声明中将default-lazy-init属性设置为true,BeanFactoryPostProcessor 也会尽早地实例化。

示例:类名替换 PropertyPlaceholderConfigurer

你可以使用 PropertyPlaceholderConfigurer 从一个独立的使用标准 Java Properties 格式的文件来表达一个bean 定义的属性值。这样做让人们根据环境特定的属性来部署应用,如数据库 URLs 和密码,没有了修改主配置 XML文件或者容器文件的复杂和风险。

参考下面基于 XML的 配置元数据的片段,里面使用占位值声明了一个 dataSource

<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 定义的属性。此外,你可以定制占位符的前缀和后缀。

在 Spring 2.5 引入的 context 命名空间里,你可以用专门配置元素来配置属性占位符。你可以在 location 属性里提供一个或多个位置用逗号隔开的列表,如下面例子所示:

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

PropertyPlaceholderConfigurer 不仅在你限定的 Properties 文件里查找属性。默认情况下,如果不能再特定属性文件中找到属性,它也会在 Java 的System 属性上检查。你可以通过设置配置对象的 systemPropertiesMode 属性定制这个行为,以下是它所支持的三个整数值:

  • never(0):从不检查系统属性。
  • fallback(1):如果在给定属性文件没有解析到,就检查系统属性。这是默认的行为。
  • override(2):在解析特定属性文件之前,首先检查系统属性。这使得系统属性可以覆盖任何其他属性源。

有关详细信息,可见PropertyPlaceholderConfigurer javadoc。

你可以使用 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 的解析会失败。这样将发生于 ApplicationContext里 非懒加载 bean的preInstantiateSingletons阶段。

示例:PropertyOverrideConfigurer

PropertyOverrideConfigurer,另一个bean 工厂后置处理器,与 PropertyPlaceholderConfigurer 很相似,但是不同于后者,对于 bean 属性,原始定义可以具有默认值或者没有值。如果一个覆盖的 Properties 文件没有某个 bean 属性时,默认上下文的定义会被使用。

请注意,bean 定义是不会感知到被覆盖,因此不能立即看出是XML 定义文件覆写了在使用的配置。如果有多个 PropertyOverrideConfigurer 实例定义了一个 bean 属性但不同的值,那么由于覆写机制,最后定义的一个值会生效。

Properties 文件配置行都采用以下格式:

beanName.property=value	

下面列举了示例的格式:

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

示例配置文件可用于容器中定义了名为 dataSource 的 bean的 driverurl 属性。

也支持复合属性名称,只要路径的每个组件(被重写的最终实现属性除外)都是非 null(都由构造函数初始化)。

下面的示例中,名为 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"/>

1.8.3 用 FactoryBean 定制实例化逻辑

你可以实现 org.springframework.beans.factory.FactoryBean 接口来创建本身是工厂的对象。

FactoryBean 接口对 Spring IoC 容器实例化逻辑实现是可插拔的。如果你有复杂的初始化代码,使用 Java 代码 好于冗长的 XML 配置,你可以创建自己的 FactoryBean,在这个类里写复杂的实例化,并且将定制的 FactoryBean 插入到容器中。

FactoryBean 接口提供了三个方法:

  • Object getObject(): 返回工厂创建的实例对象。这个实例可能是共享的,这个依赖于这个工厂师傅返回单例对象还是原型对象。
  • boolean isSignletion(): 如果 FactoryBean 返回单例对象则返回 true,否则为 false
  • Class getObjectType(): 返回 方法 getObject()的对象的类型,如果类型还没确定则返回 null

FactoryBean概念和实现用于 Spring Framework 的许多处地方,Spring 自身提供了超过 50 多种的 FactoryBean 实现。

当你需要向一个容器访问特定 FactoryBean 实例而不是它产生的 beans 时,在用 ApplicationContextgetBean 方法时,使用 & 符号作用 bean 的 id 前缀。例如,给定一个 id 为 myBean 的 FactoryBean ,调用 getBean("myBean") 可以获得 FactoryBean 生成的 bean,而调用 getBean("&myBean") 返回 FactoryBean 实例本身。