1. 技术调研
ElegentAC(异步调用框架)是一款基于MQ的微服务异步调用框架。 这个框架可以让你更优雅地在项目中编写微服务异步调用的代码。这个组件具有的特点:
- 支持 MQTT ,rabbitMQ 等 多种MQ的实现方式,并支持用户自行扩展。
- 用最优雅的方式实现了消息的接收处理。接收消息只需要通过注解指定主题名称,主题支持通配符和变量,消息自动转换为发送时的 DTO类型,无需手动转换。
- 为异步调用的链路,产生一个唯一的链路ID,这个ID可以被应用系统用于记录日志等。
- 具备很强的扩展性,基于ElegentAC可以开发其它上层框架,比如下面介绍的ElegentACTX 就是基于ElegentAC的分布式事务框架。
阅读ElegentAC 的readme文档,了解ElegentAC基本使用方法。运行ElegentAC-demo工程,熟悉ElegentAC如何在项目中集成和使用。
2. 完成工单跨服务逻辑
2.1 技术集成
我们现在需要做的,就是将ElegentAC框架集成帝可得项目中。
(1)引入依赖 。在service_common引入依赖(因为所有的子模块都会用到异步调用)
<dependency>
<groupId>cn.elegent</groupId>
<artifactId>elegent-AC</artifactId>
<version>1.0.0</version>
</dependency>
(2)修改配置文件。工单微服务配置添加:
elegent:
ac:
type: mqtt
host: 192.168.200.128
port: 1883
clientId: monitor.task${random.int[1000,9999]}
username: admin
password: public
售货机微服务配置添加:
elegent:
ac:
type: mqtt
host: 192.168.200.128
port: 1883
clientId: monitor.vm${random.int[1000,9999]}
username: admin
password: public
2.2 代码编写
目前我们需要完成两个功能:
2.2.1 完成运维工单
工单微服务在完成运维工单后,通过ElegentAC向售货机微服务发送消息,售货机微服务接收到消息后,根据工单类型更改售货机状态。
(1)定义封装类:TaskCompleteContract
(2)定义主题:server/vms/completed (TopicConfig.VMS_COMPLETED_TOPIC)
(3)工单微服务发送消息
工单微服务添加异步调用,修改TaskServiceImpl
@Autowired
private ElegentAC elegentAC;
@Override
public boolean completeTask(long id) {
//-----略-------
//--------------如果是投放工单或撤机工单-------------
if(taskEntity.getProductTypeId()==TaskType.TASK_TYPE_DEPLOY
|| taskEntity.getProductTypeId()==TaskType.TASK_TYPE_REVOKE){
noticeVMServiceStatus(taskEntity);//运维工单封装与下发
}
return true;
}
/**
* 运维工单封装与下发
* @param taskEntity
*/
private void noticeVMServiceStatus(TaskEntity taskEntity){
//向消息队列发送消息,通知售货机更改状态
//封装协议
TaskCompleteContract taskCompleteContract=new TaskCompleteContract();
taskCompleteContract.setInnerCode(taskEntity.getInnerCode());//售货机编号
taskCompleteContract.setTaskType( taskEntity.getProductTypeId() );//工单类型
//发送到emq
try {
elegentAC.publish( TopicConfig.VMS_COMPLETED_TOPIC, taskCompleteContract );
} catch (Exception e) {
log.error("发送工单完成协议出错");
throw new LogicException("发送工单完成协议出错");
}
}
(4)售货机微服务接收消息:创建com.dkd.handler包,在com.dkd.handler包下创建消息处理类
/**
* 完成工单消息处理类
*/
@Topic(TopicConfig.VMS_COMPLETED_TOPIC)
public class TaskCompletedMsgHandler implements ACHandler<TaskCompleteContract> {
@Autowired
private VendingMachineService vmService;//售货机服务
@Override
public void process(String topic,TaskCompleteContract taskCompleteContract) throws Exception {
if(taskCompleteContract==null || Strings.isNullOrEmpty(taskCompleteContract.getInnerCode()) ) return;
//如果是投放工单,将售货机修改为运营状态
if( taskCompleteContract.getTaskType()== TaskType.TASK_TYPE_DEPLOY){
vmService.updateStatus( taskCompleteContract.getInnerCode(), VmStatus.VM_STATUS_RUNNING );
}
//如果是撤机工单,将售货机修改为撤机状态
if( taskCompleteContract.getTaskType()== TaskType.TASK_TYPE_REVOKE){
vmService.updateStatus( taskCompleteContract.getInnerCode(), VmStatus.VM_STATUS_REVOKE );
}
}
}
2.2.2 完成补货工单
工单微服务在完成补货工单后,通过ElegentAC向售货机微服务发送消息,售货机微服务接收到消息后,更新售货机库存。
(1)封装类:SupplyContract
(2)主题: server/vms/supply (TopicConfig.VMS_SUPPLY_TOPIC )
(3)工单微服务发送消息:分两步:封(查询工单明细表, 封装协议类) 、发(发送到EMQ)
TaskDetailsService的buildSupplyContract 用于构建补货协议内容,已提供。
(1)工单微服务添加异步调用,修改TaskServiceImpl
@Override
public boolean completeTask(long id) {
//-----略-------
//如果是补货工单!!!!---------------------------
if(taskEntity.getProductTypeId()==TaskType.TASK_TYPE_SUPPLY){
noticeVMServiceSupply(taskEntity);
}
return true;
}
/**
* 补货协议封装与下发
* @param taskEntity
*/
private void noticeVMServiceSupply(TaskEntity taskEntity){
//1.根据工单id查询工单明细表
List<TaskDetailsEntity> details = taskDetailsService.getByTaskId(taskEntity.getTaskId());
//2.构建协议内容
SupplyContract supplyContract = taskDetailsService.buildSupplyContract(taskEntity.getInnerCode(), details);
//3.下发补货协议
//发送到emq
try {
elegentAC.publish( TopicConfig.VMS_SUPPLY_TOPIC, supplyContract );
} catch (Exception e) {
log.error("发送工单完成协议出错");
throw new LogicException("发送工单完成协议出错");
}
}
(4)售货机微服务接收消息:
在com.dkd.handler包下创建消息处理类,process方法中完成库存的更新(VendingMachineService的supply方法用于根据协议数据完成补货操作,已提供)。
/**
* 补货消息处理
*/
@Topic(TopicConfig.VMS_SUPPLY_TOPIC)
public class SupplyMsgHandler implements ACHandler<SupplyContract> {
@Autowired
private VendingMachineService vmService;
@Override
public void process(String topic,SupplyContract supplyContract) throws Exception {
//更新售货机库存
vmService.supply(supplyContract);
}
}
3. 深入探究 ElegentAC
3.1 架构设计
3.1.1 完整系统架构图
3.1.2 组件构成
ElegentAC框架组成
统一外观接口:提供了发送、订阅、取消订阅等方法的定义。
第三方策略集:针对统一外观接口的针对第三方中间件的实现类eg:emq(Eclipse paho)。
核心数据存储器:框架的核心数据存储器,存储了所有的消息处理类和需要订阅的主题。
消息处理类加载器:在服务启动时通过消息处理类加载器把所有的消息处理类加载到核心数据存储器中。
消息分发处理器:当微服务收到消息时,通过消息分发处理器,从核心数据存储器中拿到对应的消息处理类,并调用处理类处理消息的方法来处理消息。
ElegentAC框架组件功能简介
**发送逻辑:**用户在自己的业务层中注入ElegentAC,调用发送的api(publish方法),实现消息发送
**初始化逻辑:**项目启动的时候会系统会将所有需要订阅的主题和消息处理类初始化到核心数据存储器中
**订阅逻辑:**在初始化逻辑之后,调用统一外观接口中的订阅方法实现订阅。
**接收逻辑:**由于系统已经执行完订阅逻辑,微服务就可以接收MQ的消息,会通过消息分发处理器来找到对应的实现类,核心就是根据主题在核心数据存储器中找到匹配主题的消息处理类。
3.2 设计模式
3.2.1 外观模式(发送消息)
什么是外观模式? 外观模式提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用。使用外观模式时,我们创建了一个统一的类,用来包装子系统中一个或多个复杂的类,客户端可以直接通过外观类来调用内部子系统中方法,从而外观模式让客户和子系统之间避免了紧耦合。
ElegentAC 框架 提供了MQTT 和AMQP多种MQ协议的实现方式,通过定义一个统一的外观接口,隐藏了很多复杂的方法,使用户在使用MQ实现异步调用时变得非常容易。
3.2.2 模板方法模式(生成链路ID)
模板方法模式:
如果一个接口的多个实现类,都要实现一段相同的逻辑,那么模板方法模式可以简化这部分的开发。模板方法模式会将共同的逻辑提取到模板类的一个方法中使用。
ElegentAC 框架使用模板模式,处理了链路id的生成。这部分是通用的逻辑,写在每个策略类里代码会冗余。
什么是链路ID?
在异步调用过程中存在多级调用,这个过程中通过一个标识来标记哪些请求是属于一个完整的链路的标记就是链路ID。
如图所示:
为什么要用到链路ID?
在异步调用过程中,可能存在A调用B,B再调用C,C再调用…这样的一个过程,其实整个调用A->B->C整个过程应该是一个完整链路,但是由于都是异步调用的,当其中一个环境出现问题,其他调用方是不知道的,并且整个过程也没有标识来进行绑定,为了解决这样的问题我们提供了一层增强即提供了链路ID。
链路ID是如何实现的:
ThreadLocal+模板方法模式
在发送的时候我们注入的是ElegentAC接口,而第一层实现该接口的是我们的抽象模板
ACTemplateImpl.class
/**
* ACAbstractTemplateImpl
* @description AC框架提供的外观提供了: 发送 订阅 取消订阅 的统一实现模板
* @author WGL
* @date 2022/11/9 9:36
*/
public abstract class ACTemplateImpl implements ElegentAC {
/**
* 尝试从ThreadLocal中取出我们的全局id如果有则手动设置全局id
* 否则生成一个全局id
* @param topic
* @param data
* @return
*/
public boolean sendTemplate(String topic, Object data){
String chainID = ElegentACContext.get();
if(chainID==null) {
chainID = UUIDUtils.getUUID();
}
SendBody sendBody = new SendBody(data,chainID);
return this.publish(topic,sendBody);
}
SendBody.class
@Data
public class SendBody{
private Object data;
private String chainID;
}
该方法的参数列表是String 主题名称,Object类型 消息体,也就是说无论什么对象都可以目的是构建一个SendBody,而SendBody其实就是在原有的数据上进行了一层封装加上了链路ID(chainID)
然后进行一个判断通过ElegentACContext这个上下文获取ThreadLocal中的内容,这时如果是第一次发送消息这个ThreadLocal中的内容一定是空的,于是会通过UUIDUtils去构建一个UUID,作为这个链路ID,然后调用抽象方法publish将封装好的SendBody传递到第三方的发送方法进行消息发送。
在接收消息的时候会通过我们的消息分发处理器(ACDistributerImpl)来进行消息分发。如下
ACDistributerImpl.class
@Component
public class ACDistributerImpl implements ACDistributer {
@Override
public void distribute(String topic, String msgContent) {
try {
//获取对应的消息处理类
ACHandler acHandler = CoreData.get(topic);
SendBody sendBody = JsonUtil.getByJson(msgContent, SendBody.class);
String chainID = sendBody.getChainID();
//将链路id传入到ThreadLocal中
ElegentACContext.set(chainID);
if (acHandler == null) return;
//跟进获取泛型的类型进行Json转换
Object body = JsonUtil.getByJson(sendBody.getData().toString(), dtoClass);
//执行处理消息的逻辑
acHandler.process(topic, body);
}catch (Exception e){
e.printStackTrace();
}finally{
ElegentACContext.remove();
}
}
在拿到这个消息的时候会通过消息分发处理器将Json转换成SendBody并获取对应的链路ID,
SendBody sendBody = JsonUtil.getByJson(msgContent, SendBody.class);
String chainID = sendBody.getChainID();
将发送消息的链路ID存储在ThreadLocal中
ElegentACContext.set(chainID);
根据获取的消息处理类调用其处理业务的方法
acHandler.process(topic, body); //执行处理消息的逻辑
5)回收资源(删除ThreadLocal)
ElegentACContext.remove();//ThreadLocal资源回收
那么在消息分发处理的时候还进行异步消息分发,这时我们再来看发送逻辑的代码,这时由于消息分发处理器已经帮我们往线程副本里存放了一个链路ID这里发送的时候就不会拿到一个null了,会把上一级发送过来的链路ID拿到并继续构建当前这一级的SendBody对象,这样只要是一个异步调用的链路,则都会带上一个统一的链路ID。
ACTemplateImpl.class
public boolean sendTemplate(String topic, Object data){
String chainID = ElegentACContext.get();
if(ElegentACContext.get()==null) {
chainID = UUIDUtils.getUUID();
}
SendBody sendBody = new SendBody(data,chainID);
return this.publish(topic,sendBody);
}
3.2.3 消息分发-策略模式
什么是策略模式?针对一组算法,将每一个算法封装到具有共同接口的独立的类中,使得它们可以互换。
策略模式的特点
- 功能:具体算法从具体业务处理中独立
- 多个if-else出现考虑使用策略模式
- 策略算法是形同行为的不同实现(多态)
- 客户端选择,上下文来具体实现策略算法
ElegentAC 框架使用策略模式来实现消息的分发,避免了代码中写大量的if-else ,也使系统变得更加灵活便于维护。 ElegentAC 框架将消息处理类作为策略类,在服务启动的时候将所有的策略类加载到核心数据存储器中。当消息接收过来时,通过匹配消息主题,匹配到具体的消息处理类来执行。
具体的做法,我们撸代码:
策略封装环节:
在服务启动时会根据加载完成所有的ACHandler到CoreData的handlerMap中,这些步骤都是在服务启动时自动执行的。
ACHandlerLoader.class
/**
* 消息处理类加载器(策略模式)
* @author wgl
*/
@Component
@Slf4j
public class ACHandlerLoader implements ApplicationContextAware{
@Override
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
log.info("消息处理类加载器启动");
//key 订阅的主题 vlaue 是消息处理类
// 拿到了所有的消息处理类
Map<String,ACHandler> map = ctx.getBeansOfType(ACHandler.class);//拿到IOC容器中所有的MsgHandler的类
//我们需要的是什么 map key : 该消息处理类处理的主题名称 value: 消息处理类
map.values().stream().forEach(v->{
//当前消息处理类应该处理的是哪个主题下对应的消息
try {
Topic annotation = v.getClass().getAnnotation(Topic.class); //获取注解
if (annotation != null) {
//开起了自动订阅的才会添加到List集合中去进行自动订阅
String topicName = annotation.value();
CoreData.handlerMap.put(topicName, v); //处理器集合
}
}catch (Exception e){
e.printStackTrace();
}
});
//.......
}
.......
}
由于当前类是一个Spring组件(加上了@Component),Spring启动的时候会自动加载这个类,并且由于该类实现了ApplicationContextAware接口,Spring容器会自动调用该Bean的setApplicationContext(参数)方法,调用该方法时,在这里我们会通过Spring的上下文获取所有的消息处理类,即找到所有的ACHandler
ACHandlerLoader.class
Map<String,ACHandler> map = ctx.getBeansOfType(ACHandler.class);//拿到IOC容器中所有的MsgHandler的类
这时拿到的Map集合里的key是类名,Value是消息处理类,而我们最终需要构建一个Map集合,key为该消息处理类处理的主题名称,value是对应的消息处理类,而对应的主题保留在当前类的@Topic注解里,需要将@Topic注解里的主题名称和群组名称拿到,这里使用的是反射获取对应消息处理类上的@Topic注解。
消息分发:
当有用户发送消息到mq之后,由具体第三方消息中间件将消息发送给具体的微服务,这个时候每个第三方策略集都会有一个统一的接收消息的方法,接收到消息,以MQTT为例:MQTT的接收消息的方法如下:
MqttCallback.class
/**
* 基于Eclipse paho设置事件
* @author wgl
*/
@Component
@Slf4j
@ConditionalOnProperty(prefix = "elegent.ac",name = "type",havingValue = "mqtt")
public class MqttCallback implements MqttCallbackExtended{
@Autowired
private ACDistributer acDistributer;//消息分发处理器
//==============================重点关注==============================
@Override//接收到消息
public void messageArrived(String topic, MqttMessage message) throws Exception {
String msgContent = new String(message.getPayload());//获取emq发过来的消息体
acDistributer.distribute(topic,msgContent);//消息分发
mqttClient.messageArrivedComplete(message.getId(),message.getQos());
}
}
接收到消息以后,进行消息的分发。
MqttCallback.class
acDistributer.distribute(topic,msgContent);//消息分发
这里的ACDistributer是我们提供的消息分发处理器,该处理器处理的步骤:
1)根据收到消息的主题找对应的消息处理类
2)根据获取的消息处理类调用其处理业务的方法
ACDistributerImpl.class
/**
* 分发处理器
*/
@Component
public class ACDistributerImpl implements ACDistributer {
@Override
public void distribute(String topic, String msgContent) {
ACHandler acHandler = CoreData.get(topic);//这一步是重点 拿到消息的实现类
acHandler.process(topic, msgContent); //执行处理消息的逻辑
}
}