后端视角看 EventBus:发布订阅总线的原理、场景与用法

大家好,我是程序员天天困。今天聊一个能让你少写一大堆回调的东西------EventBus,一个在 Android 和 Java 里都挺火的事件发布订阅库。我自己做后端多一些,但这东西在 Android 用得最广,所以咱们两边都照顾着讲,争取你写哪端都能用得上。话不多说,直接开始。

一、EventBus 到底是干什么的

EventBus 不是什么新发明,它就是把「发布-订阅」模式做成一个开箱即用的总线,让两个模块不用互相认识就能通信。

EventBus(发布订阅事件总线):greenrobot 出品的开源库,用发布-订阅模式解耦组件通信------发布者只管把事件丢进总线,订阅者通过注解方法接收,两者互不持有引用。你可以把它理解成「代码里的广播站」。

广播站的比喻最贴切:拿话筒的人喊一嗓子,所有调到这个频道的接收器都能听到,喊的人根本不用知道谁在听。回到后端,你写 Spring 时用过的 @EventListenerApplicationEventPublisher,或者 Guava 的 EventBus,乃至 Kafka、RabbitMQ,都是同一套发布订阅思想的不同重量级实现。EventBus 是其中最轻的一个:一个约 60k 的 jar,不依赖任何中间件,在进程内同步或异步分发,延迟在微秒级。

它的运行机制简单到一句话:发布者调 post 把事件丢进 EventBus,EventBus 根据事件类型查到所有订阅了这个类型的方法,依次调用。就这么直接,发布者不关心谁来收,订阅者也不关心谁发的。

二、为什么需要它:组件通信的痛

那有人就会问了,Android 自己不是有通信机制吗,为啥还要再造一个 EventBus?这得先看看原生的那套东西痛在哪。

Android 想让组件之间传消息,最常用的就两样:Handler 和 BroadcastReceiver。Handler 用来切线程、投递消息,BroadcastReceiver 用来发系统级广播。能用是能用,但写起来烦------一堆样板代码不说,发消息的还得知道收消息的是谁,两边经常要互相持有引用,组件一多,引用关系就缠成一团。

最常见的场景:你在子线程里跑接口请求,数据回来要刷新 UI,得拿 Handler 切回主线程;两个 Fragment 要互换数据,得借 Activity 当中间人,或者写个 Listener 接口回调。项目小的时候忍忍就过去了,业务一膨胀,这种回调套回调、引用套引用的写法,代码量和耦合度一起失控。后端其实一个毛病------Service 之间互相 @Autowired,最后织成一张谁也拆不动的网,根子都是发布者把「我要通知谁」写死在了自己代码里。

EventBus 的解法很简单:A 不认识 B/C/D,它只认识 EventBus。谁关心谁自己订阅,发布者彻底从「通知谁」里解放出来,新增一个订阅者,发布者一行代码都不用改。

下面这张图把两种方式摆在一起------左边是组件之间互相持有引用、回调层层传递;右边是发布者只对总线 post,订阅者各自从总线取,谁也不认识谁。

可能有人会问:那我后端直接上 Kafka、RabbitMQ 不就完了,为啥还要一个进程内的 EventBus?

两者解决的不是同一个量级的问题。MQ 是跨进程、跨机器的异步消息,带持久化和集群;EventBus 是单进程内的方法调用级通信,没有网络、没有序列化开销,延迟在微秒级。同一个 JVM 内部模块解耦,上 MQ 是杀鸡用牛刀;跨服务通信,EventBus 又鞭长莫及。各管一段,别混用。

三、三步上手:在 Spring Boot 里用 EventBus

EventBus 的全部 API 浓缩成三步------定义事件、注册订阅、发送事件,背下来就能用。它不绑 Android,Spring Boot 后端一样能跑,把依赖换成 eventbus-java 就行。如果你团队已经定了用 EventBus,或者要和 Android 端共用一套事件模型,下面这套 Spring Boot 写法直接抄;至于要不要在 Spring 里上它,选型建议放在第六节。

第一步:引入 Maven 依赖。pom.xml 里加这一段:

xml 复制代码
<!-- 以 Maven Central 3.3.1 为准;Spring Boot 2.x 注解用 javax,3.x 用 jakarta -->
<dependency>
    <groupId>org.greenrobot</groupId>
    <artifactId>eventbus-java</artifactId>
    <version>3.3.1</version>
</dependency>

第二步:定义事件 + 写订阅者。 事件就是一个普通 POJO,不继承任何基类;订阅者用 @Subscribe 标注接收方法,方法必须 public、有且仅有一个参数:

java 复制代码
public class LoginEvent {
    private final String username;
    public LoginEvent(String username) { this.username = username; }
    public String getUsername() { return username; }
}
java 复制代码
@Component
public class UserModule {
    @Subscribe(threadMode = ThreadMode.POSTING)
    public void onLogin(LoginEvent event) {
        System.out.println("用户 " + event.getUsername() + " 已登录");
    }

    @PostConstruct           // Bean 启动时注册
    public void init() { EventBus.getDefault().register(this); }

    @PreDestroy              // Bean 销毁时注销
    public void destroy() { EventBus.getDefault().unregister(this); }
}

把订阅者声明成 Spring 的 @Component,再用 @PostConstruct 注册、@PreDestroy 注销,register 和 unregister 就跟着 Bean 生命周期自动成对,不用你手动记着去调。

第三步:发送事件。 在任何 Service 里直接 post,发布者完全不需要知道谁在监听:

java 复制代码
@Service
public class LoginService {
    public void login(String username) {
        // 业务处理......
        EventBus.getDefault().post(new LoginEvent(username));
    }
}

我见过太多线上 OOM,根因就是 register 了却没 unregister------EventBus 持有订阅者引用,对象回收不掉。所以哪怕图省事,也务必让注册和注销跟着生命周期走,别只 register 不注销。

下面这张时序图展示了从 post 到被订阅方法处理的完整链路:发布者 post 后,EventBus 按事件类型查订阅表,再按线程模式决定同步直调还是丢进线程池。

四、@Subscribe 三个关键参数:线程模式、粘性事件、优先级

@Subscribe 注解的三个参数不是花架子,它们决定了事件在哪个线程跑、能不能收到「早到」的事件、谁先收到。

@Subscribe:EventBus 的订阅方法注解,标注「这个方法要接收什么类型的事件」,可指定线程模式、是否粘性、优先级。被注解的方法要求 public 修饰、有且仅有一个参数。

粘性事件(Sticky Event)postSticky 发出后会被 EventBus 缓存下来的事件,之后新注册的订阅者也能收到它。你可以理解成「迟到的订阅者也能拿到上一条广播」。

线程模式(ThreadMode)决定了订阅方法在哪个线程被执行。纯 Java 后端主要用 POSTING 和 ASYNC;MAIN 系列依赖 Android 主线程,后端用不到:

ThreadMode 在哪执行 纯 Java 能用 典型场景
POSTING post 所在线程直接同步调用 不耗时的事件,最常用
ASYNC 永远丢进线程池异步执行 订阅者要做耗时 IO
BACKGROUND post 在主线程则入池,在子线程则直接调 轻量后台任务
MAIN Android 主线程执行 更新 UI
MAIN_ORDERED 主线程,按顺序非阻塞 多个 UI 更新串行

粘性事件有个坑要单独说:缓存的粘性事件不会自动消失,重复 postSticky 同类型会覆盖旧的,但如果你忘了 removeStickyEvent,换场景后新订阅者可能收到一条本不该收的「历史事件」。我的习惯是粘性事件用完立刻 removeStickyEvent,别让缓存兜着脏数据。

优先级 priority 是个 int,数字越大越先收到,且只在同一线程模式下才有意义。

五、什么场景下用 EventBus

EventBus 适合「一对多、发布者不想知道订阅者是谁」的场景;不适合「调用方需要返回值」或「强顺序依赖」的场景。后者本质上是在做方法调用,硬套事件总线只会让链路更难追踪。

几类典型场景:

  1. 全局状态广播:登录登出、网络断连恢复------一处 post,多处 UI 或模块各自响应。
  2. 跨层组件通信:Fragment 之间、Fragment 与 Activity、Service 与页面,不再借 Activity 中转。
  3. 后端模块解耦:领域事件的轻量版------下单成功后通知积分、库存、推送模块各自处理,下单服务不关心下游有谁。
  4. 配置热更新 :配置中心拉到新值后 post 一个 ConfigChangedEvent,各模块自己刷新。

可能有人会问:后端真要用 EventBus 吗,还是直接 Spring @EventListener 更香?

老实说,纯 Spring 后端我更推荐用 Spring 自带的 @EventListener:零额外依赖、和容器生命周期天然集成、还支持 @TransactionalEventListener 跟着事务走。EventBus 的甜区在 Android 和非 Spring 的纯 Java 工程------没有 Spring 容器、又想要个轻量发布订阅,它比手撸观察者模式省心。要是跨进程,老老实实上 MQ。

六、后端视角的取舍:EventBus vs Guava vs Spring

选哪个,看你在不在 Spring 容器里、要不要跨进程、对依赖体积多敏感。

方案 依赖 线程模型 跨进程 适合谁
greenrobot EventBus ~60k jar ThreadMode 五种 Android / 非 Spring 纯 Java(Spring 里也能跑)
Guava EventBus Guava 整包 同步或异步二选一 已用 Guava 的纯 Java 项目
Spring @EventListener Spring 容器 同步,可配异步 任何 Spring 后端
Kafka / RabbitMQ 独立中间件 异步 跨服务、跨进程

EventBus 也不是银弹,几个老问题一直都在:事件类会越定义越多,一个项目几十个 Event 类很常见;订阅方法靠反射查找(编译时索引能优化,但要配 annotation processor);忘了 unregister 就内存泄漏;最麻烦的是事件流难追踪------post 一条事件,到底谁会响应、什么顺序,得全局搜,调试链路偏长。这些都是它换来解耦的代价。

写法上以 Maven Central 的 org.greenrobot:eventbus:3.3.1(2021 年最后大版本)为准,长期稳定。Android 新项目也有人用 Kotlin 的 Flow / Channel 替代,但就「最快上手、几行解耦」而言,EventBus 依然是门槛最低的方案之一。

后端要不要上 EventBus,我的判断就一句:在 Spring 里别折腾,@EventListener 已经够用;写 Android 或纯 Java 又想轻量解耦,EventBus 依然是几行代码搞定、最省心的那个。跨进程的事,交给 MQ,别让一条进程内总线去干它不该干的活。

相关推荐
码栈研说1 小时前
Go 语言大白话入门 7 - 函数:把重复代码收起来
后端·程序员
要开心吖ZSH1 小时前
AI医疗分诊与健康咨询助手agent开发——(4-2)从零到一:我给AI医疗分诊助手加了个“知识库大脑“,终于搞懂了RAG是什么
java·docker·agent·医疗·rag
ServBay1 小时前
WordPress 核心漏洞 wp2shell,波及超 5 亿网站
后端·php
分布式存储与RustFS2 小时前
RustFS Beta.10 性能解读:PUT 全面反超 MinIO,Rust 重写对象存储成了?
开发语言·后端·rust
Reart2 小时前
Leetcode 309.买卖股票的最佳时期含冷冻期(你也想成为股票高手吗o〃ω〃o,718)
后端
Reart3 小时前
Leetcode 188.买卖股票的最佳时机4(718)
后端·算法
怎么比啊都是大神3 小时前
关于==null、isEmpty()和StrUtil.isBlank()
java
唐青枫3 小时前
Java Neo4j 实战指南:从图模型、Cypher 到 Spring Boot 关系查询
java
Reart3 小时前
Leetcode 123.买卖股票的最佳时期3(内有随心谈,718)
后端·算法