一、简单看一下观察者模式
有时候我们希望做了一件事,触发其他的一些行为动作,这时候可以通过代码强耦合的方式顺序编码,比如美女Liuer下班了,隔壁的小王喜欢她,来公司接她,代码如下:
public class Liuer {
public void endWork() {
//Liuer下班了,穿衣服穿鞋,出公司等一系列代码
......
//此处调用小王要来接小丽,小王的一系列动作
......
}
}
这种编码可以实现需求,看起来也没什么问题。这时候还有个小张也要来接她,好整,改一下Liuer代码:
public class Liuer {
public void endWork() {
//Liuer下班了,穿衣服穿鞋,出公司等一系列代码
......
//此处调用小王要来接小丽,小王的一系列动作
......
//此处调用小张要来接小丽,小张的一系列动作
......
}
}
可以,这么改也成,但以后可能还会加小李小刘,也有可能去掉小王小张,你会发现,这种编码方式不易扩展,需要频繁修改Liuer的代码,而且Liuer下班本就是自己的事儿,在Liuer下班的代码里大量调用别人的行为代码也不合适。
此时可以引入观察者模式,观察者监听着他们感兴趣的事件,当事件发生了,做出行为。
Liuer的endWork()只管自己下班,不用管有几个人来接她,Liuer的下班可以看成是一个事件,诸如小王小张之流,他们对丽丽下班感兴趣,他们就可以成为观察者,也可以说是监听者,他们观察着/监听着柳儿下班,那么我们伪代码可以改成下面这样:
public class Liuer {
public void endWork() {
//Liuer下班了,穿衣服穿鞋,出公司等一系列代码
......
//此处不在直接调用男性动物的代码,只需要发布一个事件,如下:
Publisher.publish();
}
}
OK,Liuer调用了一个事件发布的工具类代码,接下来是这个工具类假设代码:
public class Publisher {
private static List<Object> listeners = new LinkedList();
public static Object addListener(Object o) {
return listeners.add(o);
}
public static void publish() {
//调用了此方法证明Liuer下班了,那么就循环调用listeners,执行每一个监听器的动作代码
}
}
通过以上的代码我们发现,Liuer下班的代码里,只需要调用一个发布器,然后剩下的事情由发布器来做,至于有没有人、有几个人来接她,她都不用管,再也不用修改Liuer的代码。
对Liuer下班感兴趣的人,我们都放在了发布器的列表容器中,循环调用。当然了,以上代码主要体验思想,并不完善,比如说列表的泛型是Object,这样肯定是没有意义的,因为Object并没有什么特殊的业务代码,并且事件发布器发送的只能是Liuer下班事件。没关系,主要体会这个流程设计思想,对此模式感兴趣可以专门去学一学这个模式,毕竟此篇的目的并不是观察者模式。
二、Spring环境下的事件支持
一句话:spring容器中给咱们提供了一个事件发布器,用它来发布事件。并且由于Spring IOC的天然优势,我们定义的观察者都可以被容器检测到,进而在发布器发布事件后被调用。
那么怎么用?
1.发布事件
首先是事件,我们的Liuer下班了就要发布一个下班事件,需要实现Spring的抽象类ApplicationEvent(此抽象类是对jdk中EventObject的一个扩展),那好,我们定义事件,为了方便观看,我们就定义为内部类:
@component
public class Liuer {
public void endWork() {
//Liuer下班了,穿衣服穿鞋,出公司等一系列代码
......
}
public static class LiuerEndWorkEvent extends ApplicationEvent {
/**
* @param source 也就是事件的详细信息,看我们业务情况,可能你这个发布了一个事件,观察者不但要知道你发生了,还要知道你一些数据
* 比如说Liuer下班这个事件,我们可以另外把她一天的工作状态封装到一个对象里,当作入参传入,然后观察者可以从此对象
* 中取出Liuer的一天工作状态
*/
public HelloEvent(Object source) {
super(source);
}
}
}
然后就是把这个事件发布出去,我们得拿到Spring的时间发布器,好拿,它也是个Ioc中的bean,可以直接注入,也可以通过实现一个Aware接口来拿到(Spring的给我们提供了很多Aware接口,帮助我们在Spring的生命周期中注入一些组件,包括我们可以注入Bean工厂等等),传统方式注入我就不做演示了,实现Aware接口如下:
@component
public class Liuer implements ApplicationEventPublisherAware {
private ApplicationEventPublisher publisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
public void endWork() {
//Liuer下班了,穿衣服穿鞋,出公司等一系列代码
......
//调用发布器
publisher.publishEvent(new LiuerEndWorkEvent ("哥哥们,我下班了。"));
}
public static class LiuerEndWorkEvent extends ApplicationEvent {
/**
* @param source 也就是事件的详细信息,看我们业务情况,可能你这个发布了一个事件,观察者不但要知道你发生了,还要知道你一些数据
* 比如说Liuer下班这个事件,我们可以另外把她一天的工作状态封装到一个对象里,当作入参传入,然后观察者可以从此对象
* 中取出Liuer的一天工作状态
*/
public LiuerEndWorkEvent (Object source) {
super(source);
}
}
}
我把Liuer放到容器中了。上边我们通过构造函数获取到了发布器,并且发布了事件,事件发布的同时放了一条信息:哥哥们我下班了
。
2.监听事件
两种方式,一种是实现特定接口,另一种是标个注解。
① 实现接口(我先拿小王举例):
@component
public class XiaowangListener implements ApplicationListener<LiuerEndWorkEvent> {
@Override
public void onApplicationEvent(LiuerEndWorkEvent event) {
// 会输出:哥哥们我下班了
System.out.println(event.getSource());
}
}
ApplicationListener的泛型是你要监听的事件类型,必须是ApplicationEvent的子类。
这样,当发布器发布了一个LiuerEndWorkEvent事件的时候,就会触发这个监听器代码。并且我们也从事件源信息中取出了:哥哥们我下班了
。
②使用注解(我先拿小王举例):
从Spring4.2开始支持注解
@component
public class XiaowangListener {
@EventListener({LiuerEndWorkEvent .class})
public void onApplicationEvent(LiuerEndWorkEvent event) {
// 会输出:哥哥们我下班了
System.out.println(event.getSource());
}
}
注解的方式有很多好处,比如说方法名称我们随便起,而且可以继续触发事件(有失败情况,稍后讲),比如我们可以这样:
@component
public class XiaowangListener {
@EventListener({LiuerEndWorkEvent .class})
public ApplicationEvent onApplicationEvent(LiuerEndWorkEvent event) {
// 会输出:哥哥们我下班了
System.out.println(event.getSource());
return new HelloEvent("hello......");
}
}
小王监听Liuer下班事件后,做了一些行为之后,又发布了一个HelloEvent,这样,对HelloEvent感兴趣的监听器将会被触发。
上边的两种实现方式,都可以监听多个事件,例如实现接口的方式,将泛型中的类型范围放大,将会监听所有这个类型的事件,比如说:
@component
public class XiaowangListener implements ApplicationListener<ApplicationEvent> {
@Override
public void onApplicationEvent(ApplicationEvent event) {
// 所有的ApplicationEvent被发布的时候,都会调用这段业务代码
System.out.println(event.getSource());
}
}
注解的方式,那就是在注解中指定多个类型数组,或者也可以指定一个范围较大的类型。
3.在新线程中执行监听代码
默认调用发布器的发布和监听器的逻辑执行都是在一个线程中,所以是顺序同步的,如果想要异步多线程进行,官方建议加上@Async注解,启用异步方式,需要在配置类上指定@EnableAsync(其实Spring家族的每个enable…啥啥啥,都是往里边导入了新的配置类、新的BeanPostProcessor组件等等,我们也同样可以按照官方的路子扩展Spring)。
如下:
@component
public class XiaowangListener {
@EventListener({LiuerEndWorkEvent .class})
@Async
public ApplicationEvent onApplicationEvent(LiuerEndWorkEvent event) {
// 会输出:哥哥们我下班了
System.out.println(event.getSource());
return new HelloEvent("hello......");
}
}
此时会在新线程异步执行代码,监听器中的逻辑不会阻塞Liuer中发布器之后的代码。
但是开启异步后,HelloEvent事件会失效,想要紧接着发布新的事件,需要手动调用发布器,返回值的方式是不行了。
写在最后:
- 这个事件是ApplicationContext级别的,在SpringBoot中,初始化ApplicationContext之前SpringBoot还有另外一套事件处理,SpringSecurity也有自己的一套事件处理等等。
- ApplicationContext有很多生命周期事件,我们都可以监听的到,比如说ContextRefreshedEvent。详细资料都可以在官网查阅:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#context-functionality-events
- Spring Data Jpa中就用到了本文所讲的这种ApplicationContext集别的事件,通过Spring Jpa定义的特定注解发布事件,然后用我们本文所讲的方式监听事件,感兴趣可以参考:https://docs.spring.io/spring-data/jpa/docs/2.4.7/reference/html/#core.domain-events,但是由于异步新线程执行监听,所以会与发布者不在同一个事务中,若当你保存一个用户信息之后,马上异步监听然后从数据库读出此数据,可能会不存在。解决办法:要么改成同步监听,在一个事务中。要么在异步监听中sleep一段时间,等待事务提交。