(11)容器的拓展点

容器的扩展

通常来说,开发者不需要通过继承ApplicationContext来实现自己的子类扩展功能。但是Spring IoC容器确实可以通过实现接口来增加一些功能。下面将描述一下这些接口。
接下来主要分是哪个部分来讲解:

1.通过 BeanPostProcessor后置处理器来对一个bean(可以是监控bean或者增加bean)
2.通过BeanFactoryPostProcessor工厂后置bean处理器来定义配置元数据(容器实例初始话Bean之前就改变配置元数据)
3.自定义FactoryBean的初始化逻辑

1.通过 BeanPostProcessor后置处理器来对一个bean

BeanPostProcessor接口定义了一些回调方法,开发者可以通过实现来自己的实例化逻辑,依赖解析逻辑等等如果开发者只是想在Spring容器完成了实例化,配置以及初始化Bean之后来做一些操作的话,可以通过使用BeanPostProcessor来做到

开发者可以配置多个BeanPostProcessor实例,开发者可以通过配置order属性来指定配置的BeanPostProcessor的执行顺序。当然,想要配置顺序必须同时要实现Ordered接口。如果开发者写了自己的BeanPostProcessor,开发者最好同时考虑实现Ordered接口

BeanPostProcessors是操作Bean实例的,换言之,Spring IoC容器必须先初始化好Bean,然后BeanPostProcessors才开始工作
BeanPostProcessors作用范围是基于容器的。当然,只有当开发者使用容器的层级的时候才是需要考虑的。如果开发者在容器中定义了一个BeanPostProcessor,这个实例只会在它所在的容器来处理Bean初始化以后的操作。换言之,一个容器中的Bean不会被另一个容器中定义BeanPostProcessor的在初始化以后进行后续处理,甚至就算两个容器同属同一个容器的部分。

org.springframework.beans.factory.config.BeanPostProcessor接口包含了2个回调方法。当这个接口的实例注册到容器当中时,对于每一个由容器创建的实例,这个后置处理器都会在容器开始进行初始化之前获得回调的调用。后置处理器可以针对Bean实例采取任何的操作,包括完全无视回调函数。Bean的后置处理器通常检查回调接口或者将Bean用代理包装一下。一些诸如Spring AOP代理的基础类都是 通过Bean的后续处理器来实现的

ApplicationContext会自动检查配置的Bean是否有实现BeanPostProcessor接口,ApplicationContext会将这些Bean注册为后续处理器这样这些后续处理器就会在Bean创建之后调用Bean的后续处理器就像其他的Bean一样,由容器管理的

尽管Spring团队推荐的注册Bean的后续处理的方式是通过ApplicationContext的自动检查,但是Spring也支持通过编程的方式,通过addBeanPostProcessor方法。这种方式有的时候也很有用,当需要在注册前执行一些条件判断的时候,或者在结构化的上下文中复制Bean后续处理器的时候尤其有效。需要注意的是,通过编程实现的BeanPostProcessors是会忽略掉Ordered接口的:由编程注册的BeanPostProcessors总是在自动检查到的BeanPostProcessors之前来执行的,而回忽略掉明确的顺序定义。

容器会特殊对待那些实现了BeanPostProcessor接口的类。所有的BeanPostProcessors和他们所引用的Bean都会在启动时直接初始化作为ApplicationContext启动的一个特殊阶段。然后,所有的BeanPostProcessors会按顺序注册并应用到容器中的Bean

下面通过一个例子来演示一下

/**
 * @Project: spring
 * @description:  模拟一个普通的bean
 * @author: sunkang
 * @create: 2018-09-16 15:06
 * @ModificationHistory who      when       What
 **/
public class BeanDemo{
    public BeanDemo() {
        System.out.println("beanDemo已经初始化了");
    }
}
/**
 * 模拟 BeanPostProcessor来监控其他bean,进行相应的处理
 */
public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor,Ordered {

    //在bean初始化之后调用
    public Object postProcessBeforeInitialization(Object bean,
            String beanName) throws BeansException {
        System.out.println("beforeBean ''" + beanName + "'' created : " + bean.toString());
        return bean;
    }
    public Object postProcessAfterInitialization(Object bean,
            String beanName) throws BeansException {
        System.out.println("afterBean ''" + beanName + "'' created : " + bean.toString());
        return bean;
    }
    @Override
    public int getOrder() {
        return 0;
    }
}

在spring-extensionPoint.xml的配置中如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">


    <bean id="beanPostProcessor" class="com.spring.extensionPoint.InstantiationTracingBeanPostProcessor" />

    <bean class="com.spring.extensionPoint.BeanDemo"/>
</beans>

测试的演示:

ApplicationContext  context = new ClassPathXmlApplicationContext("extensionPoint/spring-extensionPoint.xml");

测试结果如下:可以发现BeanPostProcessor的两个回调方法在容器构造方法之后进行执行的

beanDemo已经初始化了
beforeBean ''com.spring.extensionPoint.BeanDemo#0'' created : com.spring.extensionPoint.BeanDemo@1753acfe
afterBean ''com.spring.extensionPoint.BeanDemo#0'' created : com.spring.extensionPoint.BeanDemo@1753acfe

spring的例子:RequiredAnnotationBeanPostProcessor
使用回调接口或者注解和BeanPostProcessor实现是常见的来扩展Spring IoC容器的方式。Spring中的一个例子就是RequiredAnnotationBeanPostProcessor就是用来确保JavaBean的标记有注解的属性确实注入了依赖

2.通过BeanFactoryPostProcessor定义配置元数据

org.springframework.beans.factory.config.BeanFactoryPostProcessor。语义上来说,这个接口有些类似BeanPostProcessor,只是有一个很大的区别:BeanFactoryPostProcessor操作Bean配置元数据,也就是说,Spring允许BeanFactoryPostProcessor来读取配置源数据,并且可能会在容器实例初始话Bean之前就改变配置元数据

如前文所述,开发者可以配置多个BeanFactoryPostProcessors,而且开发者可以控制其具体执行的顺序。当然,配置顺序是必须实现Ordered接口的。如果实现了自己的BeanFactoryPostProcessor,开发者也应该考虑实现Ordered接口

在ApplicationContext中声明了后续处理器,Bean的后续处理器就会自动的执行,来实现在配置中定义的行为。Spring包括一些预定义好的后续处理器都可以使用,比如PropertyOverrideConfigurerPropertyPlaceholderCOnfigurer,也能够使用自定义的BeanFactoryPostProcessor,比如,来注册自定义属性编辑器

例子,类名替换BeanFactoryPostProcessor
下面就开始演示一下

/**
 * @Project: spring
 * @description:   模拟PropertyPlaceholderConfigurer来进行测试
 * @author: sunkang
 * @create: 2018-09-16 16:15
 * @ModificationHistory who      when       What
 **/
public class DataSourceConfig {
    private String driverClassName;

    private  String url;

    private String username;

    private String password;

    public String getDriverClassName() {
        return driverClassName;
    }

    public void setDriverClassName(String driverClassName) {
        this.driverClassName = driverClassName;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "DataSourceConfig{" +
                "driverClassName='" + driverClassName + '\'' +
                ", url='" + url + '\'' +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

在extensionPoint目录下放置datasouce.properties文件,文件内容如下

jdbc.driverClassName=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:hsql://production:9002
jdbc.username=sa
jdbc.password=root

在spring-extensionPoint.xml配置如下

  <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations" value="classpath:extensionPoint/datasouce.properties"/>
    </bean>
  <bean id="dataSource"  class="com.spring.extensionPoint.DataSourceConfig">
        <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>

测试方法:

ApplicationContext  context = new ClassPathXmlApplicationContext("extensionPoint/spring-extensionPoint.xml");
 DataSourceConfig config=   context.getBean("dataSource",DataSourceConfig.class);
System.out.println(config);

测试结果如下:

DataSourceConfig{driverClassName='com.mysql.jdbc.Driver', url='jdbc:mysql:mydb', username='sa', password='root'}

Spring 2.5后引入了context命名空间,可以专用的配置元素来配置属性占位符。多个地址可以使用逗号分隔符。

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

PropertyPlaceholderConfigurer不仅仅查看开发者指定的Properties文件。默认的话,它如果不能再指定的属性文件中找到属性的话,仍然会检查Java的System配置。开发者可以通过配置systemPropertiesMode属性来修改这个行为,这个属性有如下三种值:
nerver(0):从不检查系统属性
fallback(1):如果没有从指定的属性文件中找到特定的属性时检查系统属性,这个是默认值
override(2):优先查找系统属性,而后才试着检查指定配置文件。系统的属性会覆盖指定文件的属性。

接下来看看PropertyPlaceholderConfigurer的源码,先看下PropertyPlaceholderConfigurer的继承关系,发现有beanFactoryPostProcessor


image.png

在PropertyResourceConfigurer实现了BeanFactoryPostProcessor,那么postProcessBeanFactory就是主入口了,后面的流程有兴趣可以自己看看。

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        try {
            //从特定的文件加载属性到mergedProps中
            Properties mergedProps = mergeProperties();
            
            // Convert the merged properties, if necessary.
            convertProperties(mergedProps);

            //这里的处理会properties属性,会生成一个解析器,然后注入到容器中,装载beand的时候再来回调这个解析器
            // Let the subclass process the properties.
            processProperties(beanFactory, mergedProps);
        }
        catch (IOException ex) {
            throw new BeanInitializationException("Could not load properties", ex);
        }
    }

另外一个例子是PropertyOverrideConfigurer
PropertyOverrideConfigurer,是另一个bean的后置处理器,有些类似于PropertyPlaceholderConfigurer,但是区别于后者,原有的定义可能有默认值,或者没有任何值。如果覆盖的Properties文件没有一个确切的Bean属性,就使用默认的定义

覆盖的属性如下,注意但是要求每一个路径上的组件,需要是非空的,也就是dataSource该组件不能为空

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

在具体的xml配置如下,那么dataSource.driverClassName和url将被覆盖,没有覆盖的值保持原样

   <bean class="org.springframework.beans.factory.config.PropertyOverrideConfigurer">
        <property name="locations" value="classpath:extensionPoint/override.properties"/>
    </bean>

在Spring 2.5引入的context命名空间中,也可以指定配置覆盖的文件属性如下:

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

3.自定义FactoryBean的初始化逻辑

FactoryBean接口是一种类似于Spring IoC容器的插件化的逻辑。如果当开发者的代码有复杂的初始化代码,在配置上使用Java代码比XML更有效时,开发者可以考虑创建自己的FacotoryBean对象,将复杂的初始化操作放到类中,将自定义的FactoryBean扩展到容器中

FacotryBean接口提供如下三个方法

Object getObject():返回一个工厂创建的对象。实例可被共享,取决于返回Bean的作用域为原型还是单例。
boolean isSingleton():如果FactoryBean返回单例,为True,否则为False
Class getObjectType():返回由getObject()方法返回的对象的类型,如果对象类型未知,返回null。
FactoryBean概念和接口广泛用预Spring框架,Spring本身就有多于50个FactoryBean的实现

当开发者需要一个FactoryBean实例而不是其产生的Bean的时候,在调用ApplicationContext的getBean()方法时,在其id之前加上&符号。也就是说,对于一个给定的FactoryBean,其id为myBean,调用getBean("myBean")返回其产生的Bean对象,而调用getBean("&myBean")返回FactoryBean实例本身

下面进行例子演示:

/**
 * @Project: spring
 * @description:  说明是一个factroyBean    是一个bean,但是是一个工厂bean
 * @author: sunkang
 * @create: 2018-09-16 16:34
 * @ModificationHistory who      when       What
 **/
public class FactoryBeanDemo implements FactoryBean {
    @Override
    public Object getObject() throws Exception {
        return "factoryBean";
    }

    @Override
    public Class<?> getObjectType() {
        return String.class;
    }
}

在spring-extensionPoint.xml具体的配置如下:

    <bean id="factoryBean" class="com.spring.extensionPoint.FactoryBeanDemo"/>

测试如下:

ApplicationContext  context = new ClassPathXmlApplicationContext("extensionPoint/spring-extensionPoint.xml");
//当其id为factoryBean,调用getBean("factoryBean")返回其产生的Bean对象,
//而调用getBean("&factoryBean")返回FactoryBean实例本身。
FactoryBeanDemo factoryBeanDemo =   context.getBean("&factoryBean",FactoryBeanDemo.class);
System.out.println("得到工厂:factory"+factoryBeanDemo);
//得到bean对象,其实是工厂bean调用了getObject方法产生的对象
System.out.println("得到具体的bean:"+context.getBean("factoryBean"));

结果如下: 结果很显然

得到工厂:factorycom.spring.extensionPoint.FactoryBeanDemo@5025a98f
得到具体的bean:factoryBean
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容