Java Spring

事件通知机制

可以先将其理解为“事件通知机制”,即每当孙悟空将金箍棒敲在地上时,就相当于给土地发了一封 email 的通知,告诉他俺老孙来了,赶快出来接驾。当土地收到通知之后就会立即现身了。
大家都知道 Spring 已经提供好了事件监听、订阅的实现,接下来用代码来实现一下这个场景。
首先要定义一个事件,来记录下孙悟空敲地的动作。

  1. @Getter
  2. public class MonkeyKingEvent extends ApplicationEvent {
  3. private MonkeyKing monkeyKing;
  4. public MonkeyKingEvent(MonkeyKing monkeyKing) {
  5. super("monkeyKing");
  6. this.monkeyKing = monkeyKing;
  7. }
  8. }

其中 MonkeyKing 是定义好的孙悟空的实体类

  1. @Data
  2. public class MonkeyKing {
  3. /**
  4. * 是否敲地,默认为否
  5. **/
  6. private boolean knockGround = false;
  7. }

然后需要实现 ApplicationListener 来监听孙悟空敲地的动作。

  1. @Component
  2. public class MyGuardianListener implements ApplicationListener<MonkeyKingEvent> {
  3. @Override
  4. public void onApplicationEvent(MonkeyKingEvent event) {
  5. boolean knockGround = event.getMonkeyKing().isKnockGround();
  6. if(knockGround){
  7. MyGuardian.appear();
  8. }else{
  9. MyGuardian.seclusion();
  10. }
  11. }
  12. }

最后来验证下整个流程。

  1. @PostMapping
  2. public void testEvent(@RequestParam boolean knockGround) {
  3. MonkeyKing monkeyKing = new MonkeyKing();
  4. monkeyKing.setKnockGround(knockGround);
  5. MonkeyKingEvent monkeyKingEvent = new MonkeyKingEvent(monkeyKing);
  6. //发布孙悟空敲地的动作事件
  7. applicationEventPublisher.publishEvent(monkeyKingEvent);
  8. }

当调用testEvent()方法传入knockGround为 true 时,打印

  1. 土地公公出现了

传入为false时,打印

  1. 土地公公遁地了

这样就简单实现了“孙悟空召唤土地”的功能。

观察者模式

说是事件通知机制也好,事件监听-订阅的实现也罢,其实它内部的最终实现原理依赖的是观察者模式。看到这,先不要胆怯,不要觉得设计模式晦涩难懂、久攻不下。用通俗易懂的小故事来认识一下观察者模式。
故事是这样的,上边只说了孙悟空敲地的动作,但是是否还记得孙悟空将金箍棒往天上一指,便换来雷公电母、龙王等为其施法布雨?闭上双眼,与虎力大仙比试的场景仍历历在目。
由此可见,不光土地能收到孙悟空的通知,连雷公电母和龙王也是可以接收到的。在这里,把孙悟空比作主题,也就是大家说的被观察者和 Subject的概念,把雷公电母和龙王以及土地比作观察者。
以下是代码逻辑:
首先,定义一个主题的基础类,里边会记录所有订阅该主题的观察者列表,还包含了增加、删除以及通知观察者的方法。

  1. public class Subject {
  2. //观察者列表
  3. private Vector<Observer> vector = new Vector();
  4. /**
  5. * 增加观察者
  6. **/
  7. public void addObserver(Observer observer){
  8. vector.add(observer);
  9. }
  10. /**
  11. * 删除观察者
  12. **/
  13. public void deleteObserver(Observer observer){
  14. vector.remove(observer);
  15. }
  16. /**
  17. * 通知所有观察者
  18. **/
  19. public void notifyObserver(String goldenCudgel) {
  20. for(Observer observer : vector) {
  21. observer.update(goldenCudgel);
  22. }
  23. }
  24. }

然后,定义一个观察者的接口,包含观察者收到通知之后的“动作”。

  1. public interface Observer {
  2. void update(String goldenCudgel);
  3. }

这时候再分别定义出“土地”、“雷公电母”、“龙王”的观察者实体类,实现具体的打雷下雨等动作。
“雷公电母”、“龙王”等实现与“土地”类似,故此处仅展示观察者“土地”。

  1. @Component
  2. public class MyGuardianObserver implements Observer {
  3. @Override
  4. public void update(String goldenCudgel) {
  5. if(upGoldenCudgel(goldenCudgel)) {
  6. System.out.println("土地公公出现了");
  7. }
  8. }
  9. public boolean upGoldenCudgel(String goldenCudgel){
  10. if(Objects.equals(goldenCudgel,"down")){
  11. return true;
  12. }
  13. return false;
  14. }
  15. }

接着,就可以定义被观察者的具体实现类“孙悟空”了

  1. public class MonkeyKingSubject extends Subject{
  2. public void doGoldenCudgel(String goldenCudgel){
  3. notifyObserver(goldenCudgel);
  4. }
  5. }

最后来做个测试看看他们能不能响应孙悟空的通知。

  1. @PostMapping
  2. public void observerTest(){
  3. MonkeyKingSubject subject = new MonkeyKingSubject();
  4. subject.addObserver(new ThunderGodObserver());
  5. subject.addObserver(new MyGuardianObserver());
  6. subject.addObserver(new DragonKingObserver());
  7. subject.doGoldenCudgel("up");
  8. System.out.println("我是分割线-----------------------------");
  9. subject.doGoldenCudgel("down");
  10. }

结果展示

  1. 雷公电母发出电闪雷鸣
  2. 龙王前来下雨
  3. 我是分割线-----------------------------
  4. 土地公公出现了

总结

观察者模式与事件通知机制都是在一对多的关系中,当一个对象被修改时,则会自动通知依赖它的对象,两者之间相互独立,互相解耦,这样既省去了反复检索状态的资源消耗,也能够得到最高的反馈速度。
当然它的缺点也不容忽视:

  1. 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间;
  2. 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃;
  3. 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化;

    源码解析

    版本号:spring-framework-5.0.x
    为了实现广播与监听的功能,Spring提供了两个重要的函数式接口:ApplicationEventPublisherApplicationListener。前者的publishEvent()方法提供了发送广播的能力;后者的onApplicationEvent()方法提供了监听并处理事件的能力。
    接下来就来分析一下Spring是如何运用这两种能力的。
    不知道大家对单例对象的初始化调用过程是否熟悉?主要调用方法流程如下:
    Spring源码中广播与监听实现 - 图1

    发送广播

    applyBeanPostProcessorsBeforeInitialization方法会去遍历该工厂创建的所有的Bean后置处理器,然后去依次执行后置处理器对应的postProcessBeforeInitialization方法。
    在该方法的实现类中看到了两个熟悉的类名
    Spring源码中广播与监听实现 - 图2
    这俩类是在beanFactory的准备工作过程中添加的两个bean的后置处理器,所以这个地方会依次去执行这两个类中的实现方法。
    Spring源码中广播与监听实现 - 图3
    由于蓝框中类的实现方法是默认实现按照原样返回的给定的bean,所以此处不用过多分析,重点来看下红框中类的方法实现。
    该方法中最重要的是invokeAwareInterfaces方法,它的作用是检测对应的bean是否实现了某个Aware接口,如果实现了的话就去进行相关的调用。

    1. if (bean instanceof ApplicationEventPublisherAware) {
    2. ((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext);
    3. }

    可以发现在invokeAwareInterfaces方法中出现了如上代码,这不就是和广播发送相关的吗?所以只要写一个类来实现ApplicationEventPublisherAware接口,就可以在该bean中注入一个ApplicationEventPublisher对象,也就获得了发送广播的能力。

    监听消息

    applyBeanPostProcessorsAfterInitialization方法会去遍历该工厂创建的所有的Bean后置处理器,然后去依次执行后置处理器对应的postProcessAfterInitialization方法。
    同样的,该方法的实现类中也有ApplicationContextAwareProcessorApplicationListenerDetector两个类,但是不同的是,前者的类的实现方法是默认实现按照原样返回的给定bean,而后者做了相关的处理。

    1. this.applicationContext.addApplicationListener((ApplicationListener<?>) bean);

    上述代码是将实现了ApplicationListener接口的bean添加到监听器列表中,最终是保存在AbstractApplicationEventMulticaster的成员变量defaultRetriever的集合applicationListeners中。
    猜想:当发送广播消息时,就直接找到集合中的这些监听器,然后调用每个监听器的onApplicationEvent方法完成事件的处理。

    案例分析

    refresh()finishRefresh()方法中,

    1. publishEvent(new ContextRefreshedEvent(this));

    发送一条事件类型为ContextRefreshedEvent的广播消息,用来代表Spring容器初始化结束。通过分析发现,该方法中最主要的就是如下代码:

    1. //真正的广播交给 applicationEventMulticaster 来完成
    2. getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);

    refresh()initApplicationEventMulticaster()applicationEventMulticaster初始化为SimpleApplicationEventMulticaster
    在实现类SimpleApplicationEventMulticaster的方法中,会找到已注册的ApplicationListener列表,然后分别调用invokeListener方法(将监听和事件作为参数传到方法并执行的过程就是发送广播的过程)。
    底层调用的是listener.onApplicationEvent(event);方法,也就是各个监听实现类单独处理广播消息的逻辑。

    消息与监听绑定

    看到这儿,是不是已经发现了:消息类型和监听器的绑定发生在广播过程中。接下来就一探究竟
    看一下multicastEvent()方法中的getApplicationListeners(event, type)方法。
    在该方法中,用到了ConcurrentHashMap类型的缓存retrieverCache,所以每种类型的事件在广播的时候会触发一次绑定操作。它的key由事件的来源和类型确定,它的value中就包含了由事件来源和类型所确定的所有监听列表。
    其中绑定的逻辑就出现在retrieveApplicationListeners方法中,大家可以去源码中查看。

    实战教学

    纸上得来终觉浅,绝知此事要躬行。为了更好地理解广播与监听的流程,当然得用实战来加以辅佐!

    自定义事件

    1. public class MyEvent extends ApplicationContextEvent {
    2. public MyEvent(ApplicationContext source) {
    3. super(source);
    4. }
    5. }

    自定义广播

    1. @Component
    2. public class MyPublisher implements ApplicationEventPublisherAware, ApplicationContextAware {
    3. private ApplicationEventPublisher applicationEventPublisher;
    4. private ApplicationContext applicationContext;
    5. @Override
    6. public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
    7. this.applicationEventPublisher = applicationEventPublisher;
    8. }
    9. @Override
    10. public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    11. this.applicationContext = applicationContext;
    12. }
    13. //发送广播消息
    14. public void publishEvent(){
    15. System.out.println("我要开始发送消息了。。。");
    16. MyEvent myEvent = new MyEvent(applicationContext);
    17. applicationEventPublisher.publishEvent(myEvent);
    18. }
    19. }

    MyPublisher实现了ApplicationEventPublisherAware接口 ,在Spring初始化(见上文中的invokeAwareInterfaces)的时候会回调setApplicationEventPublisher方法,获取到初始化(添加bean后置处理器ApplicationContextAwareProcessor)时的AbstractApplicationContext,而AbstractApplicationContext又间接实现了ApplicationEventPublisher而获得发送能力。真正执行的是 AbstractApplicationContext类中的 publishEvent 方法。

    自定义监听

    1. @Component
    2. public class MyEventListener implements ApplicationListener<MyEvent> {
    3. @Override
    4. public void onApplicationEvent(MyEvent event) {
    5. System.out.println("我监听到你的消息了");
    6. }
    7. }

    MyEventListener实现了ApplicationListener接口,在Spring初始化(见上文中的addApplicationListener)的时候会添加到applicationListeners中,在执行publishEvent 方法时就会走MyEventListener中的onApplicationEvent方法。

    客户端

    1. @RestController
    2. @RequestMapping("/demo")
    3. public class DemoTest {
    4. @Autowired
    5. private MyPublisher myPublisher;
    6. @RequestMapping("/test")
    7. public void test() {
    8. myPublisher.publishEvent();
    9. }
    10. }

    访问127.0.0.1:8008/demo/test就可以发送广播了,发送与监听内容如下:

    1. 我要开始发送消息了。。。
    2. 我监听到你的消息了