前言:好久没更新博客啦。这阵子刚忙完,稍微空暇,就想分享下在开发中用过的领域事件。因为大家做微服务的,基本上都会用DDD去进行领域驱动设计。而领域事件是领域模型里一个很重要的概念。下面开搞,放心,不只是理论哦,有我实战的可运行demo,你可以照着这个模板去开发,领域的对象可以自己去抽象和建模哦~~ 阿信觉得这期是干货。前两节的理论部分借鉴了别的文章,但第三节开始全是个人工作中的实战,简化了核心代码,把核心框架抽成demo分享一下。

0.领域事件的优势

先说说领域事件的优势。让你明白为啥用它。事件驱动和观察者模式本质一样,事件驱动是观察者模式的经典实现。

事件驱动的好处:

1、 解耦,事件发布者和订阅者不需要预先知道彼此的存在。

2、 异步消息传递,业务逻辑和事件可以同步发生。

3、 多对多的交互,发布订阅模型。

1.领域事件定义

A domain event is a full-fledged part of the domain model, a representation of something that happened in the domain. Ignore irrelevant domain activity while making explicit the events that the domain experts want to track or be notified of, or which are associated with state change in the other model objects.

领域事件是一个领域模型中极其重要的部分,用来表示领域中发生的事件。忽略不相关的领域活动,同时明确领域专家要跟踪或希望被通知的事情,或与其他模型对象中的状态更改相关联。

针对官方释义,我们可以理出以下几个要点:

  • 领域事件作为领域模型的重要部分,是领域建模的工具之一。

  • 用来捕获领域中已经发生的事情。

  • 并不是领域中所有发生的事情都要建模为领域事件,要忽略无业务价值的事件。

  • 领域事件是领域专家所关心的(需要跟踪的、希望被通知的、会引起其他模型对象改变状态的)发生在领域中的一些事情。

简而言之,领域事件是用来捕获领域中发生的具有业务价值的一些事情。它的本质就是事件,不要将其复杂化。在DDD中,领域事件作为通用语言的一种,是为了清晰表述领域中产生的事件概念,帮助我们深入理解领域模型。

2.领域事件案例

举个栗子,拿一个订单系统来说,下单成功之后,后续的动作:需要更新订单状态为支付成功,扣减库存,通知用户交易成功。

在这个用例中,“订单支付成功”就是一个领域事件。

考虑一下,在你没有接触领域事件或EDA(事件驱动架构)之前,你会如何实现这个用例。肯定是简单直接的方法调用,在一个事务中分别去调用状态更新方法、扣减库存方法、发送用户通知方法。这无可厚非,毕竟之前都是这样干的。

那这样设计有什么问题?

  1. 试想一下,若现在要求支付成功后,需要额外发送一条付款成功通知到微信公众号,我们怎么实现?想必我们需要额外定义发送微信通知的接口并封装参数,然后再添加对方法的调用。这种做法虽然可以解决需求的变更,但很显然不够灵活耦合性强,也违反了OCP。

  2. 将多个操作放在同一个事务中,使用事务一致性可以保证多个操作要么全部成功要么全部失败。在一个事务中处理多个操作,若其中一个操作失败,则全部失败。但是,这在业务上是不允许的。客户成功支付了,却发现订单依旧为待付款,这会导致纠纷的。

  3. 违反了聚合的一大原则:在一个事务中,只对一个聚合进行修改。在这个用例中,很明显我们在一个事务中对订单聚合和库存聚合进行了修改。

那如何解决这些问题?我们可以借助领域事件的力量。

  1. 解耦,可以通过发布订阅模式,发布领域事件,让订阅者自行订阅;

  2. 通过领域事件来达到最终一致性,提高系统的稳定性和性能;

  3. 事件溯源

3.领域事件建模

抽象和建模能力是软工们必不可少的能力之一,这节针对上面的订单系统出个简单的模型设计。

抽象出如下对象:

  1. 事件源    entry

  2. 事件对象 domainMessage  = 事件类型 eventTopic + 事件源  entry

  3. 事件监听器   subscribe 处理事件

  4. 事件分发器 注册监听器  JvmEventConsumer

  5. 生产消息 实时消费,producer中直接consume 或者 MQ的形式,异步消费

领域事件流程图

4.领域事件实战

大家想要的demo在这里,可以说是大厂的编程模板了,哈哈。每段源码都会贴,但想下载源码,暂时我不想让你们偷懒

上面简单的流程图给大家看看模型的,这节直接上代码。先贴个我的类图

领域事件类图

补充:

事件的发布方式:

1. 发布订阅模式(本文采用的)

也有叫它观察者模式。其实区别不大。空了我会补上观察者模式和订阅模式的区别。

2.基于ThreadLocal的事件发布

3.MQ消息

事件的发布者生产MQ,消费去接受消费MQ。可以通过Mafka等MQ去实现的。

其实本文也是生产发布消息,只不过生产的是JVM的消息,然后消费者进程即产即消的方式去消费的。

这里事件的主题EventTopic先写两种:

  1. 支付
  2. 退款

下面是源码,包名都去掉了[Doge][Doge][Doge] ,入口是EventPublisher的Main函数


import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.core.util.UuidUtil;import java.util.Date;/*** 领域事件容器类**/
public class DomainMessage<T> {/**** msg的唯一标识* 常用语用于幂等校验*/private String messageId = StringUtils.EMPTY;/*** 消息的topic* see @{EventTopic}*/private EventTopic eventTopic;/*** 消息体*/private T messageBody;/*** 消息的创建时间*/private Date addTime = new Date();public void setMessageId(String messageId) {this.messageId = messageId;}public void setEventTopic(EventTopic eventTopic) {this.eventTopic = eventTopic;}public void setMessageBody(T messageBody) {this.messageBody = messageBody;}public String getMessageId() {return messageId;}public EventTopic getEventTopic() {return eventTopic;}public T getMessageBody() {return messageBody;}public Date getAddTime() {return addTime;}@Overridepublic boolean equals(Object obj) {if (obj == this) {return true;}if (!(obj instanceof DomainMessage)) {return false;}if (this.messageId.equals(((DomainMessage) obj).messageId)) {return true;}return super.equals(obj);}public DomainMessage() {}public DomainMessage(T messageBody, EventTopic eventTopic) {this.messageBody = messageBody;this.eventTopic = eventTopic;this.messageId = UuidUtil.getTimeBasedUuid().toString();}@Overridepublic int hashCode() {return messageId.hashCode();}@Overridepublic String toString() {return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);}}
/*** 领域事件消费者**/
public interface EventConsumer<T> {/*** 消费领域事件消息** @param domainMessage*/void consume(DomainMessage<T> domainMessage);
}

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class EventEntry {/*** 商品*/private String shop;/*** 金额*/private int amount;
}
/*** 领域事件生产者**/
public interface EventProducer<T> {/*** 生产DomainEvent事件** @param*/void produce(DomainMessage<T> domainMessage);}
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class EventPublisher {private static JvmEventProducer jvmEventProducer;public static void main(String[] args) {ApplicationContext context = new AnnotationConfigApplicationContext(BeanConfig.class);jvmEventProducer = context.getBean(JvmEventProducer.class);//发布一个支付成功的事件publishPayEvent();//发布一个退款成功的事件publishRefundEvent();}private static void publishPayEvent() {DomainMessage<EventEntry> message = new DomainMessage<>();message.setEventTopic(EventTopic.PAY);message.setMessageBody(EventEntry.builder().amount(100).build());jvmEventProducer.produce(message);}private static void publishRefundEvent() {DomainMessage<EventEntry> message = new DomainMessage<>();message.setEventTopic(EventTopic.REFUND);message.setMessageBody(EventEntry.builder().amount(100).build());jvmEventProducer.produce(message);}
}
/*** 消息订阅者**/
public interface EventSubscriber<T> {/*** 事件处理主函数** @param domainMessage*/void handlerEvent(DomainMessage<T> domainMessage);/*** 获取订阅者的订阅主题** @return*/Boolean isSubscribedTopic(EventTopic topic);
}
import org.springframework.stereotype.Component;/*** 占位用*/
@Component
public class EventSubscriberStub implements EventSubscriber<Void> {@Overridepublic void handlerEvent(DomainMessage<Void> domainMessage) {//do nothing}@Overridepublic Boolean isSubscribedTopic(EventTopic topic) {return false;}}
/*** 事件主题*/
public enum EventTopic {PAY("pay", "支付"),REFUND("refund", "退款"),;private EventTopic(String action, String desc) {this.action = action;this.desc = desc;}private String action;private String desc;public String getAction() {return action;}public void setAction(String action) {this.action = action;}public String getDesc() {return desc;}public void setDesc(String desc) {this.desc = desc;}
}
import com.google.common.collect.Lists;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** 基于JVM的消费机,只适用于单JVM项目应用,消费本JVM产生的Event**/
@Component
public class JvmEventConsumer implements EventConsumer, InitializingBean {private static final Logger LOGGER = LoggerFactory.getLogger(JvmEventConsumer.class);@Resourceprivate List<EventSubscriber> eventSubscribers;/*** 根据Topic注册信息或者EventSubscriber列表* 该列表在应用JVM实例初始化时初始化*/private Map<EventTopic, List<EventSubscriber>> topicQueue = new HashMap<EventTopic, List<EventSubscriber>>();private final ExecutorService executorService = Executors.newFixedThreadPool(100);@Overridepublic void consume(final DomainMessage domainMessage) {if (domainMessage != null) {if (MapUtils.isNotEmpty(topicQueue)) {List<EventSubscriber> topicList = topicQueue.get(domainMessage.getEventTopic());if (CollectionUtils.isNotEmpty(topicList)) {for (final EventSubscriber s : topicList) {try {executorService.submit(new Runnable() {@Overridepublic void run() {s.handlerEvent(domainMessage);}});} catch (Exception e) {LOGGER.error("EventSubscriber handler exception", e);}}}}}}@Overridepublic void afterPropertiesSet() throws Exception {try {//init rulesfor (EventTopic topic : EventTopic.values()) {for (EventSubscriber en : eventSubscribers) {if(en.isSubscribedTopic(topic)){if (topicQueue.containsKey(topic)) {topicQueue.get(topic).add(en);} else {List<EventSubscriber> tempList = Lists.newArrayList(en);topicQueue.put(topic, tempList);}}}}} catch (Exception e) {LOGGER.error("queueInit exception", e);}}
}
import com.dianping.cat.Cat;
import com.dianping.cat.message.Transaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;/*** 领域事件的生产者生产消息后,由同一个JVM实例的消费者消费;* 生产出来就同步消费掉,消息不做持久化和异步逻辑**/
@Service
public class JvmEventProducer implements EventProducer {@Autowiredprivate JvmEventConsumer jvmEventConsumer;@Overridepublic void produce(DomainMessage domainMessage) {Transaction transaction = Cat.newTransaction("JVMMessageProducer", domainMessage.getEventTopic().name());try {jvmEventConsumer.consume(domainMessage);transaction.setStatus(Transaction.SUCCESS);} finally {transaction.complete();}}}
import com.google.common.collect.Lists;
import org.springframework.stereotype.Component;import java.util.List;/*** @Desc 订单支付事件的订阅者*/@Component
public class OrderPayEventSubscriber implements EventSubscriber<EventEntry> {@Overridepublic void handlerEvent(DomainMessage<EventEntry> domainMessage) {EventTopic topic = domainMessage.getEventTopic();EventEntry entry = domainMessage.getMessageBody();switch (topic) {case PAY:handlePay(entry);break;case REFUND:handleRefund(entry);break;default:break;}}@Overridepublic Boolean isSubscribedTopic(EventTopic topic) {List<EventTopic> topics = Lists.newArrayList(EventTopic.PAY, EventTopic.REFUND);return topics.contains(topic);}private void handlePay(EventEntry entry) {System.out.println("更新订单状态为支付成功");reduceStock(entry);}private void handleRefund(EventEntry entry) {System.out.println("更新订单状态为支付失败");returnStock(entry);}private void reduceStock(EventEntry entry) {System.out.println("扣减库存成功");System.out.println("通知用户交易成功,您成功支付:" + entry.getAmount());}private void returnStock(EventEntry entry) {System.out.println("回退库存成功");System.out.println("通知用户退款成功,成功退款:" + entry.getAmount());}
}
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;@ComponentScan(basePackages = "你的包的路径")
@Configuration
public class BeanConfig {
}

最后附上运行结果:

针对demo的解释:

1.BeanConfig

是通过@Configuration和 @ComponentScan 扫面以上所有class所在的包,为了注入bean。通过XML配置的方式也可以。为了在main里可以注入需要的bean。

2.说一下JvmEventConsumer 实现了InitializingBean接口。

InitializingBean接口为bean提供了属性初始化后的处理方法,它只包括afterPropertiesSet方法,凡是继承该接口的类,在bean的属性初始化后都会执行该方法。spring在设置完属性之后就会调研afterPropertiesSet方法

关于spring初始化bean的方法:

1:spring为bean提供了两种初始化bean的方式,实现InitializingBean接口,实现afterPropertiesSet方法,或者在配置文件中同过init-method指定,两种方式可以同时使用 (我们经常用xml配置bean时候 init-method="init")

2:实现InitializingBean接口是直接调用afterPropertiesSet方法,比通过反射调用init-method指定的方法效率相对来说要高点。但是init-method方式消除了对spring的依赖

3:如果调用afterPropertiesSet方法时出错,则不调用init-method指定的方法。

好啦,看完这篇,你可以骚气的开发了。别总是把业务逻辑和事件耦合在一起,结合自己的业务场景,试试这花里胡哨却又实用的开发模式吧,解耦让你的代码更清爽,异步可以提升性能。只会无脑怼线程池去做异步操作的RD不是好RD哦。哈哈哈哈

浅谈领域事件及其应用相关推荐

  1. 浅谈“领域驱动设计”

    Eric Evans所著的<领域驱动设计>(Domain-Driven Design:通常简称为"DDD")一书可以说是经典中的经典,虽然"领域"的 ...

  2. 【浅谈DOM事件的优化】

    浅谈DOM事件的优化 在 JavaScript 程序的开发中,经常会用到一些频繁触发的 DOM 事件,如 mousemove.resize,还有不是那么常用的鼠标滚轮事件:mousewheel (在 ...

  3. 浅谈DOMContentLoaded事件及其封装方法

    我们在开发时,经常需要检测页面是否加载完毕,以确保脚本安全运行,下面我们就来浅谈一下检测页面是否加载完毕的那些事件们. 1. onload 事件 在页面的所有资源加载完成时,window对象上会触发一 ...

  4. 浅谈Javascript事件模拟

    事件是用来描述网页中某一特定有趣时刻的,众所周知事件通常是在由用户和浏览器进行 交互时触发,其实不然,通过Javascript可以在任何时间触发特定的事件,并且这些事件与浏览器创建的事件是相同的.这就 ...

  5. 浅谈Android事件分发机制

    在Android实际开发过程中经常会遇到View之间的滑动冲突,如ScrollView与Listview.RecyclerView之间的嵌套使用.在很好的解决此类问题之前,我们应深入的了解Androi ...

  6. 浅谈.net事件机制

    大家都有这样的经历: 打开VS--〉新建应用程序(FORM1)--〉往里面挪按钮--〉双击按钮--〉写代码 这看上去那么的自然,简单,可仔细想一下,窗口(FORM1)是一个类,按钮(Button)是另 ...

  7. 浅谈领域驱动设计(DDD:Domain-Driven Design)

    来源:<领域驱动设计>是2010年04月人民邮电出版社出版的图书,作者是Eric Evans.本书介绍了面向对象开发人员.系统分析人员合理地组织工作,彼此协作,有条不紊地进行复杂系统的开发 ...

  8. mousewheel 取消_浅谈Mousewheel 事件的用法

    当需要制作转动鼠标滚轮放大页面字体这样的交互效果时,会用到Mousewheel事件.其实在大多数浏览器(IE6, IE7, IE8, Opera 10+, Safari 5+)中,都提供了 " ...

  9. 浅谈:事件冒泡、事件捕获,及阻止办法

    1.事件冒泡和事件捕获分别由微软和网景公司提出,这两个概念都是为了解决页面中事件流(事件发生顺序)的问题. <div id="box1"><div id=&quo ...

最新文章

  1. 牛逼!用 12 万行代码堆出来个 蔡徐坤,编译还能通过!
  2. [Mvel]Mvel2.0使用指南一 基础
  3. 成功解决graphviz\backend.py, line 162, in pipe raise ExecutableNotFound(args) graphviz.backend.Executab
  4. vue+iview 设置全局的url以及跨越问题
  5. 全球及中国烯丙基硫脲行业十四五需求产量及投资规模预测报告2022版
  6. Vue过滤器的简单使用--实时显示格式化的时间
  7. python读写excel模块pandas_Python3使用pandas模块读写excel操作示例
  8. java zip文件夹_如何使用java压缩文件夹成为zip包
  9. linux获得系统时间 c,linux c 获取系统时间
  10. 解题报告:hdu 1556 Color the ball(区间修改,单点查询)
  11. 金明的预算方案(分组背包)
  12. Istio最佳实践:在K8s上通过Istio服务网格进行灰度发布
  13. command shortcut paste
  14. uc投屏按钮不见了_网页投屏,网页视频怎么投屏到电视,
  15. 获取与设置checkbox选中状态
  16. ai智能语音机器人的新风向
  17. KDD 2020阿里巴巴论文一分钟秒读
  18. Android 仿钉钉、微信 群聊组合头像,Android插件化入门指南
  19. 库函数 qsort 的用法
  20. Arduino录音时间延长_如何用arduino设计出可以语音播报的数字时钟

热门文章

  1. 走出新手村,软件测试银行项目怎么测试 + 面试题(答案)
  2. Java正则表达式替换域名
  3. 【愚公系列】2023年06月 网络安全(交通银行杯)-日志审计
  4. javascript:时间戳转换为日期时间
  5. 浅谈Java项目中要不要使用实体类
  6. 《这就是软件工程师》里的推荐书单
  7. python路径规划仿真实验_ROS探索总结(十四)—— move_base(路径规划)
  8. Spring Boot电商项目:概述;
  9. gluster快照创建
  10. 面试智力题:海盗分珍珠