【开源与项目实战:开源实战】85 | 开源实战四(中):剖析Spring框架中用来支持扩展的两种设计模式

news2024/12/25 9:15:00

上一节课中,我们学习了 Spring 框架背后蕴藏的一些经典设计思想,比如约定优于配置、低侵入松耦合、模块化轻量级等等。我们可以将这些设计思想借鉴到其他框架开发中,在大的设计层面提高框架的代码质量。这也是我们在专栏中讲解这部分内容的原因。
除了上一节课中讲到的设计思想,实际上,可扩展也是大部分框架应该具备的一个重要特性。所谓的框架可扩展,我们之前也提到过,意思就是,框架使用者在不修改框架源码的情况下,基于扩展点定制扩展新的功能。
前面在理论部分,我们也讲到,常用来实现扩展特性的设计模式有:观察者模式、模板模式、职责链模式、策略模式等。今天,我们再剖析 Spring 框架为了支持可扩展特性用的 2 种设计模式:观察者模式和模板模式。
话不多说,让我们正式开始今天的学习吧!

观察者模式在 Spring 中的应用

在前面我们讲到,Java、Google Guava 都提供了观察者模式的实现框架。Java 提供的框架比较简单,只包含 java.util.Observable 和 java.util.Observer 两个类。Google Guava 提供的框架功能比较完善和强大:通过 EventBus 事件总线来实现观察者模式。实际上,Spring 也提供了观察者模式的实现框架。今天,我们就再来讲一讲它。
Spring 中实现的观察者模式包含三部分:Event 事件(相当于消息)、Listener 监听者(相当于观察者)、Publisher 发送者(相当于被观察者)。我们通过一个例子来看下,Spring 提供的观察者模式是怎么使用的。代码如下所示:

// Event事件
public class DemoEvent extends ApplicationEvent {
  private String message;
  public DemoEvent(Object source, String message) {
    super(source);
  }
  public String getMessage() {
    return this.message;
  }
}
// Listener监听者
@Component
public class DemoListener implements ApplicationListener<DemoEvent> {
  @Override
  public void onApplicationEvent(DemoEvent demoEvent) {
    String message = demoEvent.getMessage();
    System.out.println(message);
  }
}
// Publisher发送者
@Component
public class DemoPublisher {
  @Autowired
  private ApplicationContext applicationContext;
  public void publishEvent(DemoEvent demoEvent) {
    this.applicationContext.publishEvent(demoEvent);
  }
}

从代码中,我们可以看出,框架使用起来并不复杂,主要包含三部分工作:定义一个继承 ApplicationEvent 的事件(DemoEvent);定义一个实现了 ApplicationListener 的监听器(DemoListener);定义一个发送者(DemoPublisher),发送者调用 ApplicationContext 来发送事件消息。
其中,ApplicationEvent 和 ApplicationListener 的代码实现都非常简单,内部并不包含太多属性和方法。实际上,它们最大的作用是做类型标识之用(继承自 ApplicationEvent 的类是事件,实现 ApplicationListener 的类是监听器)。

public abstract class ApplicationEvent extends EventObject {
  private static final long serialVersionUID = 7099057708183571937L;
  private final long timestamp = System.currentTimeMillis();
  public ApplicationEvent(Object source) {
    super(source);
  }
  public final long getTimestamp() {
    return this.timestamp;
  }
}
public class EventObject implements java.io.Serializable {
    private static final long serialVersionUID = 5516075349620653480L;
    protected transient Object  source;
    public EventObject(Object source) {
        if (source == null)
            throw new IllegalArgumentException("null source");
        this.source = source;
    }
    public Object getSource() {
        return source;
    }
    public String toString() {
        return getClass().getName() + "[source=" + source + "]";
    }
}
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
  void onApplicationEvent(E var1);
}

在前面讲到观察者模式的时候,我们提到,观察者需要事先注册到被观察者(JDK 的实现方式)或者事件总线(EventBus 的实现方式)中。那在 Spring 的实现中,观察者注册到了哪里呢?又是如何注册的呢?
我想你应该猜到了,我们把观察者注册到了 ApplicationContext 对象中。这里的 ApplicationContext 就相当于 Google EventBus 框架中的“事件总线”。不过,稍微提醒一下,ApplicationContext 这个类并不只是为观察者模式服务的。它底层依赖 BeanFactory(IOC 的主要实现类),提供应用启动、运行时的上下文信息,是访问这些信息的最顶层接口。
实际上,具体到源码来说,ApplicationContext 只是一个接口,具体的代码实现包含在它的实现类 AbstractApplicationContext 中。我把跟观察者模式相关的代码,摘抄到了下面。你只需要关注它是如何发送事件和注册监听者就好,其他细节不需要细究。

public abstract class AbstractApplicationContext extends ... {
  private final Set<ApplicationListener<?>> applicationListeners;
  
  public AbstractApplicationContext() {
    this.applicationListeners = new LinkedHashSet();
    //...
  }
  
  public void publishEvent(ApplicationEvent event) {
    this.publishEvent(event, (ResolvableType)null);
  }
  public void publishEvent(Object event) {
    this.publishEvent(event, (ResolvableType)null);
  }
  protected void publishEvent(Object event, ResolvableType eventType) {
    //...
    Object applicationEvent;
    if (event instanceof ApplicationEvent) {
      applicationEvent = (ApplicationEvent)event;
    } else {
      applicationEvent = new PayloadApplicationEvent(this, event);
      if (eventType == null) {
        eventType = ((PayloadApplicationEvent)applicationEvent).getResolvableType();
      }
    }
    if (this.earlyApplicationEvents != null) {
      this.earlyApplicationEvents.add(applicationEvent);
    } else {
      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);
      }
    }
  }
  
  public void addApplicationListener(ApplicationListener<?> listener) {
    Assert.notNull(listener, "ApplicationListener must not be null");
    if (this.applicationEventMulticaster != null) {
    this.applicationEventMulticaster.addApplicationListener(listener);
    } else {
      this.applicationListeners.add(listener);
    }  
  }
  
  public Collection<ApplicationListener<?>> getApplicationListeners() {
    return this.applicationListeners;
  }
  
  protected void registerListeners() {
    Iterator var1 = this.getApplicationListeners().iterator();
    while(var1.hasNext()) {
      ApplicationListener<?> listener = (ApplicationListener)var1.next();     this.getApplicationEventMulticaster().addApplicationListener(listener);
    }
    String[] listenerBeanNames = this.getBeanNamesForType(ApplicationListener.class, true, false);
    String[] var7 = listenerBeanNames;
    int var3 = listenerBeanNames.length;
    for(int var4 = 0; var4 < var3; ++var4) {
      String listenerBeanName = var7[var4];
      this.getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
    }
    Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
    this.earlyApplicationEvents = null;
    if (earlyEventsToProcess != null) {
      Iterator var9 = earlyEventsToProcess.iterator();
      while(var9.hasNext()) {
        ApplicationEvent earlyEvent = (ApplicationEvent)var9.next();
        this.getApplicationEventMulticaster().multicastEvent(earlyEvent);
      }
    }
  }
}

从上面的代码中,我们发现,真正的消息发送,实际上是通过 ApplicationEventMulticaster 这个类来完成的。这个类的源码我只摘抄了最关键的一部分,也就是 multicastEvent() 这个消息发送函数。不过,它的代码也并不复杂,我就不多解释了。这里我稍微提示一下,它通过线程池,支持异步非阻塞、同步阻塞这两种类型的观察者模式。

public void multicastEvent(ApplicationEvent event) {
  this.multicastEvent(event, this.resolveDefaultEventType(event));
}
public void multicastEvent(final ApplicationEvent event, ResolvableType eventType) {
  ResolvableType type = eventType != null ? eventType : this.resolveDefaultEventType(event);
  Iterator var4 = this.getApplicationListeners(event, type).iterator();
  while(var4.hasNext()) {
    final ApplicationListener<?> listener = (ApplicationListener)var4.next();
    Executor executor = this.getTaskExecutor();
    if (executor != null) {
      executor.execute(new Runnable() {
        public void run() {
          SimpleApplicationEventMulticaster.this.invokeListener(listener, event);
        }
      });
    } else {
      this.invokeListener(listener, event);
    }
  }
}

借助 Spring 提供的观察者模式的骨架代码,如果我们要在 Spring 下实现某个事件的发送和监听,只需要做很少的工作,定义事件、定义监听器、往 ApplicationContext 中发送事件就可以了,剩下的工作都由 Spring 框架来完成。实际上,这也体现了 Spring 框架的扩展性,也就是在不需要修改任何代码的情况下,扩展新的事件和监听。

模板模式在 Spring 中的应用

刚刚讲的是观察者模式在 Spring 中的应用,现在我们再讲下模板模式。
我们来看下一下经常在面试中被问到的一个问题:请你说下 Spring Bean 的创建过程包含哪些主要的步骤。这其中就涉及模板模式。它也体现了 Spring 的扩展性。利用模板模式,Spring 能让用户定制 Bean 的创建过程。
Spring Bean 的创建过程,可以大致分为两大步:对象的创建和对象的初始化。

对象的创建是通过反射来动态生成对象,而不是 new 方法。不管是哪种方式,说白了,总归还是调用构造函数来生成对象,没有什么特殊的。对象的初始化有两种实现方式。一种是在类中自定义一个初始化函数,并且通过配置文件,显式地告知 Spring,哪个函数是初始化函数。我举了一个例子解释一下。如下所示,在配置文件中,我们通过 init-method 属性来指定初始化函数。

public class DemoClass {
  //...
  
  public void initDemo() {
    //...初始化..
  }
}
// 配置:需要通过init-method显式地指定初始化方法
<bean id="demoBean" class="com.xzg.cd.DemoClass" init-method="initDemo"></bean>

这种初始化方式有一个缺点,初始化函数并不固定,由用户随意定义,这就需要 Spring 通过反射,在运行时动态地调用这个初始化函数。而反射又会影响代码执行的性能,那有没有替代方案呢?

Spring 提供了另外一个定义初始化函数的方法,那就是让类实现 Initializingbean 接口。这个接口包含一个固定的初始化函数定义(afterPropertiesSet() 函数)。Spring 在初始化 Bean 的时候,可以直接通过 bean.afterPropertiesSet() 的方式,调用 Bean 对象上的这个函数,而不需要使用反射来调用了。我举个例子解释一下,代码如下所示。

public class DemoClass implements InitializingBean{
  @Override
  public void afterPropertiesSet() throws Exception {
    //...初始化...      
  }
}
// 配置:不需要显式地指定初始化方法
<bean id="demoBean" class="com.xzg.cd.DemoClass"></bean>

尽管这种实现方式不会用到反射,执行效率提高了,但业务代码(DemoClass)跟框架代码(InitializingBean)耦合在了一起。框架代码侵入到了业务代码中,替换框架的成本就变高了。所以,我并不是太推荐这种写法。
实际上,在 Spring 对 Bean 整个生命周期的管理中,还有一个跟初始化相对应的过程,那就是 Bean 的销毁过程。我们知道,在 Java 中,对象的回收是通过 JVM 来自动完成的。但是,我们可以在将 Bean 正式交给 JVM 垃圾回收前,执行一些销毁操作(比如关闭文件句柄等等)。

销毁过程跟初始化过程非常相似,也有两种实现方式。一种是通过配置 destroy-method 指定类中的销毁函数,另一种是让类实现 DisposableBean 接口。因为 destroy-method、DisposableBean 跟 init-method、InitializingBean 非常相似,所以,这部分我们就不详细讲解了,你可以自行研究下。

实际上,Spring 针对对象的初始化过程,还做了进一步的细化,将它拆分成了三个小步骤:初始化前置操作、初始化、初始化后置操作。其中,中间的初始化操作就是我们刚刚讲的那部分,初始化的前置和后置操作,定义在接口 BeanPostProcessor 中。BeanPostProcessor 的接口定义如下所示:

public interface BeanPostProcessor {
  Object postProcessBeforeInitialization(Object var1, String var2) throws BeansException;
  Object postProcessAfterInitialization(Object var1, String var2) throws BeansException;
}

我们再来看下,如何通过 BeanPostProcessor 来定义初始化前置和后置操作?

我们只需要定义一个实现了 BeanPostProcessor 接口的处理器类,并在配置文件中像配置普通 Bean 一样去配置就可以了。Spring 中的 ApplicationContext 会自动检测在配置文件中实现了 BeanPostProcessor 接口的所有 Bean,并把它们注册到 BeanPostProcessor 处理器列表中。在 Spring 容器创建 Bean 的过程中,Spring 会逐一去调用这些处理器。
通过上面的分析,我们基本上弄清楚了 Spring Bean 的整个生命周期(创建加销毁)。针对这个过程,我画了一张图,你可以结合着刚刚讲解一块看下。

在这里插入图片描述
不过,你可能会说,这里哪里用到了模板模式啊?模板模式不是需要定义一个包含模板方法的抽象模板类,以及定义子类实现模板方法吗?
实际上,这里的模板模式的实现,并不是标准的抽象类的实现方式,而是有点类似我们前面讲到的 Callback 回调的实现方式,也就是将要执行的函数封装成对象(比如,初始化方法封装成 InitializingBean 对象),传递给模板(BeanFactory)来执行。

重点回顾

好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。
今天我讲到了 Spring 中用到的两种支持扩展的设计模式,观察者模式和模板模式。

其中,观察者模式在 Java、Google Guava、Spring 中都有提供相应的实现代码。在平时的项目开发中,基于这些实现代码,我们可以轻松地实现一个观察者模式。

Java 提供的框架比较简单,只包含 java.util.Observable 和 java.util.Observer 两个类。Google Guava 提供的框架功能比较完善和强大,可以通过 EventBus 事件总线来实现观察者模式。Spring 提供了观察者模式包含 Event 事件、Listener 监听者、Publisher 发送者三部分。事件发送到 ApplicationContext 中,然后,ApplicationConext 将消息发送给事先注册好的监听者。

除此之外,我们还讲到模板模式在 Spring 中的一个典型应用,那就是 Bean 的创建过程。Bean 的创建包含两个大的步骤,对象的创建和对象的初始化。其中,对象的初始化又可以分解为 3 个小的步骤:初始化前置操作、初始化、初始化后置操作。

课堂讨论

在 Google Guava 的 EventBus 实现中,被观察者发送消息到事件总线,事件总线根据消息的类型,将消息发送给可匹配的观察者。那在 Spring 提供的观察者模式的实现中,是否也支持按照消息类型匹配观察者呢?如果能,它是如何实现的?如果不能,你有什么方法可以让它支持吗?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/681189.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MongoDB负载均衡集群(第8章节选)

MongoDB自身可组成分片加复制的集群&#xff0c;在这个集群的前端加上负载均衡器&#xff08;比如HAProxmy Keepalived&#xff09;&#xff0c;就可组建成一个无单点故障、十分完美的高可用负载均衡集群&#xff08;如图8-1所示&#xff09;。 图8- 1 整个MongDB高可用体系结…

基于java+swing+mysql飞机票预订系统

基于javaswingmysql飞机票预订系统 一、系统介绍二、功能展示1.项目内容2.项目骨架3.数据库表4.注册窗口5.登录窗口6、用户-主窗口7、用户-查询航班8.用户--订票8.用户--取票9.管理员-所有航班信息10.管理员-添加航班11.用户信息12.订票状态 四、其它1.其他系统实现五.获取源码…

路径规划-DWA算法(C++实现)

1、简单介绍 DWA算法&#xff08;dynamic window approach&#xff09;&#xff0c;其原理主要是在速度空间&#xff08;v,w&#xff09;中采样多组速度&#xff0c;并模拟出这些速度在一定时间内的运动轨迹&#xff0c;并通过评价函数对这些轨迹进行评价(其中包括距离障碍物距…

【学习笔记】 科目一之概念篇

【学习笔记】 科目一之概念篇 概念题方法 1&#xff09;抓重点&#xff1a;科目一设计知识范围太广&#xff0c;不要妄想所有知识点都复习到&#xff0c;这是不可能的&#xff0c;我们的目标是45分几个而不是考高分&#xff0c;复习时间有限&#xff0c;所以要学会抓重点&…

图片是如何生成的--图像生成模型(GAN、VAE、扩散模型)简介

目录 1.GAN 2.AutoEncoder及其变种&#xff1a;AE/DAE/VAE/VQVAE 2.1 AE&#xff1a; 2.2 DAE&#xff1a;Denoising AutoEncoder 2.3 VAE&#xff1a;Variational AutoEncoder 2.4 VQVAE&#xff1a;Vector-quantised Variational AutoEncoder 3. 扩散模型 3.1 扩散…

【openGauss简单数据管理】--快速入门

【openGauss简单数据管理】--快速入门 &#x1f53b; 一、openGauss数据库管理&#x1f530; 1.1 连接openGauss数据库&#x1f530; 1.2 创建数据库&#x1f530; 1.3 查看数据库和切换数据库&#x1f530; 1.4 修改数据库&#x1f530; 1.5 删除数据库&#x1f530; 1.6 启停…

【QQ界面展示-实现自动回复 Objective-C语言】

一、刚才咱们监听键盘弹出事件,是怎么监听的, 1.监听键盘弹出事件的步骤 1)首先,在控制器的viewDidLoad方法中,创建一个NotificationCenter对象啊 2)通过center,让当前控制器的这个方法,监听这个通知, 3)然后,我们在这个通知里面,获取到键盘的Y值, 4)对我们的…

Rust 原始类型之数组array内置方法

目录 数组 array 声明 访问 引用 Reference 切片 Slice 方法 题目实例 数组 array 在 Rust 中&#xff0c;数组是一种固定大小的数据结构&#xff0c;用于存储具有相同数据类型的元素的有序集合。 “固定大小”是指数组中的元素的类型和数量确定&#xff0c;也就确定了…

【从零开始学习JAVA | 第十八篇】接口介绍

目录 前言&#xff1a; 接口&#xff1a; 如何定义一个接口&#xff1a; 如何使用一个接口&#xff1a; 接口中成员的特点&#xff1a; 接口与类的区别&#xff1a; 接口的应用&#xff1a; 总结&#xff1a; 前言&#xff1a; 接口其实是为了弥补继承的缺点&#xf…

C语言文件打开关闭详解、文件顺序读写详解。

文件的打开和关闭 fopen函数原型&#xff1a; FILE *fopen( const char *filename, const char *mode );const char *filename 文件的路径以及名字const char *mode 文件的打开方式 文件打开方式含义如果文件不存在“r”读文件不存在会报错“w”写(清空写)建立一个新的文件“…

【新手上路】如何在Web3时代成为XR创建者

目录 0 XR在Web3里的作用 1 XR的概念、特征、技术、设备、平台、应用和工具 1.1 VR的概念、特征和技术 1.2 AR的概念、特征和技术 1.2 XR的设备、平台、应用和工具 2 选择XR的方法 2.1 何时使用VR 2.2 何时使用AR 3 开发XR作品的4个步骤 4 成为XR构建者的路径 4.1 三…

小程序布局中相对定位的用法

小程序中一般为了有一定的设计效果&#xff0c;会将下边组件的内容提升一点到上边去&#xff0c;比如我们的电商展示模板里&#xff0c;会将商品列表覆盖一点到背景图&#xff0c;效果如下&#xff1a; 这种要如何搭建呢&#xff1f;就是利用到了CSS相对定位的原理 搭建组件 …

27.移除元素

LeetCode-27.移除元素 1、题目描述2、解题思路3、代码实现3.1Java代码实现3.2双指针代码优化 4、解题记录 1、题目描述 题目描述&#xff1a; 给你一个数组 nums 和一个值 val&#xff0c;你需要 原地 移除所有数值等于 val 的元素&#xff0c;并返回移除后数组的新长度。 不要…

windows搭建vue开发环境

参考博客&#xff1a;最详细的vue安装教程_一只野生程序媛的博客-CSDN博客 Vue安装环境最全教程&#xff0c;傻瓜式安装_浪漫主义码农的博客-CSDN博客 1、安装nodejs&#xff0c;从下面官网下载版本&#xff0c;对应安装就行了&#xff1a; Node.js 中文网 2、安装好后&…

合宙Air724UG Cat.1模块硬件设计指南--天线接口

天线接口 简介 天线是发射和接收电磁波的一个重要的无线电设备&#xff0c;没有天线也就没有无线电通信。天线品种繁多&#xff0c;以供不同频率、不同用途、不同场合、不同要求等不同情况下使用。 特性 LTE天线接口。50 欧姆特性阻抗&#xff0c;不推荐使用PCB板载天线&#…

脚本模式的特点和用法

一、什么是脚本? 脚本(script)是使用一种特定的描述性语言&#xff0c;依据一定的格式编写的可执行文件&#xff0c;又称作宏或批处理文件。脚本通常可以由应用程序临时调用并执行。 简单解释:脚本类似于演戏时用到的剧本&#xff0c;脚本其实就是一系列指令——演员看了指令就…

算法--itemCF

概述&#xff1a; 电子商务网站是个性化 推荐系统重要地应用的领域之一。亚马逊就是个性化推荐系统的积极应用者和推广者&#xff0c;亚马逊的推荐系统深入到网站的各类商品&#xff0c;为亚马逊带来了至少30%的销售额。 不光是电商类&#xff0c;推荐系统无处不在。 QQ&…

【差旅-游记】记一次海南出差

哈喽&#xff0c;大家好&#xff01;我是雷工&#xff01; 这篇不是技术分享&#xff0c;是篇差旅记录。 最近出差去了一次海南&#xff0c;应该算得上我目前出差去过最远的地方了&#xff0c;也是我第一次去海南&#xff0c;还是蛮有新鲜感的&#xff0c;因此记录下此次差旅。…

【软件设计师暴击考点】数据库系统高频考点暴击系列

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;软件…

tensorflow2 模型建立与训练

模型的构建&#xff1a; tf.keras.Model 和 tf.keras.layers 模型的损失函数&#xff1a; tf.keras.losses 模型的优化器&#xff1a; tf.keras.optimizer 模型的评估&#xff1a; tf.keras.metrics 模型&#xff08;Model&#xff09;与层&#xff08;Layer&#xff09; …