spring-事件机制-监听器-观察者模式

前言

事件机制(Event)是spring的重要功能之一。本文将从该功能的用法/和监听器的关系/应用的设计模式/源码讲解等几个方面,由浅入深做全方位讲解。
虽然是Spring的重要功能之一,但在大部分人的日常开发中,却很少直接应用该功能。
一句话概括用法场景:一个地方发出个通知,很多其他地方能收到通知并作出相应的动作

示例代码路径

后面的Demo以一个简单的Springboot项目为基础(文中也会贴出重要代码)。项目路径

重要概念

在事件机制中有以下几个重要概念:

  • 事件
  • 事件发布者(一个)
  • 事件监听者(多个)

简单应用

Demo

事件

package com.yc.blog.springboot.event.demo1;

import org.springframework.context.ApplicationEvent;

/**
 * 事件
 */
public class MyEvent1 extends ApplicationEvent {
	
	private static final long serialVersionUID = 1L;
	
	/**
	 * 自定义事件消息内容
	 */
	private String message;

	/**
	 * 为什么要这么写构造方法?
	 * 因为父类没有默认构造器,子类必须手动调用父类的那个构造器ApplicationEvent(Object source)
	 * @param source
	 * @param message
	 */
	public MyEvent1(Object source, String message) {
		super(source);
		this.message = message;
	}

	public String getMessage() {
		return message;
	}

	public void setMessage(String message) {
		this.message = message;
	}

}

事件发布者

	/**
	 * 发布者
	 * 可在spring的任意位置注入,用来发布事件
	 * 也可以使用上下文容器ConfigurableApplicationContext来发布事件(因为它也继承了ApplicationEventPublisher)
	 */
	@Autowired
	private ApplicationEventPublisher applicationEventPublisher;
	
	@Test
	public void publish1() {
		System.out.println("----发布1开始-------");
		MyEvent1 event = new MyEvent1(this, "新消息1");
		applicationEventPublisher.publishEvent(event);//发布者发布消息
		System.out.println("----发布1完成-------");
	}

事件监听者

package com.yc.blog.springboot.event.demo1;

import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

/**
 * 监听者
 * 实现ApplicationListener接口
 *
 */
@Component
public class Listener1 implements ApplicationListener<MyEvent1> {

	@Override
	public void onApplicationEvent(MyEvent1 event) {
		System.out.println("Listener1 收到事件通知:" + event.getMessage());
		//do something
	}

}

代码和概念一一对应,比较好理解,具体的细节先不管,跑起来看看效果。

...
----发布1开始-------
Listener1 收到事件通知:新消息1
Listener2 收到事件通知:新消息1
----发布1完成-------
...

简单分析总结

  1. 定义事件
    • 核心是继承 ApplicationEvent
    • ApplicationEvent作为父类,构造方法要求必须传入参数Object source,表示是谁发布的事件
    • ApplicationEvent作为父类,是可序列化的,有serialVersionUID,建议子类也加上
  2. 发布事件
    • 核心是注入 ApplicationEventPublisher,并使用其中的publishEvent方法 发布事件
    • 也可以使用spring上下文容器ConfigurableApplicationContext,因为它也继承了ApplicationEventPublisher
  3. 监听事件
    • 核心是实现 ApplicationListener 接口
    • 实现onApplicationEvent方法,方法内就是监听者对事件发生的响应

进阶

使用注解@EventListener

应用在事件机制的三个概念中的监听者身上,下面看用法

package com.yc.blog.springboot.event.demo1;

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

/**
 * 使用@EventListener实现监听者
 */
@Component
public class Listener3 {

	@EventListener
	public void listenerMyEvent1(MyEvent1 event) {
		System.out.println("Listener3 收到事件通知:" + event.getMessage());
		//do something
	}

}

执行结果

----发布1开始-------
Listener3 收到事件通知:新消息1
Listener1 收到事件通知:新消息1
Listener2 收到事件通知:新消息1
----发布1完成-------

该注解作用在方法上,跟前面普通监听者写法一样,方法参数列表里是要监听的事件类型。
在事件驱动编程中,监听者可能数量众多,有注解将会大大简化开发。 而且之前的写法一个类只能监听一个事件,有了注解,我们可以在一个类中写多个方法,监听多个事件,如下所示

/**
 * 学生监听多种事件,并作出响应
 */
@Component
public class Student {

	/**
	 * 监听上课铃声
	 * @param event1
	 */
	@EventListener
	public void listenSchoolBell(MyEvent1 event1) {
		System.out.println("上课铃响了,我要去上课...");
		//do something
	}
	
	/**
	 * 监听老师提问
	 * @param event2
	 */
	@EventListener
	public void listenTeacherAskMe(MyEvent1 event2) {
		System.out.println("老师提问了,我要起立回答问题...");
		//do something
	}
	
}

如上面定义的学生类,可以监听上课铃声,可以监听老师提问…,通过监听多种事件,让这个类更加符合java编程的“面向对象”思想。

异步监听

想一个问题:发布一个事件,可能会有一堆的监听者,假如其中一个监听者出现异常,方法内部迟迟无法执行完,会对整个事件的发布有影响吗?看示例及结果

/**
 * 异常监听者
 */
@Component
public class Listener4 {

	@EventListener
	public void listenerMyEvent1(MyEvent1 event) {
		System.out.println("Listener4 收到事件通知:" + event.getMessage());
		try {
			Thread.sleep(30000);//睡眠30秒,表示某个监听者执行时间过长
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		//do something
		System.out.println("----Listener4--over");
	}

}

执行结果:在三十秒内,会一致阻塞在如下的状态

----发布1开始-------
Listener3 收到事件通知:新消息1
Listener4 收到事件通知:新消息1

由此我们可以大胆推断:事件发布是一个一个监听者的顺序执行,其中任意一个监听者出现执行缓慢或阻塞,都会导致整个事件发布受阻(后面我们会者源码中验证这个推断)。
就像一个明星不能因为其中粉丝的特殊情况改变自己的行程一样,我们的事件发布也不能因为某些不可预知的“特例监听者”导致整个事件发布受影响。 异步监听就可以解决这个问题,并且应用起来很简单,只需要加一两个注解

@Async

应用在“可能出现意外”的事件监听者方法上,如下所示

/**
 * 异常监听者
 */
@Component
public class Listener4 {

	@Async
	@EventListener
	public void listenerMyEvent1(MyEvent1 event) {
		System.out.println("Listener4 收到事件通知:" + event.getMessage());
		try {
			Thread.sleep(30000);//睡眠30秒,表示某个监听者执行时间过长
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		//do something
		System.out.println("----Listener4--over");
	}

}

在此之前,还需要加上@EnableAsync注解
@EnableAsync是应用在整个项目的配置,只需要写一次即可,表示该项目中允许使用@Async注解,如下所示

@EnableAsync
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);

	}

}

再执行事件发布

----发布1开始-------
Listener3 收到事件通知:新消息1
Listener1 收到事件通知:新消息1
Listener2 收到事件通知:新消息1
----发布1完成-------
Listener4 收到事件通知:新消息1
----Listener4--over

事件发布照常执行,并没有因为出现“异常的事件监听者”而阻塞后面的其他监听者。

为什么前面我们说“应用在可能出异常”的事件监听者方法上,而不是全都加上异步注解呢?因为资源占用,每使用一个异步方法,整个系统就多增加一个线程,而且在执行完之后并不会自动销毁,那些我们写的时候就确定不可能出现阻塞的监听器就可以不用加,当然 最好是使用线程池。具体用法及讲解请见博客“Spring中的@Async用法及应用场景”(这两个注解并非专门用来配合事件机制使用的)

spring监听器

一开始我们也讲了,实际生产中很少直接使用事件机制。而是间接的应用比较多,我们平时总能在web开发中听到监听器的说法(和过滤器还经常混淆),这里的监听器和前面的“监听者”是不是有关联呢,答案是肯定的。
打开定义事件的父类ApplicationEvent,使用IDE看一下该类的所有子类(涂掉的是我们自己定义的事件)
image
这里显示的都是Spring自带的事件,是我们可以直接进行监听的事件,看看有没有自己熟悉的类。下面以其中的相对比较常见的RequestHandleEvent为例,看看如何使用Spring提供的“监听器”。

RequestHandleEvent

功能:在SpringMvc收到请求时,会触发一个事件,我们可以通过监听该事件捕捉到用户每个发来的rququest请求,下面是个Demo,用来监听这种事件

/**
 * 监听RequestHandledEvent事件
 */
@Component
public class Listener5 {

	@EventListener
	public void listenerMyEvent1(RequestHandledEvent event) {
		
		System.out.println("Listener5 收到事件通知:" + event.getShortDescription());
		//do something
	}

}

启动springboot,访问http://localhost:8080/project/test2,执行结果

---------test2--------
Listener5 收到事件通知:url=[/project/test2]; client=[0:0:0:0:0:0:0:1]; session=[null]; user=[null]; 

可见,spring给提供的事件,监听方式和我们自己写的一样(本质都是一样的)。这个监听器就实现了类似于“request过滤器”的效果。上面列举的其他spring内部事件用法也都是如此,一些常用的功能描述如下

事件名称功能描述
RequestHandledEventrequest请求事件
ContextClosedEvent容器关闭事件
ContextStartedEvent容器启动事件
ContextStoppedEvent容器停止事件

说到这里,也行会有人有些晕了。感觉在其他地方见过类似的监听器,但似乎和上面讲的有点区别。 那是因为你看到是servlet的监听器(或者说是Tomcat的监听器),详情请见spring/tomcat的过滤器监听器区别。

观察者模式

像Spring这样的优秀开源软件都会应用很多设计模式及编程思想。事件机制就是一个很好的例子。“事件驱动编程”本身就是一个很火的概念,Spring的事件机制就是“事件驱动编程”思想的应用。而这种思想更抽象的说法叫“观察者模式”或者叫“监听者模式” /“发布-订阅模式”(和观察者模式有细微差别:发布者和订阅者都不需要知道对方的存在)

概念角色

  1. 目标主题(Subject,目标主题这个翻译是不太准确的)
    • 实际开发中目标主题是搭建这个观察者模式主要代码逻辑,包括
      • 注册中心(增删改查观察者)
      • 事件发生时,遍历所有的观察者,并执行观察者实现的响应事件的方法
  2. 观察者(Observer:观察者模式的名字就来源于这里)
    • 根据自己的需要实现事件触发接口方法
    • 等待事件发生后被动被调用

示例代码

更详细的概念解释及示例,请看观察者模式(Observer模式)详解

定义目标主题Subject(观察模式的运作中心)

/**
 * 目标主题(观察模式的运作中心)
 */
public interface Subject {
	/**
	 * 增加观察者
	 * @param observer
	 */
	void addObserver(Observer observer);
	
	/**
	 * 删除观察者
	 * @param observer
	 */
	void removeObserver(Observer observer);
	
	/**
	 * 通知观察者
	 */
	void inform();
}

定义观察者Observer

/**
 * 观察者
 */
public interface Observer {
	/**
	 * 观察者在事件发生时的响应
	 */
	void todo();
}

那目标主题是怎么通知到每个观察者的呢。这里并没有什么高深的技术,就是把遍历目标主题里注册的全部观察者,调用他们的todo()方法。也就是说,所谓观察者在模式中是“被动观察”的,下面是目标主题的实现类

public class SubjectImpl implements Subject {
	
	private List<Observer> observerList = new ArrayList<Observer>();

	@Override
	public void addObserver(Observer observer) {
		observerList.add(observer);
	}

	@Override
	public void removeObserver(Observer observer) {
		observerList.remove(observer);
	}

	@Override
	public void inform() {
		System.out.println("事件触发...");
		for (Observer observer : observerList) {
			observer.todo();
		}
	}

}

其他示例代码就略过(详细请看demo3),直接看执行结果

事件触发...
------观察者1响应事件------
------观察者2响应事件------

既然事件机制是一种观察者模式的实现,那么对照一下概念:

观察者模式spring事件机制
观察者(Observer)监听者(Listener)
目标主题(Subject)EventMulticaster(事件广播器)
-Event(事件)

小结

  • Observer对应Listener,这个比较好理解。
  • 事件机制中的Event是spring新增的抽象概念(更准确的说是“事件驱动编程”里抽象出来的概念)。
  • 事件机制中的“发布者”其实只是一个事件触发点,并不在观察者模式的定义里。
  • 从Subject所包含的核心逻辑就验证里前面的猜想:监听者者是一个一个顺序执行的。而观察者模式里的核心Subject是spring给我实现好的。下面我们通过源码,看一下Spring给实现好的Subject。

源码解析

事件广播器

前面说的EventMulticaster(事件广播器)在spring中实际叫ApplicationEventMulticaster ,下面是它的接口方法(之所先看接口,是因为接口是spring的骨架,定义了spring的原始设计想法,后面的抽象方法、具体实现都比较复杂,但都只是对接口的填充而已)

image
是不是和我们定义的Subject很像,虽然这里有7个方法,但归结起来还是那两个需求(3个方法):

  1. 增删维护观察者: addxxx, removexxx
  2. 通知观察者: multicastEvent(对应上面我们写的todo()方法)

看一下这个接口的注释说明

Interface to be implemented by objects that can manage a number of ApplicationListener objects and publish events to them.
这是一个这样的接口:它用来管理一堆监听者对象并发布事件给他们

Spring事件机制流程

通过前面介绍事件广播器的几个功能,大致可以猜到Spring为我们做了些什么,但还是对细节会有一些疑问:

  1. Listener是什么时候加载的,谁加载的
  2. 是怎么执行到Listener里的方法里的

我就不直接截源码的图里,因为以本人看文章的体验来说,枯燥的截图对读者帮助并不大,下面直接看根据源码画的流程图
image

流程解释
  1. 流程分为两个阶段
    • 一个是启动Spring容器(或者可以直接理解为启动Springboot的时候)
    • 另外一个是我们触发事件的时候
  2. 核心还是事件广播器ApplicationEventMulticaster(这里实际指的是它的实现类ApplicationEventMulticaster,SimpleApplicationEventMulticaster)
  3. 增加监听器是在启动Spring容器时候完成的(图中紫红色的部分)。这也是Spring容器的核心位置。为防止读者在自己看源码的时候疑惑,图中我特意把两个加载linstener的过程都画出来。这两个addxxx分别是:
    • 增加普通的监听器。比如我们demo1中的Linstener1,Linstener2,是我们用“传统手段”,通过实现ApplicationListener实现的监听器(细节请看附录1)。
    • 增加使用注解(@EventListener)实现的监听器(细节请看附录1)。
  4. 事件发布。这是我们写程序可触及到的一部分流程。核心是ApplicationEventPublisher。这里会首先去调用事件广播器的getApplicationListeners方法,拿到所有的监听器(由于前面启动时已经加载里所有监听器,所以这里可以拿到),然后逐个调用监听器内的方法。

附录

加载监听器的几个细节

前面说了加载监听器分为两步,其实也对应了在AbstractApplicationEventMulticaster#ListenerRetriever的两个成员变量
image
通过名字也能看出,下面的applicationListenerBeans是第一步通过addApplicationListenerBean加载的。而上面的applicationListeners是第二步通过addApplicationListener加载的。下面详细说一下这两种加载方式

addApplicationListenerBean

这一步加载的是所有通过“传统手段”,正常实现ApplicationListener接口的监听器。找到这些监听器也比较好理解:
String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
通过接口类型找到所有的beanName,就可以找到Listener1,Listener2了。

addApplicationListener

这一步是在EventListenerMethodProcessor里完成的,但这一步其实并不是完全专门为加载注解监听器的(而是主要工作是完成注解监听器的加载)。如下关键的一句话:
String[] beanNames = beanFactory.getBeanNamesForType(Object.class);
它首先加载里所有的Object对象,也就是说加载里Spring里管理的所有对象,包括那些传统手段写的监听器。其中最令人兴奋的就是下面这句话:如何把注解方法变成一个监听器对象
image

大致原理就是通过代理或者CGLib生成动态代理对象(细节将在之后的其他文章里解释)。
其实上面关于监听器,还是跳过里一些不太重要的细节:在这两步加载前面还会加载一些Spring内部特殊的监听器。

如何读的源码

把示例项目跑起来

Spring的代码千千万,硬生生的找代码肯定是困难的

在合适的位置打断点

以上面为例,一开始我们只知道ApplicationEventMulticaster这么个接口。现在我们想知道它是如何/何时加载监听器的,那么就需要在关键的方法上打断点,这里的关键方法就是那两个addxxx方法。首先就是找到它的实现类

image

接下来就是以debug方式启动Spring,发现启动过程中就停到断点处了,然后用debug的方法栈

image

往前点,我们可以看到上一步是在哪个位置,然后就知道这个动作的起因在哪里。
通过这种方式,就可以知道:如果想知道执行的时候的流程,那么断点应该打在我们写的监听器方法内就可以了。

总结

  1. Spring事件机制就是观察者模式的一种实现。
  2. 我们所谓的“监听器”就是事件机制的一个应用,是Spring框架给我定义好的一些可以直接监听的事件,包括对request请求的监听,对bean对象的监听等等。
  3. 事件发生后,监听器会一个一个的执行,所以效率并不高。可以使用异步方法防止异常的监听器阻塞事件发布,但要注意因此带来的线程资源开销。
  4. 使用监听器注解本质是Spring通过动态代理生成监听器对象,在容器启动的时候加载好了,所以这里大可不必担心影响效率(最多也就是拖慢点启动速度)。

参考链接

Spring Event事件通知机制 源码学习
@EventListener注解使用及源码解析
观察者模式(Observer模式)详解