Spring IOC进阶-事件机制&监听器

从 0 开始深入学习 Spring 专栏目录总览

接下来的几章,介绍的都是 SpringFramework 中的一些进阶的功能 / 特性,这些特性在平时开发不一定全部用得上,但想熟练掌握 SpringFramework 的开发,这些知识点还是非常有必要的。

本章源码均在 com.linkedbear.spring.event

本章咱来学习事件驱动和监听器。说到事件和监听器,小伙伴们最先想到的估计就是观察者模式了吧。下面咱先快速回顾一下观察者模式。

1. 观察者模式【回顾】

观察者模式,也被称为发布订阅模式,也有的人叫它**“监听器模式”,它是 GoF23 设计模式中行为型模式的其中之一。观察者模式关注的点是某一个对象被修改 / 做出某些反应 / 发布一个信息等,会自动通知依赖它的对象(订阅者)**。

观察者模式的三大核心是:观察者、被观察主题、订阅者。观察者( Observer )需要绑定要通知的订阅者( Subscriber ),并且要观察指定的主题( Subject )。

2. SpringFramework中设计的观察者模式【掌握】

SpringFramework 中,体现观察者模式的特性就是事件驱动和监听器。监听器充当订阅者,监听特定的事件;事件源充当被观察的主题,用来发布事件;IOC 容器本身也是事件广播器,可以理解成观察者

不过我个人比较喜欢把 SpringFramework 的事件驱动核心概念划分为 4 个:事件源、事件、广播器、监听器

  • 事件源:发布事件的对象
  • 事件:事件源发布的信息 / 作出的动作
  • 广播器:事件真正广播给监听器的对象【即 ApplicationContext 】
    • ApplicationContext 接口有实现 ApplicationEventPublisher 接口,具备事件广播器的发布事件的能力
    • ApplicationEventMulticaster 组合了所有的监听器,具备事件广播器的广播事件的能力
  • 监听器:监听事件的对象

也许这样理解起来会更容易一些:

Spring IOC进阶-事件机制&监听器

3. 快速体会事件与监听器【掌握】

下面咱先通过一个最简单的实例来体会 SpringFramework 中监听器的使用。

3.1 编写监听器

SpringFramework 中内置的监听器接口是 ApplicationListener ,它还带了一个泛型,代表要监听的具体事件:

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
	void onApplicationEvent(E event);
}

我们要自定义监听器,只需要实现这个 ApplicationListener 接口即可。

为快速体会事件和监听器的功能,下面咱先介绍两个事件:ContextRefreshedEvent 和 ContextClosedEvent ,它们分别代表容器刷新完毕即将关闭。下面咱编写一个监听器,来监听 ContextRefreshedEvent 事件。

@Component
public class ContextRefreshedApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
    
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        System.out.println("ContextRefreshedApplicationListener监听到ContextRefreshedEvent事件!");
    }
}

记得用 @Component 注解标注监听器哦,一会要进行包扫描的。监听器必须要注册到 SpringFramework 的 IOC 容器才可以生效。

3.2 编写启动类

写好监听器,就可以编写启动类,驱动 IOC 容器来测试效果了。这个时候可能有小伙伴有点懵:我去,我思想工作还没建设好呢,这就完事了?然而真的就是这么回事,下面代码一写你就恍然大悟了。

public class QuickstartListenerApplication {
    
    public static void main(String[] args) throws Exception {
        System.out.println("准备初始化IOC容器。。。");
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
                "com.linkedbear.spring.event.a_quickstart");
        System.out.println("IOC容器初始化完成。。。");
        ctx.close();
        System.out.println("IOC容器关闭。。。");
    }
}

哈哈,不会真的有小伙伴忘了 AnnotationConfigApplicationContext 可以直接传包扫描路径吧,不会吧不会吧!(狗头保命)

运行 main 方法,控制台会根据流程依次打印如下信息:

准备初始化IOC容器。。。
ContextRefreshedApplicationListener监听到ContextRefreshedEvent事件!
IOC容器初始化完成。。。
IOC容器关闭。。。

3.3 注解式监听器

除了实现 ApplicationListener 接口之外,还可以使用注解的形式注册监听器。

使用注解式监听器,组件不再需要实现任何接口,而是直接在需要作出事件反应的方法上标注 @EventListener 注解即可:

@Component
public class ContextClosedApplicationListener {
    
    @EventListener
    public void onContextClosedEvent(ContextClosedEvent event) {
        System.out.println("ContextClosedApplicationListener监听到ContextClosedEvent事件!");
    }
}

重新运行 QuickstartListenerApplication 的 main 方法,控制台可以打印出 ContextClosedApplicationListener 监听事件的反应:

准备初始化IOC容器。。。
ContextRefreshedApplicationListener监听到ContextRefreshedEvent事件!
IOC容器初始化完成。。。
ContextClosedApplicationListener监听到ContextClosedEvent事件!
IOC容器关闭。。。

由这两种监听器的 Demo ,可以得出几个结论:

  • ApplicationListener 会在容器初始化阶段就准备好,在容器销毁时一起销毁;
  • ApplicationListener 也是 IOC 容器中的普通 Bean ;
  • IOC 容器中有内置的一些事件供我们监听。

4. SpringFramework中的内置事件【熟悉】

在 SpringFramework 中,已经有事件的默认抽象,以及 4 个默认的内置事件了,下面咱了解一下它们。

4.1 ApplicationEvent

很明显,它是事件模型的抽象,它是一个抽象类,里面也没有定义什么东西,只有事件发生时的时间戳。值得关注的是,它是继承自 jdk 原生的观察者模式的事件模型,并且把它声明为抽象类:

public abstract class ApplicationEvent extends EventObject

关于这个设计,它的文档注释就已经说明了:

Class to be extended by all application events. Abstract as it doesn’t make sense for generic events to be published directly.

由所有应用程序事件扩展的类。它被设计为抽象的,因为直接发布一般事件没有意义

如果说只是有这么一个派生,那看上去没什么太大的意义,所以 SpringFramework 中又给这个 ApplicationEvent 进行了一次扩展。

4.2 ApplicationContextEvent

先看一眼源码,从这里面或许就能意识到什么:

public abstract class ApplicationContextEvent extends ApplicationEvent {
    
	public ApplicationContextEvent(ApplicationContext source) {
		super(source);
	}
    
	public final ApplicationContext getApplicationContext() {
		return (ApplicationContext) getSource();
	}
}

它在构造时,会把 IOC 容器一起传进去,这意味着事件发生时,可以通过监听器直接取到 ApplicationContext 而不需要做额外的操作,这才是 SpringFramework 中事件模型扩展最值得的地方。下面列举的几个内置的事件,都是基于这个 ApplicationContextEvent 扩展的。

4.3 ContextRefreshedEvent&ContextClosedEvent

这两个是一对,分别对应着 IOC 容器刷新完毕但尚未启动,以及 IOC 容器已经关闭但尚未销毁所有 Bean 。这个时机可能记起来有点小困难,小伙伴们可以不用记很多,只通过字面意思能知道就 OK ,至于这些事件触发的真正时机,在我的 SpringBoot 源码小册第 16 章中有提到,感兴趣的小伙伴可以去看一看。在后面的 IOC 原理篇中,这部分也会略有涉及。

4.4 ContextStartedEvent&ContextStoppedEvent

这一对跟上面的时机不太一样了。ContextRefreshedEvent 事件的触发是所有单实例 Bean 刚创建完成后,就发布的事件,此时那些实现了 Lifecycle 接口的 Bean 还没有被回调 start 方法。当这些 start 方法被调用后,ContextStartedEvent 才会被触发。同样的,ContextStoppedEvent 事件也是在 ContextClosedEvent 触发之后才会触发,此时单实例 Bean 还没有被销毁,要先把它们都停掉才可以释放资源,销毁 Bean 。

5. 自定义事件开发【熟悉】

上面咱了解了 SpringFramework 中内置的事件,如果我们想自己在合适的时机发布一些事件,让指定的监听器来以此作出反应,执行特定的逻辑,那就需要自定义事件了。下面咱模拟一个场景,来体会自定义事件的开发过程。

本节可能会使人产生不适感,请做好心理准备再继续往下阅读 -.-

5.1 场景概述

论坛应用,当新用户注册成功后,会同时发送短信、邮件、站内信,通知用户注册成功,并且发放积分。

在这个场景中,用户注册成功后,广播一个“用户注册成功”的事件,将用户信息带入事件广播出去,发送短信、邮件、站内信的监听器监听到注册成功的事件后,会分别执行不同形式的通知动作。

5.2 自定义用户注册成功事件

SpringFramework 中的自定义事件的方式就是通过继承 ApplicationEvent :

/**
 * 注册成功的事件
 */
public class RegisterSuccessEvent extends ApplicationEvent {
    
    public RegisterSuccessEvent(Object source) {
        super(source);
    }
}

对,没了,这样写就 OK 。

5.3 编写监听器

使用上述的两种方式,分别编写发送短信、发送邮件,和发送站内信的监听器:

@Component
public class SmsSenderListener implements ApplicationListener<RegisterSuccessEvent> {
    
    @Override
    public void onApplicationEvent(RegisterSuccessEvent event) {
        System.out.println("监听到用户注册成功,发送短信。。。");
    }
}
@Component
public class EmailSenderListener {
    
    @EventListener
    public void onRegisterSuccess(RegisterSuccessEvent event) {
        System.out.println("监听到用户注册成功!发送邮件中。。。");
    }
}
@Component
public class MessageSenderListener {
    
    @EventListener
    public void onRegisterSuccess(RegisterSuccessEvent event) {
        System.out.println("监听到用户注册成功,发送站内信。。。");
    }
}

5.4 编写注册逻辑业务层

只有事件和监听器还不够,还需要有一个事件源来持有事件发布器,在应用上下文中发布事件。

Service 层中,需要注入 ApplicationEventPublisher 来发布事件,此处选择使用回调注入的方式。

@Service
public class RegisterService implements ApplicationEventPublisherAware {
    
    ApplicationEventPublisher publisher;
    
    public void register(String username) {
        // 用户注册的动作。。。
        System.out.println(username + "注册成功。。。");
        // 发布事件
        publisher.publishEvent(new RegisterSuccessEvent(username));
    }
    
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }
}

5.5 编写测试启动类

测试代码都准备好了,下面咱就来写一把启动类,模拟一次用户注册。

public class RegisterEventApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
                "com.linkedbear.spring.event.b_registerevent");
        RegisterService registerService = ctx.getBean(RegisterService.class);
        registerService.register("张大三");
    }
}

运行 main 方法,控制台打印出注册动作,以及监听器的触发反应:

张大三注册成功。。。
监听到用户注册成功,发送邮件中。。。
监听到用户注册成功,发送站内信。。。
监听到用户注册成功,发送短信。。。

由此又得出来另外一个结论:注解式监听器的触发时机比接口式监听器早

5.6 调整监听器的触发顺序

如果业务需要调整,需要先发送站内信,后发送邮件,这个时候就需要配合另外一个注解了:@Order 。标注上这个注解后,默认的排序值为 Integer.MAX_VALUE ,代表最靠后

按照这个规则,那咱在 MessageSenderListener 的 onRegisterSuccess 方法上标注 @Order(0) ,重新运行启动类的 main 方法,观察控制台的打印:

张大三注册成功。。。
监听到用户注册成功,发送站内信。。。
监听到用户注册成功,发送邮件中。。。
监听到用户注册成功,发送短信。。。

需求得以解决。不过这个时候我们再思考一个问题:如果不标注 @Order 注解,默认的顺序是多少呢?

尝试着把刚才的 @Order 注解中,value 改为 Integer.MAX_VALUE - 1 ,重新运行,发现运行结果还是像上面那样打印,证明默认的排序值是 Integer.MAX_VALUE 。

5.7 理性看待自定义事件

到这里,估计部分小伙伴快看不下去了,这有点多此一举呀!我完全可以用一个监听器搞三个方法一块写就完了呀!甚至,完全可以把发送信息、邮件的动作,整合在注册的逻辑中。那这自定义事件到底有什么刚需吗?讲道理,真的非常少。很多场景下,使用自定义事件可以处理的逻辑,完全可以通过一些其它的方案来替代,这样真的会显得自定义事件很鸡肋。所以,一定要理性看待自定义事件的使用,千万不要一学到点东西,就疯狂输出哦 ( ̄▽ ̄)/。

免责声明:
1.本站所有内容由本站原创、网络转载、消息撰写、网友投稿等几部分组成。
2.本站原创文字内容若未经特别声明,则遵循协议CC3.0共享协议,转载请务必注明原文链接。
3.本站部分来源于网络转载的文章信息是出于传递更多信息之目的,不意味着赞同其观点。
4.本站所有源码与软件均为原作者提供,仅供学习和研究使用。
5.如您对本网站的相关版权有任何异议,或者认为侵犯了您的合法权益,请及时通知我们处理。
火焰兔 » Spring IOC进阶-事件机制&监听器