观察者模式

Spring Event是Spring框架的核心特性之一,它是基于标准的观察者设计模式实现的,本篇文章就通过分析Spring中的事件机制的原理,一方面学习使用Spring框架开发时,如何更好的实现一些场景下的功能,更重要的一方面是通过学习优秀的开源项目中的编码思想学习设计模式,以便在日后的开发中有意识的改良代码结构,提高程序的可读性和可维护性。

什么是观察者模式?

观察者模式是一种行为型设计模式,它定义了一种一对多的依赖关系,当一个对象的状态发生改变时,其所有依赖者都会收到通知并自动更新。

为什么要使用观察者模式?

举个例子,在使用HTTP协议实现一个即时通讯系统时,由于HTTP2之前的版本不具有服务端推送的功能,因为HTTP是无状态的,服务端无法知道客户端是谁,所以客户端就需要不断的向服务端询问,是否有它的消息,如果你没有,隔一段时间再次发起询问…这就是HTTP轮询的设计方案,早期的web端的即时通讯系统就是采用类似的方案设计的。

这种方案存在的问题:

  • 客户端数量少的时候还好,当客户端的数量多的时候,服务端将不堪重负
  • 消息不及时,因为是每隔一段时间轮询一次,受间隔时间的影响
  • 效率低,大多数情况下的轮询报文成为了网络流量中的垃圾,占用外网带宽

所以基于以上问题,后来出现了WebSocket协议,通过建立长连接,支持服务端向客户端主动发起请求。

这个例子中,客户端就是观察者,它们都需要关注服务器上和自己密切相关的消息,但是需要主动去询问,这浪费了大量的时间和资源,所以就引入一种观察者设计模式,让观察者闲下来,就像早先的邮差一样,有你的邮件,就打个电话通知你,然后你在抽出时间处理。(反客为主)

最明显的一个区别就是:观察者设计模式将传统的观察者置于被动的地位,并引入一个Notifier来通知相应的观察者。

观察者模式解决了什么问题?

观察者模式显然提高的软件的执行效率,使软件的代码结构更加合理清晰。

Spring框架中的Spring Event特性

Spring Event中主要包含了三个核心的组件:

  • ApplicationEvent(事件)
  • **ApplicationEventPublisherAware **(事件的发布者)
  • ApplicationListener(事件的监听者)

Spring中的默认事件:

Spring Event

场景:高中学校不同年级放假的场景,一般来讲学校会先开会,然后通知不同年级的主任具体的放假时间,班级主任再通知所在年级的所有班主任具体的放假时间,学生肯定最关心的就是放假时间。

环境:

  • SpringBoot 2.7.18
  • Lombok 1.18.30

自定义假期事件

@Getter
public class HolidayEvent extends ApplicationEvent {
   

    // 假期开始时间
    private Date startDate;
    // 假期结束时间
    private Date endDate;

    public HolidayEvent(Object source) {
   
        super(source);
    }

    public HolidayEvent(Object source, Date startDate, Date endDate) {
   
        super(source);
        this.startDate = startDate;
        this.endDate = endDate;
    }
}

班主任类(充当Publisher)

这里设计可能不太严谨,现实情况下班主任应该也是监听者,只不过监听的是上级的通知,这里简化了结构。

@Component
public class HeadTeacher implements ApplicationEventPublisherAware {
   

    private ApplicationEventPublisher publisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
   
        this.publisher = publisher;
    }

    // 班主任通过该类获取放假通知
    public void getHolidayNotice() {
   
        System.out.println("Teacher Get Notification");
        // 通过publisher将放假通知告诉学生
        publisher.publishEvent(new HolidayEvent(this, new Date(System.currentTimeMillis()), new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000)));
    }
}

学生类(充当观察者 Observer)

@Component
public class Student implements ApplicationListener<HolidayEvent> {
   
    @Override
    public void onApplicationEvent(HolidayEvent event) {
   
        System.out.println("Student Get Holiday Notification: " + event.getStartDate().toString() + "~" + event.getEndDate().toString());
        // 学生得知放假通知的行为
    }
}

启动

@SpringBootApplication
public class SpringEvent {
   

    public static void main(String[] args) {
   
        ApplicationContext context = SpringApplication.run(SpringEvent.class, args);
        HeadTeacher teacher = context.getBean(HeadTeacher.class);
        teacher.getHolidayNotice();
    }
}

运行结果

Teacher Get Notification
Student Get Holiday Notification: Fri Dec 01 23:52:10 CST 2023~Sat Dec 02 23:52:10 CST 2023

当然Spring自4.2开始支持注解,这里不再演示

Spring Event原理

我按照自己在调试源码的时候的思路进行分析

首先直接在调用getHolidayNotice的地方直接打断点,然后启动调试:
在这里插入图片描述
在这里插入图片描述
从上面调试的步骤发现,其实ApplicationEventPublisher的实例其实是一个AnnotationConfigApplicationContext(这其实也能理解,接口在Java中就是某种功能)

类的继承结构:

在这里插入图片描述

其实ApplicationContext接口就实现了ApplicationEventPublisher接口,也就是所有的上下文的实例都具有发布事件的能力。

继续,走进AnnotationConfigApplicationContext(其实是调用的父类AbstractApplicationContext中的方法)中的publishEvent方法,最后调用的该方法:

    protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
   
        Assert.notNull(event, "Event must not be null");
        Object applicationEvent;
        if (event instanceof ApplicationEvent) {
   
            applicationEvent = (ApplicationEvent)event;
        } else {
   
            applicationEvent = new PayloadApplicationEvent(this, event);
            if (eventType == null) {
   
                eventType = ((PayloadApplicationEvent)applicationEvent).getResolvableType();
            }
        }
        // earlyApplicationEvents是用来保存之前的事件的,如果没有设置则不保存
        if (this.earlyApplicationEvents != null) {
   
            this.earlyApplicationEvents.add(applicationEvent);
        } else {
   
            // 这里是关键,获取一个多路的广播器,并调用multicastEvent将该事件广播给监听者
            this.getApplicationEventMulticaster().multicastEvent((ApplicationEvent)applicationEvent, eventType);
        }

        if (this.parent != null) {
   
            if (this.parent instanceof AbstractApplicationContext) {
   
                ((AbstractApplicationContext)this.parent).publishEvent(event, eventType);
            } else {
   
                this.parent.publishEvent(event);
            }
        }

    }
    ApplicationEventMulticaster getApplicationEventMulticaster() throws IllegalStateException {
   
        if (this.applicationEventMulticaster == null) {
   
            throw new IllegalStateException("ApplicationEventMulticaster not initialized - call 'refresh' before multicasting events via the context: " + this);
        } else {
   
            //  这里返回该类所维护的多路广播器
            return this.applicationEventMulticaster;
        }
    }

到这里,我其实对于这个广播器什么初始化的还有疑惑,不过仔细观察会发现上面为null的情况,为了确保多路广播器被初始化,需要先调用context的refresh方法(等会再去查看refresh方法中的逻辑)

//SimpleApplicationEventMulticaster.java
    
    public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
   
        ResolvableType type = eventType != null ? eventType : this.resolveDefaultEventType(event);
        Executor executor = this.getTaskExecutor();
       
        Iterator var5 = this.getApplicationListeners(event, type).iterator();

        while(var5.hasNext()) {
   
            ApplicationListener<?> listener = (ApplicationListener)var5.next();
            if (executor != null) {
   
                executor.execute(() -> {
   
                    this.invokeListener(listener, event);
                });
            } else {
   
                this.invokeListener(listener, event);
            }
        }

    }

上面的代码应该很容易看懂,因为我是根据IDEA反编译字节码文件得出的,变量名可能不太合适,但不影响理解,大致流程:

  • 获取应用上下文中监听event类型的监听器(观察者)列表的迭代器
  • 遍历每一个迭代器中每个观察者,然后调用它们的onApplicationEvent方法(这里调用的invokeListener)
  • 上面调用invokeListener有两种方式,如果配置了Executor,则异步执行该方法,否则同步执行

到这里和事件的广播相关的流程就基本梳理完毕了,总结一下:

  1. 首先Spring通过自动注入的方式,调用我在HeadTeacher类中重写父类的setApplicationEventPublisher方法,将AnnotationConfigApplicationContext的实例传递进来,并使用一个变量publisher来进行接收。
  2. 当我调用HeadTeacher实例的getHolidayNotice的方法是,在内部调用publisher的publishEvent方法,将事件广播给观察者(Listener或者称为Observer,Spring中称为Listener)
  3. publishEvent方法本质上是AnnotationConfigApplicationContext继承自AbstractApplicationContext中的方法,方法内部通过获取本地维护的一个广播器,然后调用广播方法,将事件广播出去
  4. 在广播器的multicastEvent方法中,通过获取一个我们发布的事件类型(这就是为啥我们Student类实现的是一个泛型接口)的监听者列表,然后通过遍历监听者列表,调用他们的onApplicationEvent方法来达到事件通知的目的

到这里,我们回过头来,看一下之前的困惑,这个多路广播器是什么玩意,什么时候初始化的,这次直接在run方法这里打断点:

此处跳过若干不重要的步骤…

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

以上就是为啥最后的context是一个AnnotationConfigApplicationContext的实例

上面图片中的this.refreshContext是我们关注的方法,其它的本篇文章不涉及。

最终调用就是下面的refresh方法:

    public void refresh() throws BeansException, IllegalStateException {
   
        synchronized(this.startupShutdownMonitor) {
   
            StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
            this.prepareRefresh();
            ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
            this.prepareBeanFactory(beanFactory);

            try {
   
                this.postProcessBeanFactory(beanFactory);
                StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
                this.invokeBeanFactoryPostProcessors(beanFactory);
                this.registerBeanPostProcessors(beanFactory);
                beanPostProcess.end();
                // 对多路广播器进行初始化
                this.initMessageSource();
                this.initApplicationEventMulticaster();
                this.onRefresh();
                this.registerListeners();
                this.finishBeanFactoryInitialization(beanFactory);
                this.finishRefresh();
            } catch (BeansException var10) {
   
                if (this.logger.isWarnEnabled()) {
   
                    this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var10);
                }

                this.destroyBeans();
                this.cancelRefresh(var10);
                throw var10;
            } finally {
   
                this.resetCommonCaches();
                contextRefresh.end();
            }

        }
    }

initApplicationEventMulticaster还是AnnotationConfigApplicationContext父类AbstaractApplicationContext中的方法, 看完这个方法就基本明白了

protected void initApplicationEventMulticaster() {
     

  ConfigurableListableBeanFactory beanFactory = this.getBeanFactory();  

  if (beanFactory.containsLocalBean("applicationEventMulticaster")) {
     
   
    this.applicationEventMulticaster = (ApplicationEventMulticaster)beanFactory.getBean("applicationEventMulticaster", ApplicationEventMulticaster.class);  

  if (this.logger.isTraceEnabled()) {
     

    this.logger.trace("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]");  

  }  

    } else {
     

      this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);  

  beanFactory.registerSingleton("applicationEventMulticaster", this.applicationEventMulticaster);  

  if (this.logger.isTraceEnabled()) {
     

    this.logger.trace("No 'applicationEventMulticaster' bean, using [" + this.applicationEventMulticaster.getClass().getSimpleName() + "]");  

  }  

    }  

}

该方法的逻辑:

首先通过bean工厂检查是否存在用户自己写的多路广播器,不存在则创建一个SimpleApplicationEventMulticaster的实例,并把当前实例以单例的模式添加到容器中进行管理,这种情况下最终执行onApplicationEvent方法时都是同步的。

如果想异步执行怎么办,根据前面的逻辑,我们可以自定义配置一个多路广播器,并在其中给它配置一个Executor的实例,让其异步执行。

比如:

@Configuration
public class Config {
   

    @Bean
    public ApplicationEventMulticaster applicationEventMulticaster() {
   
        SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();

        multicaster.setTaskExecutor(new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),
                Runtime.getRuntime().availableProcessors() * 2,
                1, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10)));

        return multicaster;
    }
}

这样,当beanFactory发现我们自定义了多路广播器,就会使用我们自定义的。

总结

本篇文章通过对Spring Event的原理进行分析,学习了Spring框架中的观察者模式实现,利用IDEA强大的调试能力,其实Spring源码也没有那么难理解(但是继承体系真的好复杂)。

相关推荐

  1. 观察模式 Observer

    2023-12-06 18:12:06       42 阅读
  2. 观察模式学习

    2023-12-06 18:12:06       35 阅读

最近更新

  1. TCP协议是安全的吗?

    2023-12-06 18:12:06       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2023-12-06 18:12:06       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2023-12-06 18:12:06       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2023-12-06 18:12:06       20 阅读

热门阅读

  1. 将数据导出为excel的js库有哪些

    2023-12-06 18:12:06       39 阅读
  2. PyTorch深度学习实战(18)——目标检测基础

    2023-12-06 18:12:06       37 阅读
  3. informer辅助笔记:exp/exp_informer.py

    2023-12-06 18:12:06       32 阅读