概述
有时候,我们只是想发布一些本地的事件,并不需要引入MQ的,可以直接使用Spring的ApplicationEventPublisher来完成简单事件的发布和监听的。
比如像下面的场景,ApplicationEventPublisher就够用了。
- 模块间的逻辑解耦,但不跨服务;
- 轻量级的本地事件通知
- 应用内部的解耦和扩展点:比如说在DDD里,聚合保存数据成功后,需要触发若干的后续动作
- 无强事务要求、无强持久化要求的通知场景
生产环境实战,门店创建成功后发布【门店已成功创建的事件】
在我目前这边,门店一旦创建成功后,是有很多一系列的后续动作要去做的,比如配置门店的各种各样的规则信息。
具体的代码很简单。
- 就是用ApplicationEventPublisher的publishEvent方法发布事件
- 用EventListener注解监听事件就可以了。
具体的代码实现如下。
事件发布者封装 ApplicationEventPublisher
ShopCreationEventPublisher 对 Spring 的 ApplicationEventPublisher 做了一层封装,统一事件创建和日志输出:
less
@Component
@RequiredArgsConstructor
@Slf4j
public class ShopCreationEventPublisher {
private final ApplicationEventPublisher applicationEventPublisher;
public void publishShopCreationEvent(Long shopId) {
if (shopId == null) {
return;
}
applicationEventPublisher.publishEvent(
new ShopCreationEvent(
shopId,
ShopConstant.ShopDomainEvent.SHOP_CREATION_EVENT.getCode()
)
);
log.info("Publishing {} for shopId: {}",
ShopConstant.ShopDomainEvent.SHOP_CREATION_EVENT.getCode(), shopId);
}
}
less
@AllArgsConstructor
@Getter
public enum ShopDomainEvent {
SHOP_CREATION_EVENT("shop_creation_event", "门店已创建"),
SHOP_UPDATE_EVENT("shop_update_event", "门店已更新");
/**
* 编码
*/
private final String code;
/**
* 描述
*/
private final String desc;
}
这个类做了几件小事:
- 封装事件构造 :上层只需要传
shopId,不关心事件 code 的细节 - ShopDomainEvent:定义门店的时间,有创建和更新
通过这层封装,业务代码几乎完全与 ApplicationEventPublisher 解耦,只依赖一个语义明确的发布器。ShopApplicationService类只需要调用发布方法,事件就发布出去了。
less
@RequiredArgsConstructor
@Service
@Slf4j
public class ShopApplicationService {
private final ShopCreationEventPublisher shopCreationEventPublisher;
/**
* 保存门店聚合信息
*/
public void saveShopInfo(SaveShopInfoCommand saveShopInfoCommand) {
// 构建聚合根
ShopInfoAggregateRoot root = shopInfoFactory.createShopInfoAggregateRoot(saveShopInfoCommand);
// 持久化
shopInfoRepository.persistence(root);
// 发布【门店已创建】的事件
shopCreationEventPublisher.publishShopCreationEvent(root.getId());
}
}
事件监听者实现
用EventListener注解就可以了。
less
@Component
@Slf4j
@RequiredArgsConstructor
class NewShopRuleGenerateListener {
@EventListener
public void onShopCreationEvent(ShopCreationEvent event) {
log.info("NewShopRuleGenerateListener received : {}", JSON.toJSONString(event));
Long shopId = event.getShopId();
if (shopId == null) {
return;
}
// 新启一个线程执行,避免阻塞事件发布线程
new Thread(() -> {
ShopInfoAggregateRoot shopInfoAggregateRoot = shopInfoRepository.selectByShopId(shopId.intValue());
// 新增的门店配置各种各样的规则
}).start();
}
可以看出:
- 监听方式非常直观 :通过
@EventListener标注方法,参数就是要监听的事件类型ShopCreationEvent - 业务逻辑完全从 ShopApplicationService中拆了出来,转移到一个职责清晰的监听器类里
- 为避免阻塞事件发布线程, 监听器内部主动使用新线程执行耗时操作
这条链路的好处在于:
-
保存门店的主流程对"谁在监听这个事件"是无感知的;
-
新增或删除监听器只影响监听方代码,不需要修改
ShopApplicationService;
实战落地建议
尽量为每类事件提供单独的发布器
- 比如
ShopCreationEventPublisher、ShopUpdatePublisher,而不是在业务代码里直接拿ApplicationEventPublisher - 便于后续在发布器层面统一做日志、限流或埋点
事件命名和字段保持业务语义清晰
ShopCreationEvent这种名称比"门店变更事件"更具体- 事件体只放必要字段,比如
shopId、事件类型 code,避免变成"大而全 DTO`
监听器职责要单一
- 一个监听器专注做一件事情
- 如果后续还有"新门店初始化库存"、"新门店推送消息"等,可以新建监听器按职责拆分
异步处理尽早抽象到统一机制
- 目前上面的代码使用
new Thread。这个是不太合理的, 应该是用线程池。
在不引入 MQ 的前提下,基于 ApplicationEventPublisher 的这种本地事件机制,可以在保持代码结构清晰的同时,给系统预留足够的扩展点,对很多中小体量的业务来说,这种方案已经足够实用。