订单重构新老接口切换方案
订单新老接口切换方案
1. 背景
订单重构代码,防止新代码上线,出现未知bug时控制影响范围,需对请求进行ab分流。
2.技术方案
基于RequestCondition
和Aviator
搭配Apollo
,对相同url请求,根据参数和配置进行版本判定,将部分请求走新接口,其余请求走旧版本。
如下列配置:
api.version.expressions.POST_order_v2_create="(partnerId != null && partnerId == '2221') ? 2 : 1"
则代表根据body中的partnerId==2221时,走版本2,其余走版本1
2.1 RequestCondition
用于Spring原生接口,用于处理请求的匹配规则,url header 等匹配本质上都是作为 RequestCondition
,只不过Spring已经为我们实现
2.1 Aviator
由于需要根据参数进行动态的版本判定,调研后选择使用Aviator
规则引擎,结合Apollo
动态配置来实现。Aviator
具有体积小巧,执行速度快(因为会将规则编译为字节码)的优点。
订单重构方案
1. 背景
订单基础服务目前存在多个许多针对已迁走商户的定制化业务;接口传递参数不规范;缓存混乱,对一致性未做出保证;代码分层不明确;同层级之间互相循环调用;数据库事务不规范,导致提前发送mq、刷新缓存。 针对以上问题,故进行代码重构,明确代码分层结构,和相关规范。
2. 重构方案
结合CQRS模式和阿里COLA进行分包
2.1. 代码结构
- client 模块
L api // CommandService、QueryService
L model
L req // 命令参数
L query // 查询参数
L resp
- app 模块
L application
L config // 配置相关
L internal
L commandservice // CommandServiceImpl
L queryservice // QueryServiceImpl
L outboundservice // ApplicationEventPublisher事件监听
L excutor // 执行器
L query // 查询执行器
L interfaces // 服务入口(web、mq等)
L controller // 对外提供web接口
L scheduled // 定时任务
L consumer // mq消费
- domain模块
L domain
L modle // 各种内部使用实体
L dto // 各种内部使用DTO
L data // 数据类DTO
L enums // 枚举
L entity // 数据库映射实体
L exception // 异常
L domainservice // 公用业务实现
L gateway // 内部网关声明,用于屏蔽底层
L util // 工具类相关
- infrastructure 模块
L infrastructure
L gateway // 内部网关实现,调用具体的mq、rpc、mappr、cache
L mq // mq发送
L mapper // 数据库
L cache // 缓存
L rpc // 服务调用
L req
L resp
L convert
模块依赖关系:
app
/ \
client domain
\
infrastructure
订单三个服务,domain和infrastructure进行公用
2.2. 规范
2.2.1. 调用规范
调用层级图:
controller
/ \
commandservice queryservice
\ /
excutor
/ \
/ domainService
/ /
gateway
/ \
-------------------
mapper mq cache rpc
interfaces
只作为入口,不处理具体逻辑,不得直接调用domainService层和infrastructure层。commandService
和queryService
中不进行具体的业务处理,而是交由excutor
进行处理。excutor
只暴漏一个execute(cmd)
方法, 事务包裹整个execute
方法。- 具体业务逻辑由
excutor
处理,禁止同层调用,如有公用逻辑,则应当抽取公共部分放入domainService
。 - 无特殊情况, 在
domainService
和excutor
中,尽可能调用gateway
层,而非直接访问infrastructure
。
2.2.2. 命名规范
- 命令参数:
*Cmd
- 查询参数:
*Qry
- 响应参数:
*Resp
- 命令执行器:
*CmdExe
- 查询执行器:
*QryExe
- DTO:
*DTO
- 工具类:
*Util
- 转换类:
*Converter
- 枚举:
*Enum
- 缓存管理器:
*CacheManager
2.2.3. 领域术语规范
由于订单做为基础服务,针对场景不单只有点餐,所以对于订单状态和订单动作通用术语作出如下调整:
创建订单 支付成功 接单 备货完成 发货 完成
无 -----> 待支付 -----> 待接单 -----> 备货中 -----> 待交付 -----> 配送中 -----> 已完成 已取消
| \_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _/ / \
| | / \ |
| | | 部分退(商品/金额) | 全退 |
| | \_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
| \ / | |
| 申请售后 同意申请 退货完成 退款完成 |
| 取消支付 无 -----> 待审核 -----> 待退货 -----> 已退货 -----> 已完成 <----\ |
| / \ \|
| 已拒绝 <----- / \ -----> 已取消 |
| 拒绝申请 取消申请 |
| |
\_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ /
2.3. 代码部分
2.3.1. 基础命令执行器模版
命令执行器作为业务处理的核心,已进行高度抽象形成模版,形成统一流程,其他命令执行器,只需要实现各自业务即可。
--- BaseCmdExeTemplate ---
@RequiredArgsConstructor
@Component
public abstract class BaseCmdExeTemplate<C extends CommonCmd, D, E, R> {
protected final ApplicationEventPublisher applicationEventPublisher;
@Transactional(rollbackFor = Exception.class)
public R execute(C cmd) {
E entity = null;
try {
// 验证参数
checkCmd(cmd);
// 查询
entity = queryEntity(cmd);
// 验证
checkEntity(cmd, entity);
// 构造数据
D saveData = buildSaveBean(cmd, entity);
// 保存
save(saveData, entity);
// 广播事件
publishEvent(cmd, saveData, entity);
return buildResp(saveData, entity);
} catch (Exception e) {
return processException(cmd, entity, e);
}
}
protected void checkCmd(C cmd) {}
protected abstract E queryEntity(C cmd);
protected void checkEntity(C cmd, E entity) {}
protected abstract D buildSaveBean(C cmd, E entity);
protected abstract void save(D saveData, E entity);
protected void publishEvent(C cmd, D saveData, E entity){}
protected R buildResp(D saveData, E entity) {
return null;
}
protected R processException(C cmd, E entity, Exception exception) {
if (exception instanceof RuntimeException) {
throw (RuntimeException) exception;
}
throw new RuntimeException(exception);
}
}
--- OrderBaseCmdExeTemplate ---
@Component
public abstract class OrderBaseCmdExeTemplate<C extends CommonOrderCmd, D, R> extends BaseCmdExeTemplate<C, D, OrderDTO, R> {
protected final OrderGateway orderGateway;
public OrderBaseCmdExeTemplate(ApplicationEventPublisher applicationEventPublisher,
OrderGateway orderGateway) {
super(applicationEventPublisher);
this.orderGateway = orderGateway;
}
@Override
protected OrderDTO queryEntity(CommonOrderCmd cmd) {
return orderGateway.queryAssertNotNullOrderByOrderCode(cmd.getPartnerId(), cmd.getOrderCode(),
QueryFieldEnum.ORDER_INFO
);
}
}
--- OrderStateFireCmdExeTemplate ---
@Component
public abstract class OrderStateFireCmdExeTemplate<C extends CommonOrderStateCmd, D, R> extends OrderBaseCmdExeTemplate<C, D, R> {
protected final StateMachineFactory stateMachineFactory;
public OrderStateFireCmdExeTemplate(ApplicationEventPublisher applicationEventPublisher,
OrderGateway orderGateway,
StateMachineFactory stateMachineFactory) {
super(applicationEventPublisher, orderGateway);
this.stateMachineFactory = stateMachineFactory;
}
@Override
protected void checkEntity(C cmd, OrderDTO entity) {
// 状态机
OrderStatus orderState = stateMachineFire(cmd, entity);
cmd.setFireOrderState(orderState.getCode());
}
protected abstract OrderStatus stateMachineFire(C cmd, OrderDTO order);
}
示例: 订单接单
--- OrderController ---
@PostMapping("/accept")
public CommonResp<Void> accept(@Validated @RequestBody OrderAcceptCmd cmd) {
return orderStateCmdService.accept(cmd);
}
--- OrderStateCmdServiceImpl ---
@Override
public CommonResp<Void> accept(OrderAcceptCmd cmd) {
acceptCmdExe.execute(cmd);
return CommonResp.success();
}
--- OrderAcceptCmdExe ---
@Component
public class OrderAcceptCmdExe extends OrderStateFireCmdExeTemplate<OrderAcceptCmd, OrderAcceptDTO, Void> {
private final OrderTaskGateway orderTaskGateway;
public OrderAcceptCmdExe(ApplicationEventPublisher applicationEventPublisher,
OrderGateway orderGateway,
StateMachineFactory stateMachineFactory,
OrderTaskGateway orderTaskGateway) {
super(applicationEventPublisher, orderMag, stateMachineFactory);
this.orderTaskGateway = orderTaskGateway;
}
@Override
protected OrderStatus stateMachineFire(OrderAcceptCmd cmd, OrderDTO order) {
OrderStateMachine orderStateMachine = stateMachineFactory.getOrderMachine(order.getOrderState());
orderStateMachine.fire(OrderStatusEvent.ACCEPT, order);
return orderStateMachine.getCurrentState();
}
@Override
protected OrderAcceptDTO buildSaveBean(OrderAcceptCmd cmd, OrderDTO order) {
OrderAcceptDTO acceptData = BeanMapperUtil.map(cmd, OrderAcceptDTO.class);
acceptData.setOrderState(cmd.getFireOrderState());
acceptData.setAcceptTime(new Date());
Map<String, Object> extInfo = new HashMap<>();
extInfo.put("dispatchType", Optional.ofNullable(cmd.getDispatchType()).orElse("0"));
extInfo.put("dispatchTimeout", Optional.ofNullable(cmd.getDispatchTimeout()).orElse(0));
acceptData.setExtInfo(ExtInfoUtil.mergeToStr(order.getExtInfo(), extInfo));
return acceptData;
}
@Override
protected void save(OrderAcceptDTO saveData, OrderDTO order) {
orderGateway.accept(saveData, order);
// 解锁任务
orderTaskGateway.unBlock(order.getOrderCode(), order.getOrderState(), OrderTaskType.BOOK_TASK.getCode(), saveData.getTimeout());
if (Objects.equals(OrderClientType.SAAS_MALL.getCode(), order.getOrderClient())
|| Objects.equals(OrderBizType.SAAS_MALL.getCode(), order.getBizType())) {
// 创建任务
orderTaskGateway.createOrderProducedRefundTimeout(order.getPartnerId(), order.getOrderCode());
}
}
@Override
protected void publishEvent(OrderAcceptCmd cmd, OrderAcceptDTO saveData, OrderDTO order) {
applicationEventPublisher.publishEvent(new OrderAcceptEvent(
OrderAcceptEvent.EventData.builder()
.partnerId(order.getPartnerId())
.storeId(order.getStoreId())
.orderClient(order.getOrderClient())
.bizType(order.getBizType())
.orderCode(order.getOrderCode())
.orderState(saveData.getOrderState())
.timeout(cmd.getTimeout())
.remindTime(cmd.getRemindTime())
.build()
));
}
}
--- OrderStateEventPublisherService ---
@TransactionalEventListener
public void handleAcceptEvent(OrderAcceptEvent event) {
OrderAcceptEvent.EventData eventData = event.getEventData();
// 缓存处理
orderCacheManager.clear(eventData.getOrderCode(),
OrderCacheField.ORDER_DTO,
OrderCacheField.ORDER_OPT_HISTORY_LIST
);
// 餐饮订单
if (Objects.equals(OrderBizType.ORDER.getCode(), eventData.getBizType()) || Objects.equals(OrderBizType.ADVANCE_ORDER.getCode(), eventData.getBizType())) {
// 订单变化
orderChangedCacheManager.add(eventData.getPartnerId(), eventData.getStoreId(), eventData.getOrderClient(), eventData.getOrderCode());
}
// 发送总线消息
orderMsgService.sendOrderMessage(eventData.getPartnerId(), eventData.getOrderCode(), eventData.getOrderState(),
MsgEvent.ACCEPT, eventData.getTimeout(), eventData.getRemindTime());
}
Eden-Flow 组件
版本更新历史
版本号 | 更新时间 | 更新人 | 更新内容 |
---|---|---|---|
1.0.0 | 2022-01-10 | 查志伟 | flow融入eden |
1.0.5-SNAPSHOT | 2023-04-10 | 周晓航 | flow的task新增可拓展方法执行功能 |
FLow的功能
- 通过接口泛型约束,强制代码规范化
- 业务流程结构化,链路清晰,数据传输更简单,易维护
- 支持拦截器,每个flow实现可自由指定一个或多个拦截器实现
- 支持同步、异步的方式来进行流程编排
- 可灵活指定任意流程的异常处理
- 可控制任意流程的超时时间
Flow使用手册
定义与前端交互的报文结构(request、response)
request需要继承FlowRequest public class FlowTestRequest extends FlowRequest {...}
定义flow中的dto
dto需要继承AppDTO, AppDTO的俩个泛型类型为上面定义的 request和response
public class FlowTestDto extends FlowDto<FlowTestRequest, String> {...}
定义flow接口
flow接口需要继承FlowInf,泛型类型为上面定义的 dto、request、response, 接口需要使用@EdenFlow注解标识 关于EdenFlow的属性配置,可查看源码api
@EdenFlow(async = true, interceptor = MyFlowInterceptor.class)
public interface TestInf extends FlowInf<FlowTestDto, FlowTestRequest, String> {
@FlowMethod(order = 1)
boolean step1(FlowTestDto dto);
@FlowMethod(order = 2)
boolean step2(FlowTestDto dto);
@FlowMethod(order = 2)
boolean step3(FlowTestDto dto);
@FlowMethod(order = 2, isMust = false, timeout = 100)
boolean step4(FlowTestDto dto);
@FlowMethod(order = 3)
boolean step5(FlowTestDto dto);
@FlowExceptionHandle(bindMethod = "step5", handleException = NullPointerException.class)
boolean ex1(FlowTestDto dto);
@FlowExceptionHandle(handleException = Exception.class, completelyMatch = false)
boolean ex2(FlowTestDto dto);
}
定义业务流程
/**
* 方法入参必须是父类的第一个泛型类型(即定义的dto)
* 方法返回值必须是Boolean值, 表示是否继续执行下一个flowMethod
* 方法必须加上@FlowMethod注解,否则不执行,order可指定方法执行优先级,越小优先级越高
* order最小值为1
*/
@FlowMethod(order = 1)
Boolean queryStoreMenuInfo(QueryStoreMenuDto dto) ;
定义异常处理流程(可选)
/**
* 方法入参必须是父类的第一个泛型类型(即定义的dto)
* 方法返回值必须是Boolean值, 表示是否继续执行下一个flowMethod
* 方法必须加上@FlowExceptionHandle,否则不生效
* FlowExceptionHandle的属性,可查询源码api
*/
@FlowExceptionHandle(bindMethod = "step5", handleException = NullPointerException.class)
boolean ex1(FlowTestDto dto);
@FlowExceptionHandle(handleException = Exception.class, completelyMatch = false)
boolean ex2(FlowTestDto dto);
业务流程实现
/**
* 注入到spring容器中
* 必须继承FlowImpl,实现上一步定义的interface
* 在该类中聚合处理业务逻辑,后续开发遵从规范编写即可
*/
@Service
public class TestImpl extends FlowImpl<FlowTestDto, FlowTestRequest, String> implements TestInf {
}
使用Flow
@RestController
@RequiredArgsConstructor
public class FlowController {
private final TestInf testInf;
@PostMapping("/flowTest")
public Result<String> flowTest(@RequestBody FlowTestRequest request) {
return testInf.start(request);
}
}
Flow拦截器
实现接口FlowInterceptor并注入到spring容器中,然后再EdenFlow属性中配置即可
@Configuration
public class MyFlowInterceptor implements FlowInterceptor {
/**
* flow 执行任务前,会统一被拦截器进行拦截
*
* @param dto flow中的dto对象
* @return boolean 是否继续执行
*/
@Override
public boolean handle(FlowDto dto) {
MyFlowDto myDto = (MyFlowDto) dto;
System.out.println("请求报文:" + JSON.toJSONString(myDto.getRequestVO()));
myDto.setSessionId("666666666");
return true;
}
/**
* 返回拦截器的优先级, 越小优先级越高
*
* @return 优先级
*/
@Override
public int order() {
return 1;
}
}
flow核心api
/**
* Flow的接口注解,定义flow的一些配置
* @author Clover.z
* @since 1.0.0
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EdenFlow {
/**
* 是否异步执行
*/
boolean async() default false;
/**
* flow 配置的拦截器, 可配置多个
* 拦截器需实现FlowInterceptor接口
* @see com.freemud.eden.flow.intercept.FlowInterceptor
*/
Class<?>[] interceptor() default {};
}
/**
* 被注解的方法会被flow执行
*
* @author Clover.z
* @since 1.0.0
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FlowMethod {
/**
* 执行顺序 必须>0
* 相同的order方法标识为一组任务
* flow为同步执行时, 这组任务会无序执行
* flow为异步执行时, 这组任务会并发执行
*/
int order();
/**
* 超时时间 正整数, 0表示永不超时
* 单位 毫秒
* 异步执行时生效
*/
long timeout() default 0;
/**
* 是否必须执行完成
* 配置为false则表示, 不管任务执行结果如何(或者异步中超时),都不会中断流程
*/
boolean isMust() default true;
}
/**
* 标志方法为异常处理方法
*
* @author Clover.z
* @since 1.0.0
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FlowExceptionHandle {
/**
* 异常处理顺序索引, 越小优先级越高
*/
int index() default 0;
/**
* 指定哪些flowMethod中出现异常才会被处理
* 不配置 默认所有method中的异常,都会被处理
*/
String[] bindMethod() default {};
/**
* 指定flowMethod中出现指定的异常才会被处理
* 不配置,默认所有类型异常都会被处理
*/
Class[] handleException() default { };
/**
* 匹配异常时 是否使用完全匹配
* true: 异常类型必须完全相等(配置Exception就只能捕捉Exception,其他类型的不进行捕捉)
* false: 是指定异常的子类 也可以捕捉(配置Throwable就等于匹配全部异常)
*/
boolean completelyMatch() default true;
}
/**
* flow 拦截器
* @author Clover.z
* @since 1.0.0
*/
public interface FlowInterceptor {
/**
* flow 执行任务前,会统一被拦截器进行拦截
* @param dto flow中的dto对象
* @return boolean 是否继续执行
*/
boolean handle(FlowDto dto);
/**
* 返回拦截器的优先级, 越小优先级越高
* @return 优先级
*/
int order();
}
一、初始化信息
FlowInitListener implements ApplicationListener<ContextRefreshedEvent>
重写onApplicationEvent方法 实现了自定义监听
ContextRefreshedEvent 在spring容器重载后监听执行 启动时会进行重载,从而实现了自动加载
处理步骤
1.通过容器获得所有FlowInf的实现类
2.寻找@EdenFlow 用于获取基础配置信息(是否异步、interceptor)
遍历其方法,并寻找注解,具体如下
@FlowMethod 根据isMust 与 超时时间细分 preGroup(前置Task信息) 与 taskGroup(主流程Task信息)
注意:这里将构建好的信息使用TreeMap(红黑树结构)存储
根据数据结构特性自动实行了排序,保障了后续操作的数据都是根据order从小到大排列
Key:order
Value:List<FlowTask.Task>
@FlowFinally 最终方法
@FlowExceptionHandle 异常处理器,可以自定义捕获异常与处理方法
3.构建FlowContext
3.1 flowBeanMap:
Map结构
Key:类的reference
(例如:com.freemud.sandload.alapos.business.flow.coupon.impl.QueryCouponProductFlowImpl)
Value:根据上述注解的信息构建FlowBean实体
@FlowMethod -> taskList
@FlowFinally -> finallyMethod
@FlowExceptionHandle -> handleList
@EdenFlow -> config
3.2 flowInterceptorMap
Map结构
Key:类的reference
(例如:com.freemud.sandload.alapos.business.flow.coupon.impl.QueryCouponProductFlowImpl)
Value:config所配置的interceptor
二、执行过程
程序入口: FlowInf的start方法
实现类FlowImpl
处理步骤:
1.获取FlowContext的interceptor,并执行其handle方法
2.根据FlowBean的config生成runner的实现类(AsyncRunner/SyncRunner)
3.FlowRunner开启执行runner方法
run(String flowName, FlowDto<?, ?> dto, ThreadPoolExecutor threadPoolExecutor)
flowName: 类的referenct
Dto: runner执行链路中的共享参数类
threadPoolExecutor: 线程池
4.根据flowName从FlowContext获取初始化存储的FlowBean实体
5.遍历FlowBean的taskList执行,根据初始化时分类好的preGroup与taskGroup,执行invoke方法(异步形式需要额外处理)
6.异常处理 使用flowBean的handleList遍历处理异常
7.执行flowBean的finalMethod方法
三、补充说明多线程执行方式
使用AsyncRunner时
1.线程池使用
FlowAutoConfig中@ConditionalOnMissingBean接收 无注入类的情况(当前线程池接口无实现类)
Executors.newCachedThreadPool() 默认配置
2.执行顺序
根据taskGroup和preGroup构建TaskWrapper
CompletableFuture.runAsync 开启多线程
返回futures和wrappers
根据futures的执行时间判断是否超时
根据wrappers的状态判断是否执行成功