IoC容器8——容器扩展点

原创
2017/07/05 16:39
阅读数 107

容器扩展点

一般的,一个应用开发者不需要扩展ApplicationContext的实现类。相反,Spring IoC容器可以通过插入特殊的集成接口的实现被扩展。

1 使用 BeanPostProcessor 自定义bean

BeanPostProcessor接口定义了一些回调函数,可以通过实现它们提供自己的(覆盖容器默认的)实例化逻辑、依赖关系解析逻辑等等。如果想在Spring容器完成实例化、配置和初始化bean后实现一些用户逻辑,可以插入一个或多个BeanPostProcessor实现。

可以配置多个BeanPostProcessor实例,并且可以通过设置order属性来控制BeanPostProcessor的执行顺序。只有BeanPostProcessor实现了Ordered接口才能设置这个属性;如果自己编写BeanPostProcessor应该考虑实现Ordered接口。

BeanPostProcessor操作bean或者对象的实例,因此Spring IoC容器实例化一个bean的实例然后BeanPostProcessor开始工作。

BeanPostProcessor的作用域是每个容器一个。这与使用容器层次结构有关。如果在一个容器中定义了一个BeanPostProcessor,它仅仅“后处理”这个容器中的bean。也就是说,在一个容器中定义的bean不会被另一个容器定义的BeanPostProcessor后处理,即使两个容器是同一层次结构的一部分。

为了改变实际的bean定义(即定义bean的蓝图),需要使用BeanFactoryPostProcessor。

org.springframework.beans.factory.config.BeanPostProcessor接口包括两个回调方法。当这样的类被注册为容器的后处理器,对于容器创建的每一个bean实例,后处理器从容器获取回调,在容器调用初始化方法(例如InitializingBean的afterPropertiesSet()以及任何声明初始化方法)之前以及任何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的自动探测机制(上文所述),但是也可以使用ConfigurableBeanFactory的addBeanPostProcessor方法编程的注册它们。这在需要注册之前执行条件逻辑或者在层次结构中将bean后处理器拷贝到其它上下文时有用。需要注意的是通过编程方法添加BeanPostProcessor不遵守Ordered接口。这里注册的顺序意味着执行的顺序。此外编程方法注册的后处理器总是在自动检测方法注册的后处理器之前执行,不管任何顺序。

实现了BeanPostProcessor接口的类是特殊的并且被容器不同对待。所有的BeanPostProcessor和它们的直接依赖在启动时实例化,作为ApplicationContext特殊启动阶段的一部分。然后,所有BeanPostProcessor以排序的方式注册并且应用于容器中的所有其它bean。因为AOP自动代理自身也实现了BeanPostProcessor,所以BeanPostProcessor和它的直接依赖都不能被自动代理,因此不要对它们编制面。

对任何这样的bean(使用了自动代理),可以看到消息日志:“Bean foo is not eligible for getting processed by all BeanPostProcessor interfaces (for example: not eligible for auto-proxying)”。

注意,如果bean使用自动装配或者@Resource(可能回退到自动装配)注入到BeanPostProcessor,当搜索类型匹配的依赖候选者时,Spring也许访问了不期望的bean,因此使得它们不能被自动代理者或者使得它们成为其它类型的bean后处理器。例如,如果使用@Resource注释的依赖项,其中字段/ setter名称与bean的已声明名称不直接对应并且没有使用name属性,那么Spring将访问其他bean以便按类型匹配它们。

下面的例子展示如何在ApplicationContext中编写、注册和使用BeanPostProcessor。

例子:Hello World,BeanPostProcessor风格

第一个例子展示基础用法。例子展示一个用户BeanPostProcessor实现,它当容器创建每一个bean的时候调用bean的toString()方法并且把结果字符串打印到系统控制台。

package scripting;

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

public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {

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

    public Object postProcessAfterInitialization(Object bean,
            String beanName) throws BeansException {
        System.out.println("Bean '" + beanName + "' created : " + bean.toString());
        return bean;
    }
}
<?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
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/lang
        http://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。

下面的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

使用回调接口或者注解关联用户BeanPostProcessor实现是一种扩展Spring IoC容器的常见方法。一个例子是Spring的RequiredAnnotationBeanPostProcessor——Spring distribution附带的BeanPostProcessor实现,它保证使用注解注释的JavaBean的属性确实依赖注入了值。

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

org.springframework.beans.factory.config.BeanFactoryPostProcessor是另一个扩展点。这个接口的语义与BeanPostProcessor类似,但有一点主要的不同:BeanFactoryPostProcessor操作bean配置元数据;即Spring IoC容器允许BeanFactoryPostProcessor读取配置元数据并且在容器实例化除BeanFactoryPostProcessor之外的任何bean之前改变它。

可以配置多个BeanFactoryPostProcessor,可以控制BeanFactoryPostProcessor的执行顺序,通过设置order属性。然而只有当BeanFactoryPostProcessor实现了Ordered接口才能设置order属性。如果自己实现BeanFactoryPostProcessor,建议实现Ordered接口。

如果想要改变实际的bean实例(即根据配置元数据创建的对象),需要使用BeanPostProcessor。尽管技术上使用BeanFactoryPostProcessor来修改bean实例(例如使用BeanFactory.getBean()),这么做导致bean过早的实例化,违反了标准的容器生命周期。这可能会导致负作用,如bypassing bean 后处理(???)。

BeanFactoryPostProcessor的作用域是每个容器一个。这与使用容器层次结构有关。如果在一个容器中定义了一个BeanFactoryPostProcessor,它仅仅应用于这个容器中的bean定义。也就是说,在一个容器中中的bean定义不会被另一个容器定义的BeanFactoryPostProcessor后处理,即使两个容器是同一层次结构的一部分。

当一个bean工厂后处理器在ApplicationContext中声明,它会被自动执行,以便对容器中的配置元数据定义进行修改。Spring包含了一些预定义的bean工厂后处理器,例如PropertyOverrideConfigurer和PropertyPlaceholderConfigurer。也可以使用一个用户定义的BeanFactoryPostProcessor,例如注册自定义属性编辑器(???)。

ApplicationContext自动检测发布在其中的实现BeanFactoryPostProcessor接口的bean。它在适当的时候把这些bean作为bean工厂后处理器使用。可以像其它bean一样发布这些后处理器bean。

与BeanPostProcessor一样,通常不需要将BeanFactoryPostProcessor配置为懒加载。如果是懒加载,如果没有其它bean引用一个Bean(Factory)PostProcessor,后处理器将不会被实例化。因此,将其标注懒加载将会被忽略,并且Bean(Factory)PostProcessor会被立即实例化,甚至在<bean/>元素中将default-lazy-init属性设置为true。

例子:类名替换 PropertyPlaceholderConfigurer

可以使用PropertyPlaceholderConfigurer可以将bean定义的属性外部化到单独的使用标准Java属性格式文件中。这么做可以在发布应用时自定义面向环境的属性例如数据库URL、密码,从而避免修改主XML定义文件或者容器文件的复杂性和风险。

考虑基于XML的配置元数据段,定义了使用占位符值的DataSource。下面的例子展示了在外部Properties文件定义属性值。在运行时,PropertyPlaceholderConfigurer被应用于元数据,替代Datasource的一些属性。被替代的值被指定为${property-name}形式、遵循Ant/log4j/JSP EL风格的占位符。

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="locations" value="classpath:com/foo/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>

实际的值在另一个文件中,使用标准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/foo/jdbc.properties"/>

PropertyPlaceholderConfiguerer不仅仅在指定的Properties文件中查找属性。默认的,如果没有在指定的配置文件中找到,它也检查Java系统属性。可以自定义这种行为,通过设置systemPropertiesMode属性为以下支持的整数值:

  • never(0): 从不检查系统属性;
  • fallback(1): 如果在指定的配置文件中无法解析属性,则查找系统属性,这是默认的行为;
  • override(2): 首先检查系统属性,在试图解析指定的配置文件之前。这种行为运行系统属性覆盖其它属性源。

可以使用PropertyPlaceholderConfigurer替换类名,当需要在运行时选择一个特定的实现类时这很有用。例如:

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="locations">
        <value>classpath:com/foo/strategy.properties</value>
    </property>
    <property name="properties">
        <value>custom.strategy.class=com.foo.DefaultStrategy</value>
    </property>
</bean>

<bean id="serviceStrategy" class="${custom.strategy.class}"/>

如果无法在运行时解析为有效的类,那么在创建bean时会解析失败,这是在ApplicationContext非延迟初始化Bean的preInstantiateSingletons()阶段期间。

例子: PropertyOverrideConfigurer

PropertyOverrideConfigurer是另一个bean工厂后处理器,类似PropertyPlaceholderConfigurer,但是与之不同的是,原始的bean定义可以使用默认值或者不使用值。如果覆盖的配置文件没有一个bean属性的键,将使用默认上下文定义。

请注意,bean定义不知道被覆盖,所以从XML定义文件不能判断覆盖配置程序正在被使用。在多个PropertyOverrideConfigurer实例为同一个bean属性定义不同的值的情况,由于覆盖机制会使用最后一个。

配置文件定义行使用下面的格式:

beanName.property=value

例如:

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

这个示例文件可以被用于一个包含数据源bean的容器定义中,数据源包含driver和url属性。

也支持复合属性名,只要出了最终被覆盖的属性之外的每个组件都是非空的(一般由构造函数实例化)。在下面的例子中:

foo.fred.bob.sammy=123

foo bean的fred属性的bob属性的sammy属性被设置为纯量123.

指定的覆盖值总是字面值,它们不能被翻译成bean引用。当XML中的bean定义的原始值指定了 bean 引用时,这个约定也适用。

使用Spring 2.5引入的context命名空间,可以使用专用的配置元素配置属性覆盖。

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

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

实现org.springframework.beans.factory.FactoryBean接口的对象自身就是工厂类。

FactoryBean接口是Spring IoC容器的实例化逻辑的可插入点。如果有复杂的实例化代码,用Java代码可以更好的表达,而不是(可能)冗长的XML,可以创建自己的FactoryBean,在类中编写复杂的实例化逻辑,然后将自定义的FactoryBean插入容器中。

FactoryBean接口提供三个方法:

  • Object getObject():返回工厂创建的一个对象实例。实例可能被共享,取决于工厂返回singletons还是prototypes。
  • boolean isSingleton():如果FactoryBean返回singleton,则返回true,否则返回false。
  • Class getObjectType():返回对象的类型,如果实现不知道类型则返回null。

FactoryBean的概念和接口在Spring Framework中的许多地方都被用到;Spring自身有超过50个FactoryBean接口的实现。

如果需要向容器获取一个实际的FactoryBean实例自身替代它生产的bean,在调用ApplicationContext的getBean()方法时,在bean的id之前加&符号。所以,对于给定的FactoryBean,id为myBean,调用容器的getBean("myBean")返回FactoryBean生产的对象,调用getBean("&myBean")返回FactoryBean实例本身。

展开阅读全文
加载中

作者的其它热门文章

打赏
0
0 收藏
分享
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部