《SpringBoot 3:入门与应用实战》第 4 章 IOC 容器中的 Bean 阅读笔记 6

《SpringBoot 3:入门与应用实战》第 4 章 IOC 容器中的 Bean 阅读笔记 6

Spring Framework 中的 IOC 容器内部设计极其精巧,提供了强大且实用的功能和机制,通过加载解析 XML 配置文件、注解配置类实现按照配置内容工作。

4.1 BeanFactory

第2章讲到,BeanFactory 是基础 IOC 容器,ApplicationContext 包含 BeanFactory 的所有功能,且扩展了更多实用特性。BeanFactory 作为Spring Framework 中的基础 IOC 容器,它的设计相对简单且纯粹,但也是最核心的。

4.1.1 BeanFactory 接口系列

作为 Spring Framework 中的顶级容器接口,BeanFactory 的设计是简单纯粹的。下面将对 BeanFactory 的核心特性进行分析。

1.核心特性

BeanFactory 接口直接扩展的 3 个子接口,可以总结出 BeanFactory 的特性:层次性、可列举、支持自动注入。

  • 层次性:BeanFactory本身支持父子结构,如果在一个BeanFactory中没有找到想要的Bean,则会向上查找父BeanFactory。
  • 可列举:ListableBeanFactory 支持将容器中所有的 Bean一一列举。
  • 支持自动注入:BeanFactory 中定义了 getBean 方法用于依赖查找,其子接口 AutowireCapableBeanFactory 中对依赖注入予以支持。

2.可读与可写

在 HierarchicalBeanFactory 的下方有一个接口:ConfigurableBeanFactory。Spring Framework 中对于核心 API 的命名有非常强的规律性,当我们看到类名的前面带有 "Configurable" 时,意味着这个接口的行为有"写"的动作,不带 Configurable 前缀的接口只有"读"的动作。

可写与可读的概念,在学习 Java SE 的面向对象编程中,一个类的属性设置为 private 后,提供 getter 方法则意味着该属性可读,提供 setter 方法则意味着该属性可写。

同样,Spring Framework 中的这些 BeanFactory,包括后面的 ApplicationContext 中都有这样的设计:普通的 BeanFactory 只有 get 相关的操作,而 "Configurable" 开头的 BeanFactory 或者 ApplicationContext 就具有了 set 相关的操作。

4.1.2 BeanFactory 的实现类

有关 BeanFactory 实现类的简单讲解,我们重点关注两个抽象子类和一个落地实现类。抽象实现 AbstractBeanFactory、一个底层实现DefaultListableBeanFactory 以及介于两者之间的 AbstractAutowireCapableBeanFactory。

1.核心特性

简单描述 BeanFactory 的实现类中设计的功能和机制。

  • 加载 Bean 的信息:BeanFactory 利用不同的加载机制(XML或注解配置类等)加载 Bean 的信息。
  • 作用域:BeanFactory 的内部能分辨 Bean 的作用域是单实例还是原型等。
  • 创建 Bean :BeanFactory 具备创建 Bean 的能力,以及属性赋值、依赖注入、生命周期回调。
  • 生命周期管理:BeanFactory 负责控制 Bean 在创建阶段的生命周期。

2.BeanFactory 的约定

在 BeanFactory 的设计之初,它制定了 Bean 创建的两大环节和几个核心步骤。

(1)加载 Bean 的注册信息:从 XML 配置文件、注解配置类、模式注解+组件扫描等方式得知哪些 Bean 需要被注入到 IOC 容器。

(2)创建并初始化 Bean:该环节包含 2 个步骤,分别是 Bean 的创建、Bean的初始化。

4.2 ApplicationContext

BeanFactory 仅实现了 IOC 容器中最核心的容器部分,而 ApplicationContext 提供了更多可以用于实际应用开发的强大功能特性。打一个比方,如果说 BeanFactory 是 IOC 容器的"芯片"​,那么 ApplicationContext 则是一台完整的主机。

4.2.1 ApplicationContext 接口系列

借助 IDEA,可以展示 ApplicationContext 接口的父接口、扩展接口之间的关系,如图所示。

从图中可以看到,ApplicationContext 除了继承 BeanFactory 接口,还额外继承了几个功能性接口,这些接口共同组成了 ApplicationContext 扩展的几个核心特性。简单概括 ApplicationContext 的功能如下。

  • 访问 Bean 的能力:ApplicationContext 继承自 BeanFactory,拥有 BeanFactory 的所有能力。
  • 层级关系:ApplicationContext与BeanFactory一样,也存在层级关系。
  • 加载文件资源:ApplicationContext 可以从类路径 (classpath)、文件系统等来源中加载文件。
  • 事件发布与驱动机制:ApplicationContext 设计了基于观察者模式的事件驱动机制,可以实现事件的发布与广播。
  • 国际化支持:ApplicationContext可以针对不同国家/地区的访问,提供符合用户阅读习惯(语言)的页面和数据。
  • 配置属性:ApplicationContext内部有一块专有区域,用于管理和保存程序运行时所使用的配置属性。

此外,ApplicationContext 还有一个子接口 ConfigurableApplicationContext,它的设计与 ConfigurableBeanFactory 之于 BeanFactory 完全一致。ConfigurableApplicationContext 接口中有两个新增的重要方法,分别是 refresh 和 close,代表初始化(刷新)和关闭 IOC 容器。

4.2.2 ApplicationContext 的实现类

我们先将 Spring Framework 中 ApplicationContext 的重要实现类都罗列出来,如图所示。

根据加载配置源的不同,可以将 ApplicationContext 分为两个大类:

基于 XML 配置文件驱动的实现(代表类 ClassPathXmlApplicationContext)和基于注解驱动的实现(代表类AnnotationConfigApplicationContext)​。

无论是哪种实现,它们都来自同一个父类 AbstractApplicationContext。AbstractApplicationContext 中定义和实现了绝大部分应用上下文的特性和功能,包含生命周期实现、容器初始化和销毁的逻辑、特殊类型 Bean 的处理等。

当下 Spring Framework 的版本已发展至 6.0,Spring Boot 早已成为企业项目开发的必备基础框架,而 Spring Boot 推荐使用注解驱动配置的方式,相应地放弃了基于 XML 配置文件的实现方式,所以本书主要侧重注解驱动的 IOC 容器讲解。此外,ApplicationContext 在 Web 应用下还有对应的扩展,这部分内容会在 WebMvc 开发中讲解。

4.3 事件驱动与监听器

下面我们学习一些 IOC 容器支持的功能特性,这些特性在平时的开发中不一定全部得到使用,但想熟练掌握 Spring Framework,了解并掌握这些知识点非常重要。

4.3.1 观察者模式

本节讲解事件驱动机制。对于事件与监听器,可能读者会联想到观察者模式。

观察者模式,也被称为发布订阅模式,也有人叫它 "监听器模式"​,它是 GoF 23 设计模式中行为型模式之一。观察者模式关注的点是某一个对象被修改 / 做出某些反应 / 发布一个信息等,会自动通知依赖它的对象(订阅者)​。观察者模式的三大核心是:观察者、被观察的主题、订阅者。观察者 (Observer) 需要绑定要通知的订阅者 (Subscriber),并且要观察指定的主题 (Subject)。

4.3.2 Spring 中的观察者模式

Spring Framework 中能够体现观察者模式的特性就是事件驱动和监听器。在观察者模式中,监听器充当订阅者,监听特定的事件;事件源充当被观察的主题,用来发布事件;IOC 容器本身也是事件广播器,可以理解为观察者。

Spring Framework 中事件驱动的核心可以划分为 4 部分:事件源、事件、广播器和监听器。

(1) 事件源:发布事件的对象。

(2) 事件:事件源发布的信息/做出的动作。

(3) 广播器:广播事件给监听器的对象 (ApplicationContext)。

  • ApplicationContext 接口中实现了 ApplicationEventPublisher 接口,具备事件广播器发布事件的能力。
  • ApplicationEventMulticaster 组合了所有的监听器,具备事件广播器广播事件的能力。

(4) 监听器:监听事件的对象。

4.3.3 事件与监听器实践

下面通过一个最简单的示例,体会 Spring Framework 中事件与监听器的使用。

1.编写监听器

Spring Framework 中内置的监听器接口是 ApplicationListener,它附带了一个泛型,可以指定监听器"感兴趣"的具体事件(只有泛型指定类型的事件才会被监听响应)​。如果我们要自定义监听器,则只需实现 ApplicationListener 接口。

为了快速体会事件和监听器的功能,本节先介绍两个事件:ContextRefreshedEvent 和 ContextClosedEvent,它们分别代表容器刷新(初始化)完毕和即将关闭。下面编写一个监听器 ContextRefreshedApplicationListener,并指定其监听 ContextRefreshedEvent 事件。注意,编写完毕的监听器需要注册到 IOC 容器才可以生效,在本示例中使用模式注解 + 组件扫描的方式加载监听器。

java 复制代码
package com.yangjunbo.bean.event.examplea;

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

@Component
public class ContextRefreshedApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
    
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        System.out.println("ContextRefreshedApplicationListener监听到ContextRefreshedEvent事件!");
    }
}

2.编写启动类

随后编写一个测试启动类 QuickstartListenerApplication,使用组件扫描的方式驱动 IOC 容器,并在 IOC 容器初始化完毕后关闭 IOC 容器。调用创建 IOC 容器和 close 方法这两个动作,可以在 IOC 容器的内部分别产生 ContextRefreshedEvent 和 ContextClosedEvent 事件,从而触发相应的监听器逻辑。运行 main 方法,控制台会根据流程依次打印如下信息,证明监听器已经成功执行。以上就是 Spring Framework 中事件与监听器的最简单使用。

java 复制代码
package com.yangjunbo.bean.event.examplea;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class QuickstartListenerApplication {
    
    public static void main(String[] args) throws Exception {
        System.out.println("准备初始化IOC容器。。。");
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
                "com.yangjunbo.bean.event.examplea");
        System.out.println("IOC容器初始化完成。。。");
        ctx.close();
        System.out.println("IOC容器关闭。。。");
    }
}

3.注解式监听器

编写监听器的方式,除了通过实现 ApplicationListener 接口,还可以使用 @EventListener 注解注册监听器。

使用注解式监听器时,定义监听器的类不再需要实现任何接口,而是直接在需要做出事件响应的方法上标注 @EventListener 注解即可。

java 复制代码
package com.yangjunbo.bean.event.examplea;

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

@Component
public class ContextClosedApplicationListener {
    
    @EventListener
    public void onContextClosedEvent(ContextClosedEvent event) {
        System.out.println("ContextClosedApplicationListener监听到ContextClosedEvent事件!");
    }
}

之后重新运行 QuickstartListenerApplication 的 main 方法,控制台可以打印出 ContextClosedApplicationListener 监听事件的响应,证明注解式监听器同样正确生效。

由以上两种监听器的代码示例,可以总结出以下几个结论:

  • ApplicationListener 会在容器初始化阶段就准备就绪,在容器销毁时一同销毁。
  • ApplicationListener 也是 IOC 容器中的普通Bean。
  • IOC容器中有一些内置事件供我们监听。

4.3.4 Spring 的内置事件

在 Spring Framework 中已经内置了一些事件。

1.ApplicationEvent

ApplicationEvent 是 Spring Framework 中事件模型的抽象,它本是一个抽象类,里面没有定义重要的内容,只有事件发生时的时间戳。

ApplicationEvent 继承自 JDK 中原生的观察者模式的事件模型。

ApplicationEvent 表示基础事件,它被设计为抽象的,因为直接发布基础事件没有意义。

2.ApplicationContextEvent

ApplicationContextEvent 继承自 ApplicationEvent,由 ApplicationContextEvent 的源码可知,它在构造时会将 IOC 容器 ApplicationContext 一并传入,这就意味着事件发生时,可以通过监听器直接取到 ApplicationContext。

3.ContextRefreshedEvent 和 ContextClosedEvent

ContextRefreshedEvent 与 ContextClosedEvent 是成对的事件定义,分别对应 IOC 容器刷新完毕但尚未启动完成,以及 IOC 容器已经关闭但尚未销毁所有 Bean。

对于上述两个事件发生时机,随着后续学习的推进,会对 IOC 容器的生命周期有更全面的认识。

4.ContextStartedEvent 和 ContextStoppedEvent

ContextStartedEvent 与 ContextStoppedEvent 事件的触发事件跟Bean 的生命周期扩展点,Lifecycle 接口相关。

在第 3 章的最后讲解了一个 Lifecycle 接口,当时讲了一个要点:Lifecycle 的触发时机,容器调用 start 和 stop 方法时触发。

事件名称 触发时机 关键说明
ContextRefreshedEvent 容器 refresh() 完成时 所有单例 Bean 已创建、属性注入、初始化回调(如 @PostConstruct)均已完成。
ContextStartedEvent 容器 start() 完成时 ContextRefreshedEvent 之后触发。此时所有实现了 Lifecycle 接口的 Bean 的 start() 方法已被回调。
ContextClosedEvent 容器 close() 执行时 IOC 容器已关闭,但此时单例 Bean 尚未被销毁。
ContextStoppedEvent 容器 stop() 完成时 ContextClosedEvent 之后触发。此时所有实现了 Lifecycle 接口的 Bean 的 stop() 方法已被回调,但单例 Bean 仍未被销毁。

4.3.5 自定义事件开发

下面考虑一个 Spring Framework 内置事件实际的业务场景:

论坛应用中,当新用户注册成功后,会同时发送短信、邮件、站内信,通知用户注册成功。

在这个场景中,如果后续再添加一个 "发放论坛积分" 逻辑,如何实现才能保证代码编写得更优雅、更易维护?

毫无疑问,选择使用合适的设计模式有助于提高代码的可维护性。

在这个场景中,当用户注册成功后广播一个 "用户注册成功" 事件,将用户信息随事件广播出去,发送短信、邮件、站内信的监听器监听到注册成功的事件后,会分别执行不同形式的通知动作;即便是后期再添加 "发放论坛积分" 逻辑,也只需要再注册一个对应的监听器到 IOC 容器,不会产生很强的代码耦合。

下面就来演示上述的场景的代码实现。

1.自定义用户注册成功事件

Spring Framework 中的自定义事件的方式是通过继承抽象类 ApplicationEvent 实现的,定义一个简单的用户注册成功事件RegisterSuccessEvent,所有自定义的事件类都需要声明一个带有一个参数的构造方法。

java 复制代码
package com.yangjunbo.bean.event.exampleb;

import org.springframework.context.ApplicationEvent;

/**
 * 注册成功的事件
 */
public class RegisterSuccessEvent extends ApplicationEvent {
    
    public RegisterSuccessEvent(Object source) {
        super(source);
    }
}

2.编写监听器

使用两种不同的方式编写三个监听器,分别代表短信推送、邮件推送、站内信推送。在一个类中可以同时出现多个被 @EventListener 注解标注的方法,即一个类中可以注册多个注解式事件监听器。

java 复制代码
package com.yangjunbo.bean.event.exampleb;

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

@Component
public class SenderListener {
    
    @EventListener
    public void sendMessage(RegisterSuccessEvent event) {
        System.out.println("监听到用户注册成功,发送站内信。。。");
    }
    
    @EventListener
    public void sendEmail(RegisterSuccessEvent event) {
        System.out.println("监听到用户注册成功,发送邮件中。。。");
    }
}
java 复制代码
package com.yangjunbo.bean.event.exampleb;

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

@Component
public class SmsSenderListener implements ApplicationListener<RegisterSuccessEvent> {
    
    @Override
    public void onApplicationEvent(RegisterSuccessEvent event) {
        System.out.println("监听到用户注册成功,发送短信。。。");
    }
}

3.编写注册逻辑业务层

只有事件和监听器还不够,还需要有一个事件源作为事件发布器,在应用上下文中发布事件。

我们可以编写一个 RegisterService 类,并注入 Spring Framework 内置的事件发布器 ApplicationEventPublisher,注入的方式可以选用注解@Autowired 自动注入,也可以借助 ApplicationEventPublisherAware 实现回调注入。

java 复制代码
package com.yangjunbo.bean.event.exampleb;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;

@Service
public class RegisterService implements ApplicationEventPublisherAware {
    
    ApplicationEventPublisher publisher;

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

    public void register(String username) {
        // 用户注册的动作。。。
        System.out.println(username + "注册成功。。。");
        // 发布事件
        publisher.publishEvent(new RegisterSuccessEvent(username));
    }
}

4.编写测试启动类

接下来编写测试启动类 RegisterEventApplication。IOC 容器初始化完毕后获取 RegisterService,随后调用 register 方法模拟一次用户注册的发生。

运行 main 方法,控制台打印出注册动作以及监听器的触发反应。

java 复制代码
package com.yangjunbo.bean.event.exampleb;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class RegisterEventApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
                "com.yangjunbo.bean.event.exampleb");
        RegisterService registerService = ctx.getBean(RegisterService.class);
        registerService.register("张三");
    }
}

5.调整监听器的触发顺序

如果业务需要调整为先发送站内信,后发送邮件,这就需要我们定制监听器的执行顺序。

Spring Framework 提供了两种排序的定制方式:

(1)实现 Ordered 接口,并重写 getOrder 方法传入排序值;

(2)在需要排序的类/方法上标注 @Order 注解,并传入排序值(默认的排序值为 Ordered.LOWEST_PRECEDENCE,即 Integer.MAX_VALUE,代表最靠后)​。

按照这个规则,我们可以在 SenderListener 的 sendMessage 方法上标注 @Order(0),重新运行启动类 RegisterEventApplication 的 main 方法,观察控制台的打印内容,可以发现触发顺序被成功修改。

将 sendMessage 方法上 @Order 注解的 value 指定为 Integer.MAX_VALUE - 1,随后再次重新运行 RegisterEventApplication 的 main 方法,发现运行结果发送站内信在发送邮件中之前,证明默认的排序值是 Integer.MAX_VALUE。

6.最佳实践

通过使用事件驱动机制,可以很好地解决单体应用中业务逻辑之间的解耦。

实际项目开发中,自定义事件的使用场景恰恰就如本节演示的这样,针对一个基础业务逻辑牵动多个关联的子业务。

可以在基础业务逻辑发生时,向 IOC 容器广播一个特定的事件,并将关联的核心业务数据放入事件中,随后负责监听该事件的监听器就可得以回调,从而从事件体中获取业务数据,进而执行相应的子业务逻辑。

相关推荐
不会代码的小猴4 小时前
7. JSON
开发语言·c++·笔记·qt·算法·json
用户3721574261354 小时前
如何使用 Java 在 Word 文档中添加和删除水印:分步指南
java
王中阳Go5 小时前
老板用AI三天写到90%,让我明天上线:Java 团队怎么接这10%的烂摊子?
java
gugucoding5 小时前
55. 【Java】Maven:项目构建的“管家”
java·maven
GGMM7895 小时前
富士 EP‑3955D 变频器主板维修调试笔记
笔记·变频器·变频器维修
今儿敲了吗6 小时前
CN——网络层(上)
网络·笔记
PC2005-cloud6 小时前
Go学习笔记:基本概念与项目结构——GOPATH、Go Modules 与常用命令
笔记·学习·golang
郑州光合科技余经理6 小时前
海外版多语言团购系统架构:主数据互通与核销边界
java·开发语言·前端·后端·系统架构·php·ai编程
_Narcissus_6 小时前
排序算法总结表格
c语言·数据结构·c++·笔记·算法·排序算法·总结