Spring Application Event 在事件驱动设计中的应用
1. 什么是事件驱动设计
我们先从去餐厅吃饭来辅助理解什么是事件驱动设计,我们从点菜到上菜通常涉及到以下角色:
- 服务员
- 厨师
- 上菜员
不同角色的职责:
- 服务员负责协助点餐
- 厨师负责制作菜品
- 上菜员负责上菜
我们通过事件的角度来考虑整个流程:
- 顾客点餐事件,事件内容为菜品与餐位信息;
- 厨师收到顾客点餐事件,开始制作菜品;
- 厨师完成菜品制作事件,事件内容为菜品与餐位信息;
- 上菜员收到菜品制作事件,上菜到指定餐位;
通过以上案例,我们可以将事件驱动设计总结为对业务过程中所发生的事件进行抽象,通过抽象后的事件来考虑代码、应用架构、业务流程编排设计的一种思维。
2. 代码中使用事件发布监听好处
在系统建设过程中通常会遇到两个不同领域耦合的老代码和基于系统规模和时间成本不得不采用妥协方案而导致的耦合,为了应对这种工程实践的现状,考虑可扩展、易维护、可拆分,我们在无法对其进行服务级别隔离时,可以考虑通过事件发布和监听的手段将需要协同处理业务的不同领域代码进行依赖分离。
3. 为什么使用 Spring Application Event
实现事件发布监听的方式多种多样,比较常接触到的有以下几种:
-
基于 Java 内置观察者模式相关接口实现
-
Guava EventBus
-
Spring Application Event
现在 Java 后端开发主流都是基于 Spring 生态,从减少依赖和提升开发效率方面考虑选择了 Spring Application Event,当然 Guava EventBus 也非常不错,只不过 Guava 的版本管理做的不够好,经常冲突不断,所以我通常在项目里能不用就不用。
4. Spring Application Event 应用案例
4.1. 订单领域事件
public class OrderDomainEvent<T> extends PayloadApplicationEvent<T> {
/**
* Create a new PayloadApplicationEvent.
*
* @param source the object on which the event initially occurred (never {@code null})
* @param payload the payload object (never {@code null})
*/
public OrderDomainEvent(Object source, T payload) {
super(source, payload);
}
}
4.2. 订单领域事件源
public enum OrderDomainEventSource {
/**
* 销售订单创建事件源
*/
SALE_ORDER_CREATE("ORDER-SERVICE:ORDER-CREATE-EVENT", "销售订单创建事件"),;
/**
* 事件原
*/
private final String source;
/**
* 事件原描述
*/
private final String desc;
OrderDomainEventSource(String source, String desc) {
this.source = source;
this.desc = desc;
}
public String getSource() {
return source;
}
public String getDesc() {
return desc;
}
}
4.3. 订单领域事件信息载体
public class OrderCreateEventPayload {
/**
* 订单编号
*/
private String orderNumber;
public String getOrderNumber() {
return orderNumber;
}
public void setOrderNumber(String orderNumber) {
this.orderNumber = orderNumber;
}
@Override
public String toString() {
return "OrderCreateEventPayload{" +
"orderNumber='" + orderNumber + '\'' +
'}';
}
}
通常在事件中携带信息设计可分两种:
- 通过唯一标识回查信息
- 事件载体中携带当前领域对象不可变信息
在实践过程中我通常使用第二种方案,这样可以减少依赖。
4.4. 订单领域事件发布
@Component
public class OrderDomainEventPublisher implements ApplicationEventPublisherAware {
private static final LogUtil LOG_UTIL = LogUtil.getLogger(OrderDomainEventPublisher.class);
private ApplicationEventPublisher applicationEventPublisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
/**
* 订单领域事件发布
* @param orderDomainEvent 订单领域事件
*/
public <T> void publishEvent(OrderDomainEvent<T> orderDomainEvent){
OrderDomainEventSource saleOrderDomainEventSource = (OrderDomainEventSource) orderDomainEvent.getSource();
LOG_UTIL.info("{} 订单领域事件发布,事件源:{}", DateUtil.date(orderDomainEvent.getTimestamp()), saleOrderDomainEventSource.getDesc());
applicationEventPublisher.publishEvent(orderDomainEvent);
}
}
4.5. 订单领域事件监听
4.5.1. 监听者1
@Component
public class OrderCreateEventListener1 implements ApplicationListener<OrderDomainEvent<OrderCreateEventPayload>> {
private static final LogUtil LOG_UTIL = LogUtil.getLogger(OrderCreateEventListener1.class);
@Override
public void onApplicationEvent(OrderDomainEvent<OrderCreateEventPayload> event) {
OrderDomainEventSource orderCancelEventSource = (OrderDomainEventSource) event.getSource();
LOG_UTIL.info("{} 收到订单领域事件,事件发生时间:{},事件源:{}", DateUtil.now(), DateUtil.date(event.getTimestamp()), orderCancelEventSource.getDesc());
}
}
4.5.1. 监听者2
@Component
public class OrderCreateEventListener2 implements ApplicationListener<OrderDomainEvent<OrderCreateEventPayload>> {
private static final LogUtil LOG_UTIL = LogUtil.getLogger(OrderCreateEventListener2.class);
@Override
public void onApplicationEvent(OrderDomainEvent<OrderCreateEventPayload> event) {
OrderDomainEventSource orderCancelEventSource = (OrderDomainEventSource) event.getSource();
LOG_UTIL.info("{} 收到订单领域事件,事件发生时间:{},事件源:{}", DateUtil.now(), DateUtil.date(event.getTimestamp()), orderCancelEventSource.getDesc());
}
}
4.5.1. 监听者3
@Component
public class OrderCreateEventListener3 implements ApplicationListener<OrderDomainEvent<OrderCreateEventPayload>> {
private static final LogUtil LOG_UTIL = LogUtil.getLogger(OrderCreateEventListener3.class);
@Override
public void onApplicationEvent(OrderDomainEvent<OrderCreateEventPayload> event) {
OrderDomainEventSource orderCancelEventSource = (OrderDomainEventSource) event.getSource();
LOG_UTIL.info("{} 收到订单领域事件,事件发生时间:{},事件源:{}", DateUtil.now(), DateUtil.date(event.getTimestamp()), orderCancelEventSource.getDesc());
}
}
5. 通过案例来看设计的好处
在工程实践过程中通常面临两种情况:
- 多个不同领域业务划分为不同的服务;
- 多个不同领域业务因为时间成本、部署成本、运维成本、业务规模、需求发布节奏等多方面影响,而不得设计为单一服务,多个不同领域业务代码都包含在单一服务内;
从第一种情况来考虑,如果不对代码进行分离设计则会面临以下情况:
- 不关注的依赖对领域模型的侵入;
- 分布式事务解决方案对领域模型的侵入;
从第二种情况来考虑,如果不对代码进行分离设计则会面临以下情况:
- 不关注的依赖对领域模型的侵入;
- 后续拆分复杂度的增加;
从以上分析不难看出通过事件发布和监听手段对代码进行隔离的好处在于对无关依赖的隔离、对技术复杂度的隔离、方便后续拆分。