Spring Event事件通知机制 源码学习

笔记简述
本学习笔记主要是介绍Spring中的事件通知是如何实现的,同步和异步事件通知的用法和实现细节以及Spring提供的常见的Event,如果实际开发中需要根据事件推送完成相应的功能,该如何选择Event
Spring更多可查看Spring 源码学习

目录

Spring Event事件通知机制 源码学习
1、监听者模式
2、DEMO(同步)
3、Spring实现细节
4、Spring Event
4.1 ContextRefreshedEvent
4.2 ServletRequestHandledEvent
5、异步Pushlish以及DEMO

1、监听者模式

学习spring的事件通知机制肯定要先了解监听者模式(监听者模式和观察者模式有什么区别?

监听者模式包含了一个监听者Listener与之对应的事件Event,还有一个事件发布者EventPublish,过程就是EventPublish发布一个事件,被监听者捕获到,然后执行事件相应的方法

观察者模式是一对多的模式,一个被观察者Observable和多个观察者Observer,被观察者中存储了所有的观察者对象,当被观察者接收到一个外界的消息,就会遍历广播推算消息给所有的观察者
例如日常生活中的订阅报纸,报纸老板A,现在小明和老板打招呼说我要订报纸(这个过程就相当于观察者的注册),老板A就会拿出自己的小本本记下小明,下次小王、小芳也是类似的操作,现在老板A就有了三个观察者了,然后老板会自动的把报纸送到三位的家里,突然有一天小明说不想订报纸了,老板就在自己的小本本上划掉小明的名字(观察者的取消注册),等到送报纸的时候就不再送到小明家里。

2、DEMO(同步)

事件定义

public class EventDemo extends ApplicationEvent {
    private String message;
    public EventDemo(Object source, String message) {
        super(source);
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

事件监听者

@Component
public class EventDemoListern implements ApplicationListener<EventDemo> {

    @Override
    public void onApplicationEvent(EventDemo event) {
        System.out.println("receiver " + event.getMessage());
    }
}

事件发布

@Component
public class EventDemoPublish {

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    public void publish(String message){
        EventDemo demo = new EventDemo(this, message);
        applicationEventPublisher.publishEvent(demo);
    }

}

最后获取到EventDemoPublish这个bean,直接publish就可以完成操作了。

3、Spring实现细节

在spring中由于类的细节太多,参数方法也非常的多,不太建议通篇的一个一个看,优先关注我们关注的点,然后打断点的方式具体了解需要的参数和必备的方法等

AbstractApplicationContext类就有publishEvent()方法,先分析下。

protected void publishEvent(Object event, ResolvableType eventType) {
    Assert.notNull(event, "Event must not be null");
    if (logger.isTraceEnabled()) {
        logger.trace("Publishing event in " + getDisplayName() + ": " + event);
    }

    ApplicationEvent applicationEvent;
    if (event instanceof ApplicationEvent) {
           // 如果是ApplicationEvent对象
        applicationEvent = (ApplicationEvent) event;
    }
    else {
        applicationEvent = new PayloadApplicationEvent<Object>(this, event);
        if (eventType == null) {
            eventType = ((PayloadApplicationEvent)applicationEvent).getResolvableType();
        }
        // 否则就包装为PayloadApplicationEvent时间,并获取对应的事件类型
    }

    if (this.earlyApplicationEvents != null) {
           //初始化时候的事件容器,默认为null的
        this.earlyApplicationEvents.add(applicationEvent);
    }
    else {
        getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
        //  监听者容器?大概的就这个意思,推送消息给监听器
    }

    // 如果当前命名空间还有父亲节点,也需要给父亲推送该消息
    if (this.parent != null) {
        if (this.parent instanceof AbstractApplicationContext) {
            ((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
        }
        else {
            this.parent.publishEvent(event);
        }
    }
}

ApplicationEventMulticaster getApplicationEventMulticaster() throws IllegalStateException {
    if (this.applicationEventMulticaster == null) {
           // 意味着肯定需要提前初始化这个监听器容器
        throw new IllegalStateException("ApplicationEventMulticaster not initialized - " +
                "call 'refresh' before multicasting events via the context: " + this);
    }
    return this.applicationEventMulticaster;
}

通过分析,又回到了refresh这个核心函数中


image.png
public static final String APPLICATION_EVENT_MULTICASTER_BEAN_NAME = "applicationEventMulticaster";

// 初始化监听器容器
protected void initApplicationEventMulticaster() {
    ConfigurableListableBeanFactory beanFactory = getBeanFactory();
    if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
           // 手动实现了名称为applicationEventMulticaster的bean
        this.applicationEventMulticaster =
                beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
        // 还必须得是ApplicationEventMulticaster类
        if (logger.isDebugEnabled()) {
            logger.debug("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]");
        }
    }
    else {
           // 否则就默认自定义一个名称为SimpleApplicationEventMulticaster的监听器容器
        this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
        beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
        if (logger.isDebugEnabled()) {
            logger.debug("Unable to locate ApplicationEventMulticaster with name '" +
                    APPLICATION_EVENT_MULTICASTER_BEAN_NAME +
                    "': using default [" + this.applicationEventMulticaster + "]");
        }
    }
}

private final Set<ApplicationListener<?>> applicationListeners = new LinkedHashSet<ApplicationListener<?>>();
// 默认是一个空列表,而不是null

// 注册监听器,并且加入到监听器容器中
protected void registerListeners() {
    // Register statically specified listeners first.
    for (ApplicationListener<?> listener : getApplicationListeners()) {
        getApplicationEventMulticaster().addApplicationListener(listener);
        // 把提前存储好的监听器添加到监听器容器中
    }

    String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
    // 获取类型是ApplicationListener的beanName集合,此处不会去实例化bean
    for (String listenerBeanName : listenerBeanNames) {
        getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
    }
    
    Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
    this.earlyApplicationEvents = null;
    // 初始化事件为null
    if (earlyEventsToProcess != null) {
        for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
            getApplicationEventMulticaster().multicastEvent(earlyEvent);
        }
    }
}

这样就很清楚了,在spring进行refresh的时候就完成了监听器容器和监听器的初始化工作(可以很方便的注册自己需要的监听器或者自定义的监听器容器对象),只需要获取到容器就可以直接publish事件了。

4、Spring Event

前面已经说了监听器一般和具体的某一个事件绑定的(这点就和观察者模式非常不一样了),那么就来看看spring已经帮我们实现了哪些Event,具体如下图

image.png
  • ApplicationContextEvent(Context...的抽象类)
  • ContextClosedEvent 生命周期关闭
  • ContextRefreshedEvent refresh完成
  • ContextStartedEvent 生命周期启动
  • ContextStoppedEvent 生命周期停止
  • PayloadApplicationEvent
  • RequestHandledEvent
  • ServletRequestHandledEvent RequestHandledEvent的子类,Spring MVC 请求完成之后推送的事件

下面就ContextRefreshedEvent和ServletRequestHandledEvent具体分析下如何使用以及其实现的原理

4.1 ContextRefreshedEvent

image.png

在refresh结束之后推送的一个事件,这个时候大部分bean的实例化已经完成了,并且传递了this这个数据,那么如果有需要的话,可以在这个地方实现对Spring上下文数据的统计或者监控,简直不能爽歪歪,但是个人开发目前还没遇到具体的使用场景

4.2 ServletRequestHandledEvent

Spring MVC 中的事件,直接跳到DispatcherServlet类,后来到了FrameworkServlet 抽象类

image.png

如果可以推送事件的话,则利用web的上下文推送事件,其中包含了一个请求的基本信息,如果需要定制化统计整个HTTP请求的情况,完全可以通过这个事件推送实现

不过这里有一点需要注意到,还有个publishEvents字段,如果需要使用,需要设置为true,如下图web.xml配置即可

image.png

5、异步Pushlish以及DEMO

异步推送采用的是多线程的方法,具体看SimpleApplicationEventMulticaster类,也就是上面说的监听器容器

public void multicastEvent(final ApplicationEvent event, ResolvableType eventType) {
    ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
    for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {
           // 获取符合事件类型的监听器集合
        Executor executor = getTaskExecutor();
        if (executor != null) {
              // 如果线程池不为null,则提交一个新任务交由线程池运行
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    invokeListener(listener, event);
                }
            });
        }
        else {
              // 否则就是同步执行
            invokeListener(listener, event);
        }
    }
}

protected void invokeListener(ApplicationListener listener, ApplicationEvent event) {
    ErrorHandler errorHandler = getErrorHandler();
    if (errorHandler != null) {
          // 错误处理器不为null
        try {
            listener.onApplicationEvent(event);
        }
        catch (Throwable err) {
            errorHandler.handleError(err);
            // 当推送消息出现异常,利用错误处理器去处理该错误
        }
    }
    else {
        try {
            listener.onApplicationEvent(event);
        }
        catch (ClassCastException ex) {
            String msg = ex.getMessage();
            if (msg == null || msg.startsWith(event.getClass().getName())) {
                // Possibly a lambda-defined listener which we could not resolve the generic event type for
                Log logger = LogFactory.getLog(getClass());
                if (logger.isDebugEnabled()) {
                    logger.debug("Non-matching event type for listener: " + listener, ex);
                }
            }
            else {
                throw ex;
            }
        }
    }
}
  • 注入一个线程池可实现异步处理
  • 使用errorHandler可以做到最后的事件处理的兜底方案

那么问题来了,如何注入一个线程池呢?往哪里注入呢?

只需要手动实现applicationEventMulticaster的bean,并且利用Set注入的方法注入了一个线程池,线程池也需要实例化,就直接使用了spring自带的简单异步任务线程池

image.png

从同步改成异步,代码不需要修改,直接注入这么一个bean即可(当然了XML配置和Config配置均可)

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

推荐阅读更多精彩内容