动态代理:发布订阅的高级玩法

动态代理在发布订阅中的设计

当我们谈发布订阅时,通常想到的是 Event、Listener、Bus。

但在 IntelliJ IDEA、Spring、Dubbo 等大型系统中,
发布订阅早已不再是"传一个事件对象"这么简单。

本文从一个问题出发:

为什么 IntelliJ IDEA 的 MessageBus 要用"动态代理"来做发布订阅?

并通过代码,对比传统事件模型代理式发布订阅模型,拆解它背后的设计价值。


一、传统发布订阅:我们最熟悉的样子

先看一个最常见的实现。

1. 传统事件模型

java 复制代码
class EventBus {
    private final Map<String, List<Consumer<Object>>> listeners = new HashMap<>();

    public void subscribe(String topic, Consumer<Object> listener) {
        listeners.computeIfAbsent(topic, k -> new ArrayList<>()).add(listener);
    }

    public void publish(String topic, Object event) {
        for (Consumer<Object> l : listeners.getOrDefault(topic, List.of())) {
            l.accept(event);
        }
    }
}

使用方式:

java 复制代码
bus.subscribe("order", e -> System.out.println(e));
bus.publish("order", new OrderCreatedEvent());

2. 这个模型的问题

这种方式在小系统里没问题,但在大型系统中会暴露大量缺陷:

  • 弱类型(Object / String)
  • ❌ 事件语义不清晰
  • ❌ 发布者必须"认识"事件结构
  • ❌ 难以演进(加一个 before/after?)
  • ❌ 不利于插件化系统

二、问题升级:事件其实是"协议"

在复杂系统中,一个"事件"往往不是一个动作,而是一组行为:

java 复制代码
beforeSave()
afterSave()
onError()

这时,事件更像一个接口协议,而不是一个数据对象


三、代理式发布订阅的核心思想

把"事件发布"伪装成"接口方法调用"

调用者以为自己在调方法:

java 复制代码
publisher.fileOpened(file);

但实际上:

复制代码
接口方法调用
  ↓
代理拦截
  ↓
消息总线分发
  ↓
多个订阅者执行

四、基于动态代理的发布订阅模型

1. 定义事件协议(接口)

java 复制代码
public interface FileListener {
    void fileOpened(String name);
    void fileClosed(String name);
}

这一步非常关键:

接口 = 事件协议
方法 = 事件语义


2. 定义 Topic

java 复制代码
class Topic<T> {
    private final Class<T> listenerType;
    public Topic(Class<T> listenerType) {
        this.listenerType = listenerType;
    }
    public Class<T> getListenerType() {
        return listenerType;
    }
}

3. MessageBus 的核心实现(简化版)

java 复制代码
class MessageBus {
    private final Map<Topic<?>, List<Object>> listeners = new HashMap<>();

    public <T> void subscribe(Topic<T> topic, T listener) {
        listeners.computeIfAbsent(topic, k -> new ArrayList<>()).add(listener);
    }

    @SuppressWarnings("unchecked")
    public <T> T publisher(Topic<T> topic) {
        return (T) Proxy.newProxyInstance(
            topic.getListenerType().getClassLoader(),
            new Class[]{topic.getListenerType()},
            (proxy, method, args) -> {
                for (Object l : listeners.getOrDefault(topic, List.of())) {
                    method.invoke(l, args);
                }
                return null;
            }
        );
    }
}

4. 使用方式(重点看体验)

java 复制代码
Topic<FileListener> FILE_TOPIC = new Topic<>(FileListener.class);

bus.subscribe(FILE_TOPIC, new FileListener() {
    public void fileOpened(String name) {
        System.out.println("open: " + name);
    }
    public void fileClosed(String name) {}
});

FileListener publisher = bus.publisher(FILE_TOPIC);
publisher.fileOpened("test.txt");

五、为什么一定要用"动态代理"?

1️⃣ 保持接口即协议

如果不用代理,你只能写成:

java 复制代码
bus.publish(topic, "fileOpened", args);

这会导致:

  • 方法名字符串
  • 无编译期检查
  • IDE 无法重构
  • 插件风险极高

代理让 接口本身成为协议


2️⃣ 发布者零感知订阅者

java 复制代码
publisher.fileOpened(...)

发布者:

  • 不知道有几个 listener
  • 不知道 listener 在哪
  • 不知道 listener 生命周期

真正的解耦


3️⃣ 天然支持"一对多"

代理拦截的是方法调用,而不是事件对象:

java 复制代码
method.invoke(listener1)
method.invoke(listener2)
method.invoke(listener3)

无需额外设计。


4️⃣ AOP 级别的扩展能力

在代理中,你可以轻松加入:

  • 线程切换(EDT / Executor)
  • disposed 检查
  • 异常隔离
  • 父子 MessageBus 传播
  • 性能统计

而业务代码完全无感知


六、与传统 EventBus 的核心差异对比

维度 传统 EventBus 代理式 MessageBus
事件定义 Object / Event 接口
类型安全
事件语义
API 体验 函数式 面向对象
插件友好
扩展能力 极强

七、这套设计适合什么场景?

非常适合:

  • IDE / 平台型系统
  • 插件化架构
  • 模块高度解耦的系统
  • 需要长期演进的 API

不太适合:

  • 极简单系统
  • 超高频 tight-loop 场景

八、一句话总结

动态代理在发布订阅中的真正价值,
不是"炫技",
而是让"事件"升级为"接口协议",
让系统在强类型、强约束的前提下,获得最大的架构自由度。


九、写在最后

IntelliJ IDEA MessageBus、Spring 的一些高级事件机制,本质上都在做同一件事:

用运行时代理,换取架构上的长期可维护性。

这是大型 Java 系统非常典型、也非常值得学习的一种设计取舍。

相关推荐
程序员-周李斌2 小时前
Java 死锁
java·开发语言·后端
皮皮林5513 小时前
Prometheus+Grafana,打造强大的监控与可视化平台
java
JasmineWr3 小时前
CompletableFuture相关问题
java·开发语言
零雲3 小时前
java面试:知道java的反射机制吗
java·开发语言·面试
java1234_小锋4 小时前
Java进程占用的内存有哪些部分?
java
sxlishaobin4 小时前
Spring Bean生命周期详解
java·后端·spring
曹牧4 小时前
Java:Assert.isTrue()
java·前端·数据库
梦里小白龙4 小时前
JAVA 策略模式+工厂模式
java·开发语言·策略模式
你不是我我5 小时前
【Java 开发日记】我们来说一说 Redis 主从复制的原理及作用
java·redis·github