Skip to content

Event 事件总线

event 模块提供同步、进程内事件总线。它用于解耦同一 Rain 应用中的模块,不是消息队列,也不负责跨进程投递、持久化或重试。

适合 Event 的场景是“一个已经发生的事实需要多个相互独立的后续响应”,例如订单创建后记录审计、更新缓存和发送通知。不适合用 Event 隐藏必须成功的核心步骤,也不适合替代跨服务消息系统。

最短使用路径是:添加依赖 → 定义 Event → 声明 @EventListener → 注入 EventBus 发布。下文按这个顺序展开。

添加依赖

kotlin
implementation("com.IceCreamQAQ.Rain:event:1.0.0-DEV12")

EventBus 标记了 @AutoBind,应用引入模块并完成扫描后,通常可按接口注入:

kotlin
class OrderService(
    private val eventBus: EventBus,
)

定义事件

事件只需实现空标记接口:

kotlin
import rain.api.event.Event

data class OrderCreated(
    val orderId: Long,
    val customerId: Long,
) : Event

事件对象应当表达“已经发生的事实”,建议使用不可变数据。不要把 Event 当作可随意读写的共享 DTO,取消事件除外。

声明监听器

kotlin
import rain.event.annotation.EventListener
import rain.event.annotation.SubscribeEvent

@EventListener
class OrderEventListener(
    private val mailService: MailService,
) {
    @SubscribeEvent
    fun onCreated(event: OrderCreated) {
        mailService.sendReceipt(event.orderId)
    }
}

要求:

  • 监听器类使用 @EventListener,并位于 rain.scanPackages
  • 类必须能由 Rain DI 创建。
  • 监听方法使用 @SubscribeEvent
  • 监听方法必须至少有一个事件参数;当前生成器直接读取第一个参数,推荐严格只声明一个参数。
  • 方法可为实例方法或静态方法。

EventListenerLoader 扫描类的公开方法,为每个订阅方法生成 EventInvoker 实现并注册到 EventBus。

发布事件

kotlin
val canceled = eventBus.post(OrderCreated(order.id, order.customerId))

post 在调用线程同步执行全部匹配监听器。普通事件通常返回 false;可取消事件被取消时返回 true

因为是同步调用:

  • 发布者会承担监听器执行时间。
  • ThreadLocal、事务等调用线程上下文仍然存在。
  • 慢 I/O 会直接拖慢发布者。
  • 需要异步时,应在监听器中显式提交到协程/线程池,并自行处理生命周期和异常。

父类型分发

生成的 EventInvoker 先执行:

java
if (!(event instanceof ListenerParameterType)) return;

所以监听父类或接口会收到所有子类型事件:

kotlin
sealed interface DomainEvent : Event

data class UserCreated(val id: Long) : DomainEvent

data class UserDeleted(val id: Long) : DomainEvent

@SubscribeEvent
fun onAnyDomainEvent(event: DomainEvent) {
    // 两种事件都会进入
}

EventBus 的列表按权重组织,而不是按事件类型建立索引。每次发布会遍历各权重下的监听器,类型不匹配的生成 invoker 立即返回。监听器数量非常大时应关注线性遍历成本。

权重与顺序

kotlin
@SubscribeEvent(weight = SubscribeEvent.Weight.HIGH)
fun validate(event: BeforePayment) = Unit

普通权重执行顺序:

text
HIGHEST → HIGH → NORMAL → LOW → LOWEST

同一权重内按照注册列表顺序执行,但业务代码不应依赖扫描带来的同权重顺序;存在因果关系时应调整权重,或合并为一个明确的协调监听器。

RECORD 权重

RECORD 是特殊阶段,总是在其他监听器之前执行。对于可取消事件,RECORD 阶段结束后 EventBus 会把 isCanceled 重置为 false,再进入普通权重。

因此 RECORD 适合旁路记录“事件曾被发布”,不适合阻止后续传播:

kotlin
@SubscribeEvent(weight = Weight.RECORD)
fun record(event: CommandEvent) {
    audit.append(event)
}

可取消事件

实现 CancelAbleEvent,或继承 AbstractCancelAbleEvent

kotlin
class BeforeOrderSubmit(
    val order: Order,
) : AbstractCancelAbleEvent()

监听器调用 cancel()

kotlin
@SubscribeEvent(weight = Weight.HIGH)
fun checkStock(event: BeforeOrderSubmit) {
    if (!stockService.available(event.order)) event.cancel()
}

普通权重阶段中,只要监听器执行后 isCanceled == true,EventBus 立即停止传播并让 post 返回 true

kotlin
val event = BeforeOrderSubmit(order)
if (eventBus.post(event)) return SubmitResult.Rejected

取消不是异常,不会自动回滚已执行监听器的副作用。应把高优先级监听器设计为验证阶段,把真正副作用放在确认不会被取消之后。

监听器异常

EventBus 捕获监听器抛出的 Throwable

  1. 记录错误日志。
  2. 发布 EventListenerRunExceptionEvent
  3. 继续事件总线控制流。
kotlin
@SubscribeEvent
fun onListenerFailure(event: EventListenerRunExceptionEvent) {
    alertService.report(event.listenerInfo.methodFullName, event.throwable)
}

为了避免无限递归,处理 EventListenerRunExceptionEvent 本身抛错时不会再次发布同类异常事件。

错误不会传回发布者

监听器异常被 EventBus 捕获,post() 不会因普通监听器失败而抛出。需要“任何监听失败都让业务失败”的强一致流程,不应使用当前 EventBus 作为调用机制,应直接调用明确的业务服务或建立自己的失败收集协议。

编程式注册

具体实现提供:

kotlin
val impl = eventBus as EventBusImpl
impl.register(MyListener::class.java, listenerInstance)

它扫描类的 @SubscribeEvent 方法并生成 invoker。还可以直接注册/注销 EventListenerInfo

这些 API 属于实现类而非稳定 EventBus 接口,业务代码优先使用注解扫描。动态插件系统确实需要卸载监听器时,再封装实现细节。

Application 生命周期事件

Application 模块发布的是 rain.application.events.AppStatusEvent,注意不要与 Event 模块中历史遗留的同名 rain.event.events.AppStatusEvent 混淆。

kotlin
@SubscribeEvent
fun AppStatusEvent.AppStarted.onStarted() = Unit

AppStarted 在全部 ApplicationService.start() 完成后发布;AppStopping 在服务停止前发布。

EventBus 的简单实现

核心执行逻辑可以压缩为:

kotlin
for (weight in orderedWeights) {
    for (listener in listeners[weight]) {
        listener.invoker.invoke(event)
        if (event is CancelAbleEvent && event.isCanceled) return true
    }
}
return false

Rain 用 ASM 消除了每次事件分发时的反射 Method.invoke:启动时生成只有 instanceof + 强转 + 方法调用 的 EventInvoker。实现短、可读、运行路径明确;代价是没有异步、事务事件、泛型事件解析、监听返回值聚合和复杂错误策略。

使用建议

  • 用事件解耦“事实发生后的多个独立响应”。
  • 不要用事件隐藏必须成功的核心业务步骤。
  • 监听器保持快速;慢操作显式异步化。
  • 用不可变事件,避免监听器顺序影响数据内容。
  • 可取消事件只做前置决策,不在早期监听器执行不可逆副作用。
  • 为监听器异常注册统一告警监听器。

基于 Apache License 2.0 发布