【啃啊啃 Spring5 源码】细碎一:spring 事件机制

阅读spring源码时,看到ApplicationEvent相关的代码觉得熟悉又困惑,深入了解了一下,发现原来是spring事件机制(原谅我之前没用过……)。
这里在【Spring4揭秘 基础1】监听器和事件的基础下进行一下扩展深入,感谢这篇博文的作者,他的spring基础系列文章让我在阅读源码时,轻松了不少。

注:源码部分根据spring-5.0.7版本分析

设计模式

spring事件机制其实就是观察者模式的一种体现。忘记或不熟悉观察者模式的朋友可以看我前面的总结:Head First 设计模式(二)观察者模式

观察者模式简单可分为两部分:主题观察者。当一个主题改变状态时,它的所有依赖者都会收到通知并进行自动更新。

Spring事件机制简单可分为三部分:事件广播观察者。 “主题改变状态” 这个动作被抽离成了 一个“事件”,由一个持有所有观察者的“广播容器” 进行广播,“观察者”们 接收到相应事件后进行自动更新。

这种设计其实继承自Java本身的事件机制:

  1. java.util.EventObject
    事件状态对象的基类,它封装了事件源对象以及和事件相关的信息。所有java的事件类都需要继承该类。
  2. java.util.EventListener
    观察者基类,当事件源的属性或状态改变的时候,调用相应观察者内的回调方法。
  3. Source
    主题类,java中未定义,持有所有的观察者,当主题状态发生改变,产生事件,负责向所有观察者发布事件

Java的事件机制这里不敞开讲,想了解可看:java 事件机制

Spring中的事件机制

Spring的事件机制相关的核心类有四个:

  • ApplicationEvent: Spring中的事件基类,继承自java.util.EventObject,创建是需要指定事件源
public abstract class ApplicationEvent extends EventObject {
    /**
     * 创建一个事件,需要指定事件源
     */
    public ApplicationEvent(Object source) {
        super(source);
        this.timestamp = System.currentTimeMillis();
    }
}
  • ApplicationEventPublisher:发布事件者,调用广播发布事件
public interface ApplicationEventPublisher {
    /**发布事件*/
    default void publishEvent(ApplicationEvent event) {
        publishEvent((Object) event);
    }
    void publishEvent(Object event);
}
  • ApplicationEventMulticaster:广播,持有观察者集合,可向集合内的所有观察者通知事件
public interface ApplicationEventMulticaster {
    /**
     * 添加监听者(观察者)
     */
    void addApplicationListener(ApplicationListener<?> listener);

    /**
     * 删除监听者(观察者)
     */
    void removeApplicationListener(ApplicationListener<?> listener);

    /**
     * 向所有监听者发布事件
     */
    void multicastEvent(ApplicationEvent event);
}
  • ApplicationListener:观察者,接收对应事件后,执行逻辑
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
    /**
     * 接收事件后,执行相应逻辑
     */
    void onApplicationEvent(E event);
}

事件发布者ApplicationEventPublisher持有广播,而广播ApplicationEventMulticaster持有若干观察者ApplicationListener。一个事件ApplicationEvent可以通过发布者ApplicationEventPublisher发布后,会调用广播ApplicationEventMulticaster通知所有观察者,观察者ApplicationListener收到通知后执行相关操作。

下面举例说明:
当一个用户注册结束后,我们想要将这个事件发生给短信监听者和邮件监听者,让他们向用户发送短信和邮件。

public class EventDemo {

    public static void main(String[] args) {
        //构建广播器
        ApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();
        //广播添加监听器
        multicaster.addApplicationListener(new RegisterListener1());
        multicaster.addApplicationListener(new RegisterListener2());

        //构建事件发布者
        MyEventPublicsher eventPublicsher = new MyEventPublicsher();
        //事件发布者增加广播
        eventPublicsher.setEventMulticaster(multicaster);

        //构建注册事件
        User user = new User("jack", "18782252509", "jack_email@163.com");
        System.out.println("用户注册……");
        RegisterEvent registerEvent = new RegisterEvent(user);

        //发布注册事件
        eventPublicsher.publishEvent(registerEvent);
    }

    /**
     * 用户实体类
     */
    public static class User{
        private String id;
        private String name;
        private String phone;
        private String email;

        public User(String name, String phone, String email) {
            this.name = name;
            this.phone = phone;
            this.email = email;
        }

        //.....GET AND SET

    }
    /**
     * 自定义注册事件
     */
    public static class RegisterEvent extends ApplicationEvent {
        //事件的构造方法中,必须制定事件源
        public RegisterEvent(User user) {
            super(user);
        }

        public User getUser(){
            return (User) getSource();
        }
    }

    /**
     * 注册事件监听者1-短信监听者(即观察者),负责注册后发生短信
     * 注意:实现接口时,在泛形中指定事件类型,则只监听该类型事件。若不指定,则默认监听所有事件。
     */
    public static class RegisterListener1 implements ApplicationListener<RegisterEvent> {
        public void onApplicationEvent(RegisterEvent event) {
            User user = event.getUser();
            System.out.println("用户:"+ user.getName()+"注册结束,向手机"+user.getPhone()+"发送短信!");
        }
    }

    /**
     * 注册事件监听者2-邮件监听者(即观察者),负责注册后发送邮件
     * 注意:实现接口时,在泛形中指定事件类型,则只监听该类型事件。若不指定,则默认监听所有事件。
     */
    public static class RegisterListener2 implements ApplicationListener<RegisterEvent> {

        public void onApplicationEvent(RegisterEvent event) {
            User user = event.getUser();
            System.out.println("用户:"+ user.getName()+"注册结束,发生邮件:"+user.getEmail());
        }
    }


    /**
     * 事件发布者,持有监听者
     */
    public static class MyEventPublicsher implements  ApplicationEventPublisher{
        //广播
        private ApplicationEventMulticaster eventMulticaster;

        public void setEventMulticaster(ApplicationEventMulticaster eventMulticaster) {
            this.eventMulticaster = eventMulticaster;
        }

        //发布事件
        public void publishEvent(Object event) {
            eventMulticaster.multicastEvent((ApplicationEvent) event);
        }
    }

}

输出:

用户注册后
用户:jack注册结束,向手机18782252509发送短信!
用户:jack注册结束,发生邮件:jack_email@163.com

源码细节解析

我们主要分析下广播的细节,以SimpleApplicationEventMulticaster为例:

public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster {
        @Override
        public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
            //事件被统一封装成了ResolvableType,方便形参入口统一
            ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
            //根据事件类型,通过泛形反射获取对应的监听者
            for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {
                //获取广播配置的线程池
                Executor executor = getTaskExecutor();
                //如果有配置线程池,则异步通知事件监听者
                if (executor != null) {
                    executor.execute(() -> invokeListener(listener, event));
                }
                //没有配置线程池,同步通知事件监听者
                else {
                    invokeListener(listener, event);
                }
            }
        }
        
        private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
                try {
                    //执行监听者对应逻辑
                    listener.onApplicationEvent(event);
                }
                catch (ClassCastException ex) {
                    ……
                }
            }
}

我们可以看到spring广播时,会先去判断有没有配置线程池,如果配置则使用线程池异步执行监听者逻辑,否则同步。

需要注意的是,我们使用spring事件机制时,默认是没有配置线程池的,也就是默认所有的通知都是同步的,需要手动指定线程池才会开启同步。

应用

设计一个业务场景:当一个用户完成贷款订单后,我们希望执行发送提醒短信、调用积分服务增加积分、通知风控服务重算风控值(后续操作可能增加)等功能。这种业务需求开始很可能写成同步代码。

//创建订单
public void createOrder(Order order){
    创建贷款订单;
    发送提醒短信;
    调用积分服务增加积分;
    调用风控服务推送订单信息;
    ……
    返回;
}

随着业务复杂度的增加,我们很快发现createOrder()这个方法耦合了太多与创建订单无关的逻辑,即影响了原本创建订单方法的效率,在设计上又不符合“开闭原则”。

现在使用spring事件机制我们来解耦,将与注册无关的操作改为异步。这里直接使用注解式写法。

  • 首先我们修改spring中的广播,为它注入我们自定义的线程池,在spring配置加上:
    <!--自定义线程池-->
    <bean id="myExecutor" class="org.springframework.core.task.SimpleAsyncTaskExecutor" />

    <!--修改容器中的广播,注入自定义线程池-->
    <bean id="applicationEventMulticaster" class="org.springframework.context.event.SimpleApplicationEventMulticaster">
        <property name="taskExecutor" ref="myExecutor" />
    </bean>
  • 定义一个创建订单事件
/**
 * 创建订单完成事件
 */
@Component
public class AfterCreateOrderEvent extends ApplicationEvent {

    public AfterCreateOrderEvent(Order order) {
        super(order);
    }

    public Order getOrder(){
        return (Order) getSource();
    }
}
  • 使用事件机制改变原有代码
@Service
public class OrderService {
    //直接注入spring事件发布者
    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;
    
    /**
     * 简单的创建订单方法
     */
    public void createOrder(Order order) throws InterruptedException {
        System.out.println("创建订单 order:"+order.getOrderNo()+" 结束");
        //调用事件发布者发布事件
        applicationEventPublisher.publishEvent(new AfterCreateOrderEvent(order));
        System.out.println("createOrder方法 结束");
    }

     //加入@EventListener注解后,该方法可以看出一个事件监听者
    @EventListener
    public void afterCreateOrder(AfterCreateOrderEvent afterCreateOrderEvent) throws InterruptedException {
        Order order = afterCreateOrderEvent.getOrder();
        Thread.sleep(2000);
        System.out.println("调用短信通知服务:" + order.getPhone());
        System.out.println("调用积分服务增加贷款积分:"+order.getOrderNo());
    }

    public static void main(String[] args) throws InterruptedException {
        Order order = new Order("N123124124124", "18782202534");
        //这里指定自己的spring配置路径
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("config/spring-config.xml");

        OrderService orderService = context.getBean(OrderService.class);
        orderService.createOrder(order);
    }
}

输出:

创建订单 order:N123124124124 结束
createOrder方法 结束
调用短信通知服务:18782202534
调用积分服务增加贷款积分:N123124124124

自此,创建订单与其他操作便实现了异步和解耦。

另一种异步实现方式

另外,也可使用@Async注解来实现事件的异步调用

    @EventListener
    @Async
    public void afterCreateOrder(AfterCreateOrderEvent afterCreateOrderEvent) throws InterruptedException {
        Order order = afterCreateOrderEvent.getOrder();
        Thread.sleep(2000);
        System.out.println("调用短信通知服务:" + order.getPhone());
        System.out.println("调用积分服务增加贷款积分:"+order.getOrderNo());
    }

spring配置加上:

    <!--开启异步调用,并指定线程池--> 
    <task:annotation-driven executor="annotationExecutor" />
    <!--线程池-->
    <task:executor id="annotationExecutor" pool-size="20"/>

但这种方法有弊端,afterCreateOrder()方法不能放在同一类(OrderService)里面。原因是spring的代理机制。

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,599评论 18 139
  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于...
    Hsinwong阅读 22,313评论 1 92
  • 前言 在微服务架构的系统中,我们通常会使用轻量级的消息代理来构建一个共用的消息主题让系统中所有微服务实例都连接上来...
    Chandler_珏瑜阅读 6,563评论 2 39
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,748评论 6 342
  • (上一章) 丨全文总目录丨 神秘消失的情人。 纳西族世代流传的诡异传说。 可怖的黑衣引渡者。 徘徊在丽江的死亡诅咒...
    小巫先生阅读 403评论 1 6