大家好,我是程序员天天困。今天聊一个能让你少写一大堆回调的东西——EventBus,一个在 Android 和 Java 里都挺火的事件发布订阅库。我自己做后端多一些,但这东西在 Android 用得最广,所以咱们两边都照顾着讲,争取你写哪端都能用得上。话不多说,直接开始。
一、EventBus 到底是干什么的
EventBus 不是什么新发明,它就是把「发布-订阅」模式做成一个开箱即用的总线,让两个模块不用互相认识就能通信。
EventBus(发布订阅事件总线):greenrobot 出品的开源库,用发布-订阅模式解耦组件通信——发布者只管把事件丢进总线,订阅者通过注解方法接收,两者互不持有引用。你可以把它理解成「代码里的广播站」。

广播站的比喻最贴切:拿话筒的人喊一嗓子,所有调到这个频道的接收器都能听到,喊的人根本不用知道谁在听。回到后端,你写 Spring 时用过的 @EventListener 配 ApplicationEventPublisher,或者 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 里加这一段:
<!-- 以 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、有且仅有一个参数:
public class LoginEvent {
private final String username;
public LoginEvent(String username) {
this.username = username; }
public String getUsername() {
return username; }
}
@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,发布者完全不需要知道谁在监听:
@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 适合「一对多、发布者不想知道订阅者是谁」的场景;不适合「调用方需要返回值」或「强顺序依赖」的场景。后者本质上是在做方法调用,硬套事件总线只会让链路更难追踪。
几类典型场景:
- 全局状态广播:登录登出、网络断连恢复——一处 post,多处 UI 或模块各自响应。
- 跨层组件通信:Fragment 之间、Fragment 与 Activity、Service 与页面,不再借 Activity 中转。
- 后端模块解耦:领域事件的轻量版——下单成功后通知积分、库存、推送模块各自处理,下单服务不关心下游有谁。
- 配置热更新:配置中心拉到新值后 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,别让一条进程内总线去干它不该干的活。