Spring 源码学习(七) 扩展功能 下篇-BeanPostProcessor

spring 系列 转载自掘金 VipAugus https://juejin.cn/user/2348212565601415/posts

在上一篇文章中,深入分析和学习了 BeanFactoryPostProcessor ,主体是 BeanFactory 的后处理器,这次来学习主体是 Bean 的后处理器:BeanPostProcessor

定义:它也是 Spring 对外提供的接口,用来给用户扩展自定义的功能。执行的时机在 bean 实例化阶段前后

本篇思路:

  1. BeanPostProcessor 定义
  2. 如何使用
  3. 代码实现分析
  4. 介绍剩余的扩展功能

前言

BeanFactoryPostProcessor 不同的是,BeanFactoryPostProcessor 的注册和执行都在同一个方法内,而 BeanPostProcessor 分开两个方法,分为注册调用两个步骤。

常规的 BeanFactory 中是没有实现后处理器的自动注册,所以在调用的时候没有进行手动注册是无法使用的,但在 ApplicationContext 中添加了自动注册功能(在这个 registerBeanPostProcessors 方法中),最后在 bean 实例化时执行 BeanPostProcessor 对应的方法。

本次主要介绍 BeanPostProcessor,同时也会将剩下的 context 扩展功能一起学习~


BeanPostProcessor

经过上一篇文章的学习,应该对 bean 的后处理理解起来更顺利,下面直奔主题,来看下它是如何使用和结合源码分析


如何使用

新建一个 bean 后处理器

这个后处理器需要引用 InstantiationAwareBeanPostProcessor 接口(实际继承自 BeanPostProcessor),然后重载以下两个方法:

public class CarBeanPostProcessor implements InstantiationAwareBeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        // 这里没有区分 bean 类型,只是用来测试打印的顺序和时间
        System.out.println("Bean name : " + beanName + ", before Initialization, time : " + System.currentTimeMillis());
        return null;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("Bean name : " + beanName + ", after Initialization, time : " + System.currentTimeMillis());
        return null;
    }
}
复制代码

在配置文件中注册 bean-post-processor.xml

在配置文件配置我们写的自定义后处理器和两个普通 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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- beanPostProcessor -->
    <bean id="carPostProcessor" class="context.bean.CarBeanPostProcessor"/>

    <!--用以下两个 bean 进行测试打印时间和顺序-->
    <bean id="car" class="base.factory.bean.Car">
        <property name="price" value="10000"/>
        <property name="brand" value="奔驰"/>
    </bean>

    <bean id="book" class="domain.ComplexBook"/>

</beans>
复制代码

启动代码和打印结果

public class CarBeanPostProcessorBootstrap {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("factory.bean/bean-post-processor.xml");
        Car car = (Car) context.getBean("car");
        ComplexBook book = (ComplexBook) context.getBean("book");
        System.out.println(car);
        System.out.println(book);
    }
}
复制代码

输出:

Bean name : car, before Initialization, time : 1560772863996
Bean name : car, after Initialization, time : 1560772863996
Bean name : book, before Initialization, time : 1560772863999
Bean name : book, after Initialization, time : 1560772863999
Car{maxSpeed=0, brand='奔驰', price=10000.0}
domain.ComplexBook@77be656f
复制代码

从输出接口看出,打印顺序是先框架内部,再到应用层,框架内部中,在顺序实例化每个 bean 时,前面也提到执行时机:先执行 postProcessBeforeInitialization 方法,然后实例化 bean 后,执行 postProcessAfterInitialization。

所以我们重载的两个接口按照前后顺序打印出来了~


注册 BeanPostProcessor

上面介绍了使用例子,应该不难理解,接着来看下源码注册的方法:

org.springframework.context.support.AbstractApplicationContext#registerBeanPostProcessors

实际委托给了 PostProcessorRegistrationDelegate.registerBeanPostProcessors(beanFactory, this);

public static void registerBeanPostProcessors(
            ConfigurableListableBeanFactory beanFactory, AbstractApplicationContext applicationContext) {
        // 注释 7.2 从注册表中取出 class 类型为 BeanPostProcessor 的 bean 名称列表
        String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false);

        int beanProcessorTargetCount = beanFactory.getBeanPostProcessorCount() + 1 + postProcessorNames.length;
        beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker(beanFactory, beanProcessorTargetCount));
        // 将带有 权限顺序、顺序和其余的 beanPostProcessor 分开
        List<BeanPostProcessor> priorityOrderedPostProcessors = new ArrayList<>();
        // 类型是 MergedBeanDefinitionPostProcessor
        List<BeanPostProcessor> internalPostProcessors = new ArrayList<>();
        List<String> orderedPostProcessorNames = new ArrayList<>();
        List<String> nonOrderedPostProcessorNames = new ArrayList<>();
        for (String ppName : postProcessorNames) {
            // 分类,添加到对应数组中
            ...
        }
        // 首先,注册实现了 PriorityOrdered 接口的 bean 后处理器
        sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
        registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors);
        // 下一步,注册实现了 Ordered 接口的 bean 后处理器
        List<BeanPostProcessor> orderedPostProcessors = new ArrayList<>(orderedPostProcessorNames.size());
        for (String ppName : orderedPostProcessorNames) {
            BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
            orderedPostProcessors.add(pp);
            if (pp instanceof MergedBeanDefinitionPostProcessor) {
                internalPostProcessors.add(pp);
            }
        }
        sortPostProcessors(orderedPostProcessors, beanFactory);
        registerBeanPostProcessors(beanFactory, orderedPostProcessors);
        // 现在,注册常规 bean 后处理器,其实就是不带顺序
        List<BeanPostProcessor> nonOrderedPostProcessors = new ArrayList<>(nonOrderedPostProcessorNames.size());
        for (String ppName : nonOrderedPostProcessorNames) {
            BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
            nonOrderedPostProcessors.add(pp);
            if (pp instanceof MergedBeanDefinitionPostProcessor) {
                internalPostProcessors.add(pp);
            }
        }
        registerBeanPostProcessors(beanFactory, nonOrderedPostProcessors);
        // 最后,重新注册 MergedBeanDefinitionPostProcessor 类型的后处理器
        // 看起来是重复注册了,但是每次注册调用的底层方法都会先移除已存在的 beanPostProcessor,然后再加进去,最后还是保存唯一
        sortPostProcessors(internalPostProcessors, beanFactory);
        registerBeanPostProcessors(beanFactory, internalPostProcessors);
        // 添加 ApplicationContext 探测器
        beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext));
    }
复制代码

跟之前的 BeanFactoryPostProcessor 处理是不是很相似,也是进行分类,将带有权重顺序、顺序和普通 BeanPostProcessor 添加到对应的列表后,然后排序,统一注册到 beanPostProcessors 列表末尾。

BeanPostProcessor 与之前的 BeanFactoryPostProcessor 进行对比后发现,少了硬编码注册的代码,只处理了配置文件方式的注册 bean。通过书中阐释,对少了硬编码的处理有些理解:

对于 BeanFactoryPostProcessor 的处理,在一个方法内实现了注册和实现,所以需要载入配置中的定义,并进行激活;而对于 BeanPostProcessor 并不需要马上调用,硬编码的方式实现的功能是将后处理器提取并调用,对于 BeanPostProcessor,注册阶段不需要调用,所以没有考虑处理硬编码,在这里只需要将配置文件的 BeanPostProcessor 提取出来并注册进入 beanFactory 就可以了。

而且我在测试过程,想在应用代码中进行硬编码注册,发现由于 ClassPathXmlApplicationContext 最后一个方法是实例化非延迟加载的 bean,在上下文创建好时,BeanPostProcessor 就已经执行完成了,于是硬编码注册的后处理器无法执行,只能通过设定延迟加载或者在配置文件配置中进行注册,或者其它 BeanFactory 能支持硬编码。

剩下顺序 Order 类型的后处理器注册 BeanFactoryPostProcessor 类似就不重复多讲解了,这段代码的逻辑挺清晰的~


小结

结束两个扩展功能,BeanFactoryPostProcessorBeanPostProcessor 的学习使用后,还有其它的扩展功能没学习到,在一开始基础机构篇就提到剩下的方法:

这这些扩展功能中,个人感觉事件传播器、监听器和发送广播事件这三个会用得比较多,所以下面的内容会花比较大篇幅讲这三个扩展。


初始化消息资源

根据书中的内容介绍,这个消息资源 messageSource 是跟 Spring 国际化相关。

例如中美之间的中英文差别,在不同地区显示不同的资源。对于有国际化需求的系统,要为每种提供一套相应的资源文件,并以规范化命名的形式保存在特定的目录中,由系统自动根据客户端的语言或者配置选择合适的资源文件。

举个🌰: 定义了两个资源文件,简单配置如下

  • 中文地区: test=测试
  • 英文地区: test=test

所以可以通过 Applicationcontext.getMessage() 方法访问国际化信息,在不同的环境中获取对应的数据。

由于个人感觉这种配置相关的,可以通过 profile 切换来实现,所以没有去细看和使用,具体实现和使用请感兴趣的同学们深入了解吧。


事件监听

事件传播器的使用很像我们设计模式中的观察者模式,被观察者变动后通知观察者进行相应的逻辑处理。

在了解 Spring 如何初始化事件传播器之前,来看下 Spring 监听的简单用法。

定义监听事件 Event

新建一个类,继承于 ApplicationEvent,并且需要在构造方法中调用父类的构造函数 supre(source)

public class CarEvent extends ApplicationEvent {

    /**
     * 自定义一个消息
     */
    private String msg;

    public CarEvent(Object source) {
        super(source);
    }

    public CarEvent(Object source, String msg) {
        super(source);
        this.msg = msg;
    }
}
复制代码

定义监听器 Listener

新建一个类,引用 ApplicationListener 接口,然后重载 onApplicationEvent 方法:

public class CarEventListener implements ApplicationListener {
    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof CarEvent) {
            CarEvent carEvent = (CarEvent) event;
            System.out.println("source : " + event.getSource() + ",  custom message : " + carEvent.getMsg());
        }
    }
}
复制代码

由于 Spring 的消息监听器不像 kafka 等主流 MQ 可以指定发送队列或者监听主题,只要发送消息后,所有注册的监听器都会收到消息进行处理,所以这边加了一个判断,如果是我业务上需要的消息,才会进行处理。


配置文件

<bean id="testListener" class="context.event.CarEventListener"/>
复制代码

将刚才写的监听器注册到 Spring 容器中


测试代码

public class EventBootstrap {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("factory.bean/bean-post-processor.xml");
        // 第一个参数是来源,第二个参数是自定义
        CarEvent carEvent = new CarEvent("hello",  "world");
        context.publishEvent(carEvent);
        // 消息发送之后,打印以下内容
        // source : hello,  custom message : world
    }
}
复制代码

由于在配置文件中注册了监听器,然后在启动代码汇总初始化了监听事件,最终通过 context 发送消息,发现输出结果与预想的一致。

这种观察者模式实现很经典,使用起来也很简单,下面来结合源码分析一下 Spring 是如何实现消息监听的功能。


消息监听代码分析

从源码中分析,发现主要是下面三个步骤:

初始化 ApplicationEvenMulticaster

protected void initApplicationEventMulticaster() {
    ConfigurableListableBeanFactory beanFactory = getBeanFactory();
    // 如有有自己注册class Name 是 applicationEventMulticaster,使用自定义广播器
    if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
        this.applicationEventMulticaster =
                beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
        }
    }
    else {
        // 没有自定义,使用默认的事件广播器
        this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
        beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
    }
}
复制代码

广播器的作用是用来广播消息,在默认的广播器 SimpleApplicationEventMulticaster 类中发现了这个方法 multicastEvent

public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
    ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
    Executor executor = getTaskExecutor();
    // 遍历注册的消息监听器
    for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
        if (executor != null) {
            executor.execute(() -> invokeListener(listener, event));
        }
        else {
            invokeListener(listener, event);
        }
    }
}
复制代码

可以看到,在广播事件时,会遍历所有注册的监听器进行调用 invokeListener 方法,底层调用的是监听器重载的 listener.onApplicationEvent(event),所以再次强调一次,如果使用 Spring 自带的事件监听,请在业务处理方判断事件来源,避免处理错误。


注册监听器

在上一步中,已经初始化好了广播器,所以下一步来看下,监听器的注册流程,入口方法如下:

org.springframework.context.support.AbstractApplicationContext#registerListeners

protected void registerListeners() {
    // 这里是硬编码注册的监听器
    for (ApplicationListener<?> listener : getApplicationListeners()) {
        getApplicationEventMulticaster().addApplicationListener(listener);
    }
    // 不要在这里初始化 factoryBean : 我们需要保留所有常规 bean 未初始化,以便让后处理程序应用于它们!
    // 这一步是配置文件中注册的监听器
    String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
    for (String listenerBeanName : listenerBeanNames) {
        getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
    }

    // 发布早期的应用程序事件,现在我们终于有了一个多播器=-=
    Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
    this.earlyApplicationEvents = null;
    if (earlyEventsToProcess != null) {
        for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
            getApplicationEventMulticaster().multicastEvent(earlyEvent);
        }
    }
}
复制代码

这一个方法代码不多,也没啥嵌套功能,按照注释顺序将流程梳理了一遍,将我们注册的监听器加入到 applicationEventMulticaster 列表中,等待之后调用。


publishEvent

广播器和监听器都准备好了,剩下的就是发送事件,通知监听器做相应的处理:

org.springframework.context.support.AbstractApplicationContext#publishEvent(java.lang.Object, org.springframework.core.ResolvableType)

核心是这行代码:

getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);

通过获取事件广播器,调用 multicastEvent 方法,进行广播事件,这一步前面也介绍过了,不再细说。


总结

这次学习,省略了书中的一些内容,有关属性编辑器、SPEL 语言和初始化非延迟加载等内容,请感兴趣的同学继续深入了解~

我们也能从 Spring 提供的这些扩展功能中学习到,通过预留后处理器,可以在 bean 实例化之前修改配置信息,或者做其他的自定义操作,例如替换占位符、过滤敏感信息等;

也可以通过广播事件,定义事件和监听器,在监听器中实现业务逻辑,由于不是直接调用监听器,而是通过事件广播器进行中转,达到了代码解耦的效果。

所以在之后的代码设计和编写中,在整体设计上,有必要的话,考虑在更高的抽象层要预留扩展功能,然后让子类重载或者实现,实现扩展的功能。


由于个人技术有限,如果有理解不到位或者错误的地方,请留下评论,我会根据朋友们的建议进行修正

代码和注释都在里面,小伙伴们可以下载我上传的代码,亲测可运行~

Gitee 地址:https://gitee.com/vip-augus/spring-analysis-note.git

Github 地址:https://github.com/Vip-Augus/spring-analysis-note

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

推荐阅读更多精彩内容