MiniSpring框架学习-增加事件发布的简化 IoC 容器

MiniSpring框架学习-增加事件发布的简化 IoC 容器

教程: https://github.com/YaleGuo/minis
极客时间: 手把手带你写一个 MiniSpring

05. 增加事件发布的简化 IoC 容器

先说一点学习方法

学 MiniSpring 这种源码类知识,不太建议一上来就像以前跟着视频课那样,完全照着老师手搓代码。

真正手搓过一遍的同学大概率会有体会:代码是跟着敲完了,但很多时候敲完也记不住,甚至还会出现一个更尴尬的问题:代码跑起来了,但自己还是不知道到底在学什么。

我觉得更好的方式是:先把教程里的示例代码下载下来,或者用 AI 生成一份类似的范例代码,然后从入口文件开始 debug。对着教程看,顺着调用链一点点往下走。看到哪里不懂,就 debug 到哪里,多问自己几句:这行代码为什么要这么写?这个对象什么时候创建的?这个方法调用完以后,容器里多了什么东西?

然后一定要自己做笔记,把逻辑重新梳理一遍。比如入口文件里的每一行代码分别干了什么,refresh() 里面每一步在准备什么,publishEvent(...) 最后到底调用了哪些监听器。能把这些流程用自己的话写出来,比单纯把代码敲一遍更有用。

另外。IDEA 一定下个最新版,然后买 Codex 生成一些定制化的范例源码,比如"只演示事件发布""只演示监听器注册""只演示 refresh 流程"。这样你就可以拿着更小、更干净的例子反复 debug,看清楚某一个功能到底是怎么跑起来的。

学 MiniSpring 最重要的,不是把每一行源码背下来,而是学习里面的设计思路和功能实现思路。先理解,再总结成文档,不要一开始就对自己期待过高,觉得学完这一套马上就脱胎换骨。

更真实的成长路径是:你先有这些概念,后面做自己的项目、造自己的轮子时,脑子里会慢慢冒出这些设计影子。那个时候再回头看 Spring 怎么设计,思考哪些地方能借到自己的项目里,才是真的融汇贯通。

这一节继续往 MiniSpring 里加一个很常见的能力:事件发布和事件监听。

说得口语一点,就是容器里发生了一件事,比如"容器刷新完成了",不要把后续逻辑都硬写在容器启动代码里,而是把这件事包装成一个事件,再通知对它感兴趣的监听器。

这一节要重点明白四件事:

  1. JDK 事件模型里的 EventObjectEventListener 分别负责什么。
  2. MiniSpring 里怎么定义 ApplicationEventApplicationListenerApplicationEventPublisher
  3. refresh() 过程中为什么要先初始化事件发布器,再注册监听器,最后发布刷新完成事件。
  4. 这里的事件机制默认是同步回调,不是消息队列,也不等于天然异步。这里千万注意,我学之前就是这么理解的

本节主线可以先记成这样:

text 复制代码
容器 refresh
    -> 初始化事件发布器
    -> 注册事件监听器
    -> 容器刷新完成
    -> 发布 ContextRefreshEvent
    -> 遍历监听器
    -> 执行 listener.onApplicationEvent(event)

先把事件机制说清楚

Java 本身就提供了事件模型的基础类和接口:

JDK 类型 可以怎么理解
java.util.EventObject 事件对象,里面保存了事件来源 source
java.util.EventListener 监听器标记接口,表示某个接口属于事件监听器体系

注意,JDK 只是提供了基础类型,不会自动帮我们完成事件分发。

真正的分发逻辑还得由我们自己写,也就是:

text 复制代码
发生一件事
    -> 封装成事件对象
    -> 找到监听器
    -> 调用监听器方法

放到 MiniSpring 里,就是:

text 复制代码
ApplicationEvent          表示事件
ApplicationListener       表示监听器
ApplicationEventPublisher 表示事件发布器

这套东西本质上就是观察者模式:一个对象发生变化后,通知依赖它的其他对象,但不要让事件发布方直接依赖所有后续处理逻辑。

注意:事件机制不等于消息队列。

这份 MiniSpring 里的事件发布是同步方法调用。publishEvent(...) 执行时,会在当前线程里直接遍历监听器,并调用它们的 onApplicationEvent(...) 方法。

定义基础事件对象

事件对象继承 EventObjectsource 表示事件来源,比如可以是当前 ApplicationContext,也可以是某段业务数据。

java 复制代码
import java.util.EventObject;

/**
 * MiniSpring 里的基础事件对象。
 * source 表示事件来源,message 只是为了让 Demo 输出更直观。
 */
public class ApplicationEvent extends EventObject {
    private static final long serialVersionUID = 1L;

    private final String message;

    public ApplicationEvent(Object source) {
        super(source);
        this.message = String.valueOf(source);
    }

    public String getMessage() {
        return this.message;
    }

    @Override
    public String toString() {
        return this.message;
    }
}

这里有两个细节:

  1. EventObject 构造方法要求 source 不能为 null
  2. source 不一定就是字符串,本 Demo 只是额外保存了 message,方便打印日志。

定义一个容器刷新事件

容器刷新完成后,可以发布一个默认事件。

真实 Spring 里的类名是 ContextRefreshedEvent。如果当前 MiniSpring 项目里已经用了 ContextRefreshEvent,为了和前面代码保持一致,可以继续使用这个名字。

java 复制代码
/**
 * 容器刷新完成后发布的事件。
 */
public class ContextRefreshEvent extends ApplicationEvent {
    private static final long serialVersionUID = 1L;

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

这里不需要再重写 toString(),因为父类 ApplicationEvent 已经提供了默认输出。

定义监听器接口

监听器继承 EventListener,表示它属于事件监听器体系。

java 复制代码
import java.util.EventListener;

/**
 * 所有应用事件监听器都实现这个接口。
 */
public interface ApplicationListener extends EventListener {

    /**
     * 收到事件后,监听器在这里执行自己的处理逻辑。
     */
    void onApplicationEvent(ApplicationEvent event);
}

真实 Spring 的监听器会带泛型,比如监听某一种具体事件。

MiniSpring 当前先不加泛型,所有监听器都接收 ApplicationEvent,然后在方法内部用 instanceof 判断自己关心的事件类型。

定义事件发布接口

事件发布接口负责两件事:

  1. 发布事件。
  2. 添加监听器。
java 复制代码
public interface ApplicationEventPublisher {

    void publishEvent(ApplicationEvent event);

    void addApplicationListener(ApplicationListener listener);
}

这里再次提醒一下:真实 Spring 的 ApplicationEventPublisher 主要只有发布能力,监听器注册会交给上下文和事件多播器管理。Demo 这样写,是为了少引入几个中间接口。

实现一个简单事件发布器

最简版的实现思路很直接:维护一个监听器列表,发布事件时遍历列表。

java 复制代码
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

public class SimpleApplicationEventPublisher implements ApplicationEventPublisher {
    private final List<ApplicationListener> listeners = new ArrayList<>();

    @Override
    public void publishEvent(ApplicationEvent event) {
        Objects.requireNonNull(event, "event must not be null");

        // 简化版没有异步队列,发布事件时直接同步回调所有监听器。
        for (ApplicationListener listener : this.listeners) {
            listener.onApplicationEvent(event);
        }
    }

    @Override
    public void addApplicationListener(ApplicationListener listener) {
        this.listeners.add(Objects.requireNonNull(listener, "listener must not be null"));
    }
}

这段代码能跑通事件机制,但也要知道它的边界:

  1. 它是同步的,监听器执行慢,发布事件的一方也会被拖慢。
  2. 监听器如果抛异常,后面的监听器可能就不会继续执行。
  3. 这里没有按事件类型过滤,所有监听器都会收到所有事件。
  4. 这里没有线程安全增强,如果运行时频繁增删监听器,需要换成更稳的集合或加锁。

让 ApplicationContext 拥有发布事件的能力

ApplicationContext 是容器对外的统一入口。既然事件发布属于容器能力,就可以让它继承 ApplicationEventPublisher

java 复制代码
public interface ApplicationContext extends EnvironmentCapable,
        ListableBeanFactory,
        ConfigurableBeanFactory,
        ApplicationEventPublisher {
}

这样一来,外部使用者拿到 ClassPathXmlApplicationContext 后,就可以直接调用:

java 复制代码
context.publishEvent(new ApplicationEvent("手动发布事件"));

也可以手动注册监听器:

java 复制代码
context.addApplicationListener(listener);

在 AbstractApplicationContext 里接入事件流程

事件发布器应该在 refresh() 的模板流程里初始化。

整体顺序可以理解成:

text 复制代码
refresh()
    -> 准备 BeanFactory
    -> 初始化事件发布器
    -> 注册监听器
    -> 初始化剩余单例 Bean
    -> 发布容器刷新完成事件

核心代码可以整理成这样:

java 复制代码
public abstract class AbstractApplicationContext implements ApplicationContext {
    private ApplicationEventPublisher applicationEventPublisher;

    public void refresh() throws BeansException {
        // 前面已有流程:创建 BeanFactory、读取 BeanDefinition、注册 BeanPostProcessor 等。

        initApplicationEventPublisher();
        registerListeners();

        // 前面已有流程:完成单例 Bean 的预实例化等。

        finishRefresh();
    }

    protected void initApplicationEventPublisher() {
        this.applicationEventPublisher = new SimpleApplicationEventPublisher();
    }

    protected void registerListeners() throws BeansException {
        // 如果项目里的 ListableBeanFactory 方法名不同,这里替换成当前项目已有的"列举 beanName"方法。
        for (String beanName : getBeanDefinitionNames()) {
            Object bean = getBean(beanName);

            if (bean instanceof ApplicationListener) {
                addApplicationListener((ApplicationListener) bean);
            }
        }
    }

    protected void finishRefresh() {
        publishEvent(new ContextRefreshEvent(this));
    }

    @Override
    public void publishEvent(ApplicationEvent event) {
        this.applicationEventPublisher.publishEvent(event);
    }

    @Override
    public void addApplicationListener(ApplicationListener listener) {
        this.applicationEventPublisher.addApplicationListener(listener);
    }
}

这里有一个很重要的顺序问题:

text 复制代码
先 initApplicationEventPublisher()
再 registerListeners()
最后 finishRefresh() 发布 ContextRefreshEvent

如果顺序反了,比如先发布事件,再注册监听器,那监听器就收不到容器刷新完成事件。

测试入口怎么写

java 复制代码
public class Test1 {

    public static void main(String[] args) throws BeansException {
        // 创建 ApplicationContext 时,会读取 beans.xml,注册 BeanDefinition,并触发 refresh。
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");

        // 验证前一节的 @Autowired 仍然可用。
        BaseService baseService = (BaseService) context.getBean("baseservice");
        baseService.sayHello();

        // 这里是容器创建完成后手动添加的监听器,只能收到后续手动发布的事件。
        context.addApplicationListener(new ApplicationListener() {
            @Override
            public void onApplicationEvent(ApplicationEvent event) {
                System.out.println("收到事件: " + event);
            }
        });

        // 简化版发布事件:当前线程同步遍历监听器,并回调 onApplicationEvent。
        context.publishEvent(new ApplicationEvent("手动发布事件"));
    }
}

输出大概会看到:

text 复制代码
Base Service says Hello
true
收到事件: 手动发布事件

事件机制到底解耦了什么

原来没有事件机制时,业务代码可能会这样写:

java 复制代码
public void register(String username) {
    saveUser(username);
    sendCoupon(username);
    sendMessage(username);
    writeLog(username);
}

这段代码的问题不是不能跑,而是 register(...) 这个方法知道太多后续动作了。

以后如果新增"发送站内信""推送风控消息""记录积分",就要继续改这个方法。

引入事件之后,可以改成:

java 复制代码
public void register(String username) {
    saveUser(username);

    publishEvent(new UserRegisterEvent(this, username));
}

后续动作放到不同监听器里:

text 复制代码
CouponListener      监听用户注册事件,负责发优惠券
SmsListener         监听用户注册事件,负责发短信
UserLogListener     监听用户注册事件,负责写日志

这样 register(...) 只需要表达一件事:

text 复制代码
用户注册成功了。

至于谁关心这件事,关心之后做什么,交给监听器扩展。

这就是事件机制带来的主要好处:发布方不再直接依赖一堆具体后续操作。

但它不是完全没有耦合

这点也要讲清楚。

MiniSpring 这种本地事件机制,虽然解掉了"发布方直接调用具体业务类"的耦合,但它还在同一个 JVM、同一个进程里执行。

所以它仍然有几个限制:

  1. 监听器执行失败,可能影响发布事件的主流程。
  2. 监听器执行太慢,可能拖慢主流程。
  3. 应用重启后,事件不会像消息队列那样持久化。
  4. 它不能直接跨进程、跨服务投递事件。

所以它适合做应用内部扩展点,不适合直接当成 MQ 使用。

如果需要跨服务、持久化、削峰填谷、失败重试,那就应该考虑消息队列,而不是只靠 Spring 事件。

观察者模式在这里怎么体现

观察者模式要解决的问题可以这么说:

text 复制代码
一个对象发生变化后,自动通知其他依赖它的对象,
但这个对象不要直接写死依赖哪些具体对象。

对应到本节代码里:

观察者模式角色 MiniSpring 对应
被观察对象发生变化 容器刷新完成、用户注册成功等事件发生
事件对象 ApplicationEvent
观察者 ApplicationListener
通知动作 publishEvent(...)

所以 JDK 的 EventObjectEventListener 加上我们自己写的发布器,就组成了一个很小的观察者模式 Demo。

本节几个关键修正

第一,EventObject 只是事件对象基类,不负责发布事件。

发布事件的动作是 ApplicationEventPublisher 做的。

第二,EventListener 是标记接口,不会自动回调任何方法。

真正被回调的是我们自己定义的 ApplicationListener#onApplicationEvent(...)

第三,手动添加的监听器注册时间比较晚。

如果监听器是在 new ClassPathXmlApplicationContext(...) 之后才添加的,它收不到 refresh() 过程中已经发布的事件。

第四,本节 Demo 是同步事件。

publishEvent(...) 不是把事件扔进队列,而是当前线程直接调用监听器方法。

第五,事件机制的价值是扩展点。

它让"发生了什么"和"发生后要做什么"分开。后续新增监听器时,通常不需要改发布事件的代码。

最后总结

这一节可以用两句话收住:

text 复制代码
事件机制的主线是:把发生的事情封装成 ApplicationEvent,再交给 ApplicationEventPublisher 通知 ApplicationListener。
text 复制代码
MiniSpring 里的事件发布默认是同步观察者模式,它能降低直接依赖,但不能当成消息队列来理解。

学完这一节,ApplicationContext 就不只是一个拿 Bean 的入口了。

它开始有了更像 Spring 的味道:容器生命周期里发生重要动作时,可以把这个动作发布出去,让外部扩展逻辑自然接进来。

相关推荐
云烟成雨TD10 小时前
Spring AI Alibaba 1.x 系列【54】Interrupts 中断机制:析动态中断源码分析
java·人工智能·spring
布吉岛的石头10 小时前
Java 程序员第 29 阶段-01:大模型微调入门:小样本业务适配方案
java·开发语言·人工智能
小白|10 小时前
cann-learning-hub:昇腾CANN社区学习中心完全指南
java·c++·算法
高林雨露10 小时前
Java 转 Kotlin 对照开发指南
java·开发语言·kotlin
java1234_小锋10 小时前
Spring AI 2.0 开发Java Agent智能体 - 多模态支持
java·人工智能·spring
前端若水10 小时前
使用 IndexedDB 在客户端存储对话记录
java·前端·人工智能·python·机器学习
Flittly10 小时前
【日常小问】Spring Cloud Gateway 5.x 跨域和路由配置踩坑实录
java·spring boot·spring cloud
阳光九叶草LXGZXJ10 小时前
达梦数据库-学习-57-读写数据页超时告警排查(page[x,x,xxxxxx] disk write uses)-DSC集群版
linux·运维·服务器·数据库·sql·学习