项目开发过程中如果均是简单的数据请求与返回,那么方法调用和业务逻辑是最容易处理的,根据入参返回数据即可,数据的生命周期始于请求,终于数据返回,没有其他。
倘若特定需求场景需要多个接口协作完成一件事,数据流转存在多路由,业务逻辑处理将会呈现复杂化,朴素的数据流控制方式就是定义数据中间态通过硬编码形式来影响数据流向,这种设计在复杂度不深的情况下总是很容易实现,开发成本和沟通成本较低,也不失为一种非常有效的开发设计方式。而随着业务场景复杂化,流程变更频繁,开发人员会在之前得益的简单设计上发现维护和可拓展性极差,甚至陷入流程泥潭中难以自救,最直接的表现就是接口交互定制化,所有的交互看不到任何业务或故事主线,所有的服务交互都需要最原始的那些开发人员的文档、注释甚至“言传身教”的指导才可以洞察复杂业务的其中一二,这是软件开发中的技术负债和不完善,我们急需一个可以引导完整业务流程的体系或者框架来引导服务交互,来驱动业务数据流转,对数据的出生、中转、停留及最终消亡进行有效控制和监管,让服务有源可溯,有序可遵。关于以上概述都是为了引申出下面项目实践的利器,工作流。
关于工作流开源框架,一般有JBPM和Activiti,简单检索了下两者对比如下:
Activiti持久层通过MyBatis实现、与Spring融合支持事务,与当前项目技术背景较为符合,且上手较为便捷,参考资料广泛,学习成本较低,加上之前个人项目运用过Activiti前身,对PVM设计模式有一定了解,最终决定采用Activiti作为工作流来进行开发。
关于Activiti工作流的具体内容这里不做赘述,本文的核心放在Activiti工作流与业务结合的实践,下面是Activiti工作流的一些特点:
目前负责的项目是一个关于用户认证相关的业务,简单描述认证业务流程如下:
Service业务逻辑层
Dao数据持久层
目前Activiti已进入7.0.0+,翻阅了大量网上资料,该版本输出的时间较少,大部分还集中在5或6,这里基于可用可操作的思想选用了介绍参考资料详实的5.22.0进行开发,每个大版本变更差异较大,其他版本根据实践需要进行升级或取舍
<activiti.version>5.22.0activiti.version>
<dependency>
<groupId>org.activitigroupId>
<artifactId>activiti-engineartifactId>
<version>${activiti.version}version>
dependency>
<dependency>
<groupId>org.activitigroupId>
<artifactId>activiti-springartifactId>
<version>${activiti.version}version>
dependency>
Activiti默认是单库单表的,而我们现有项目是分库分表的,路由策略是根据用户ID进行水平切分的。
对Activiti的持久化存储进行分库分表改造基于以下两点考虑:
在实际项目环境下,没有bpmn流程调整我们是不需要频繁进行bpmn流程部署的,每次新流程部署都会更新刷下流程ID、实例ID等,而且数据也会产生变更调整,而我们的需求是在需要更新的时候更新,不需要更新的时候复用即可。因此在服务启动时,我这里通过时间戳作为版本号,每次启动Spring服务时进行bpmn部署检查,如果应用中的版本号未进行变更则不刷新Activiti部署信息保持之前部署快照,否则创建并部署新bpmn流程进行部署创建,更新部署快照信息,下面放一段demo代码,由于项目是分库分表的,每次启动服务会轮询每个分库的deploy信息情况。
/**
* @author: guanjian
* @date: 2020/11/24 19:48
* @description: Activiti部署
*/
@Component("activitiDeployServlet")
public class ActivitiDeployServlet {
private final static Logger LOGGER = LoggerFactory.getLogger(ActivitiDeployServlet.class);
@Autowired
private RepositoryService repositoryService;
@Autowired
private BaseDBRouter dbRouter;
/**
* BPMN流程定义部署
*
* @desc
* 1、根据分库数量(dbRouter.getDbNumber)进行bpmn流程定义部署
* 2、根据WORK_FLOW_DEPLOY进行部署,每次部署进行防重判断,变更WORK_FLOW_DEPLOY则重新部署即启用新流程
* WORK_FLOW_DEPLOY一致则不进行重新部署,复用原流程
*
*/
@PostConstruct
public void deploy() {
Optional.ofNullable(dbRouter.getDbKeyArray())
.orElse(Lists.newArrayList())
.forEach(db->{
try {
DBContextHolder.setDBKeyIndex(db);
handleDeployBpmn();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("check deploy bpmn error.");
} finally {
DBContextHolder.clearDBKeyIndex();
}
});
}
private Deployment queryBpmn() {
return repositoryService.createDeploymentQuery()
.deploymentName(ActivitiConstants.Deployment.WORK_FLOW_DEPLOY)
.singleResult();
}
private void handleDeployBpmn() {
Deployment deployment = queryBpmn();
if (Optional.ofNullable(deployment).isPresent()) {
LOGGER.info("{} bpmn exists.", DBContextHolder.getDBKeyIndex());
} else {
createBpmn();
}
}
private void createBpmn() {
repositoryService.createDeployment()
.disableSchemaValidation()
.name(ActivitiConstants.Deployment.WORK_FLOW_DEPLOY)
.addClasspathResource(ActivitiConstants.Deployment.BPMN_RESOURCE_PATH)
.disableSchemaValidation()
.deploy();
LOGGER.info("{} bpmn created.", DBContextHolder.getDBKeyIndex());
}
}
Activiti主要有以下几个核心服务,我们这里最重要的是使用到了RepositoryService、RuntimeService、TaskService,RepositoryService用来进行服务部署,RuntimeService用来启动任务管理任务实例,TaskService用来进行任务的查询、流转(完成、取消)等操作。
与Spring整合后,可以通过Spring来操作配置Activiti
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd">
<!-- Activiti processEngineConfiguration -->
<bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration">
<property name="dataSource" ref="routerTargetDataSource"/>
<property name="transactionManager" ref="routerTransactionManager"/>
<!--
flase: 默认值。activiti在启动时,会对比数据库表中保存的版本,如果没有表或者版本不匹配,将抛出异常。(生产环境常用)
true: activiti会对数据库中所有表进行更新操作。如果表不存在,则自动创建。(开发时常用)
create_drop: 在activiti启动时创建表,在关闭时删除表(必须手动关闭引擎,才能删除表)。(单元测试常用)
drop-create: 在activiti启动时删除原来的旧表,然后在创建新表(不需要手动关闭引擎)。
-->
<property name="databaseSchemaUpdate" value="true"/>
<property name="history" value="none"/>
</bean>
<!-- Activiti processEngine -->
<bean id="processEngine" class="org.activiti.spring.ProcessEngineFactoryBean">
<property name="processEngineConfiguration" ref="processEngineConfiguration"/>
</bean>
<!-- Activiti Service -->
<bean id="repositoryService" factory-bean="processEngine" factory-method="getRepositoryService"/>
<bean id="runtimeService" factory-bean="processEngine" factory-method="getRuntimeService"/>
<bean id="taskService" factory-bean="processEngine" factory-method="getTaskService"/>
<bean id="historyService" factory-bean="processEngine" factory-method="getHistoryService"/>
<bean id="managementService" factory-bean="processEngine" factory-method="getManagementService"/>
</beans>
/**
* @author: guanjian
* @date: 2020/11/25 19:44
* @description: 工作流常量
*/
public class ActivitiConstants {
private final static String BPMN_DEPLOY_PATH_TEMPLATE = "bpmn/*.bpmn";
/**
* 部署资源
*/
public static class Deployment {
//BPMN流程定义名称
public final static String WORK_FLOW_KEY = "xxx_workflow";
//BPMN流程版本号(yyyyMMddHHmm)
public final static String WORK_FLOW_VERSION = "v202012031347";
//BPMN流程部署名称(名称_版本号)
//每次部署对该变量进行防重判断,版本号变更则部署新流程进行新部署
public final static String WORK_FLOW_DEPLOY = WORK_FLOW_KEY + Constants.Symbol.LINE + WORK_FLOW_VERSION;
//BPMN文件读取路径
public final static String BPMN_RESOURCE_PATH = BPMN_DEPLOY_PATH_TEMPLATE.replace("*", WORK_FLOW_KEY);
}
/**
* 工作流节点定义
*/
public enum WorkFlowNodeEnum implements SingleItem<String> {
/**
* 起始
*/
//开始
START("START"),
//结束
END("END"),
/**
* 普通节点
*/
NODE_1("NODE_1"),
NODE_2("NODE_2"),
NODE_3("NODE_3"),
/**
* 排他网关
*/
//数据源
XOR_DATA_SOURCE("XOR_dataSource"),
//反馈结果
XOR_CHSI_RESULT("XOR_chsiResult"),
;
private String key;
WorkFlowNodeEnum(String key) {
this.key = key;
}
@Override
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}
/**
* 流程变量
*/
public static class Variables {
//用户ID
public final static String USER_ID = "userId";
//业务ID
public final static String BIZ_ID = "bizId";
//数据源
public final static String DATASOURCE = "dataSource";
//反馈结果
public final static String RESULT= "result";
}
/**
* 任务取消原因
*/
public static class CancelReason {
//强制作废流程
public final static FINISH_FORCE_INVALID = "FINISH_FORCE_INVALID";
}
/**
* 任务操作枚举
*/
public enum TaskOperateEnum {
//任务启动
START,
//任务取消
CANCEL,
//任务完成
COMPLETE,
;
}
}
@WorkFlowHandle是支持工作流操作的注解,@WorkFlowHandles则是借助JDK1.8的@Repeatable特性支持重复注解,使得@WorkFlowHandle可以重复作用在业务Service方法上进行工作流操作。
@WorkFlowHandle的定义如下:
注解字段 | 含义及作用 |
---|---|
assignee | 任务委派人。工作流的流转节点需要指派任务委托人进行处理,这里用来指定任务由谁来处理 |
bizId | 业务ID。业务数据的唯一ID,以此来关联工作流数据做绑定,说白了就是通过bizId可以找到业务数据和工作流数据的对应关系,是二者交互的桥梁,bizId必须是全局唯一的 |
variables | 环境变量。Activiti工作流中支持了任务实例流转过程中任务变量的存储和绑定,这些环境变量可以用来进行工作流路由的决策和驱动工作流流转,Activiti支持局部变量和全局变量两种形式,一般我们使用全局变量 |
taskOperate | 任务操作类型。这里是根据我们业务情况定义的工作流具体操作,一般分为任务启动、任务完成、任务取消 |
cancelReason | 任务取消原因。仅在任务取消时使用,标记取消原因 |
node | 任务节点标记。相当于对当前任务节点的标记,更新和取消任务都要明确传入当前任务节点,当传入任务节点与实际当前任务节点不符时会抛出异常 |
这里的注解字段定义可以根据具体业务进行抽象和具体,以上仅供参考。
/**
* @author: guanjian
* @date: 2020/12/02 17:14
* @description: 工作流处理
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(WorkFlowHandles.class)
public @interface WorkFlowHandle {
/**
* 任务委托人
*/
String assignee() default "";
/**
* 业务ID
*/
String bizId() default "";
/**
* 环境变量
*/
String variables() default "";
/**
* 任务操作
*/
ActivitiConstants.TaskOperateEnum taskOperate();
/**
* 取消原因
*/
String cancelReason() default "";
/**
* 任务节点
*/
ActivitiConstants.WorkFlowNodeEnum node() default ActivitiConstants.WorkFlowNodeEnum.END;
}
/**
* @author: guanjian
* @date: 2020/12/02 17:14
* @description: 工作流处理
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface WorkFlowHandles {
WorkFlowHandle[] value();
}
WorkFlowAspect是以切面形式存在的工作流业务,把涉及工作流相关的元素数据抽离到切面中进行集中处理,通过注册与业务逻辑进行融合,代码隔离减少了逻辑耦合和混杂,通过以下脚手架的拼接完成了该功能的实现,汇总如下:
元素 | 功能 | 解决问题 |
---|---|---|
Aop | 切面 | 抽离工作流业务逻辑,与业务代码绝缘,避免逻辑耦合 |
@Repeatable | 可重复注解 | 单一方法可重复注解,扩展性更高。例如,可单独启动任务、启动并完成任务、启动并完成任务并取消其他任务等 |
Spel | 表达式解析 | 通过强大的解析器Context环境进行Annotation注解参数注入并解析,支持Map类型数据结构复杂传参,解决了业务方法入参数据与切面业务传递可共享 |
TransactionTemplate | 事务支持 | 由于切面包裹业务方法,同时监听业务方法和切面方法逻辑,实现数据的原子操作,天然的支持事务,保证数据和业务逻辑的完整一致性 |
/**
* @author: guanjian
* @date: 2020/12/02 17:23
* @description: 工作流处理切面
*/
@Aspect
@Component("workFlowHandleAspect")
public class WorkFlowHandleAspect extends BaseAspect {
private final static Logger LOGGER = LoggerFactory.getLogger(WorkFlowHandleAspect.class);
@Resource
private WorkFlowService workFlowService;
@Resource(name = "routerTransactionTemplate")
private TransactionTemplate transactionTemplate;
@Resource
private BaseDBRouter dbRouter;
@Around("@annotation(WorkFlowHandle) || @annotation(WorkFlowHandles)")
public Object around(ProceedingJoinPoint pjp) {
try {
WorkFlowHandle[] workFlowHandles = getDeclaredMethod(pjp).getAnnotationsByType(WorkFlowHandle.class);
dbRouter.doRouter(parseAssignee(workFlowHandles[0],initContext(pjp)));
return transactionTemplate.execute(transactionStatus -> {
try {
doTransaction(pjp);
return Result.success();
} catch (Exception e) {
LOGGER.error("workflow transaction error, trigger rollback.", e);
transactionStatus.setRollbackOnly();
return Result.unknowError();
}
});
} finally {
dbRouter.clear();
}
}
private void doTransaction(ProceedingJoinPoint pjp) {
doWorkFlow(pjp);
doTarget(pjp);
}
/**
* 执行工作流
*
* @param pjp
* @return
*/
private void doWorkFlow(ProceedingJoinPoint pjp) {
EvaluationContext context = initContext(pjp);
WorkFlowHandle[] workFlowHandles = getDeclaredMethod(pjp).getAnnotationsByType(WorkFlowHandle.class);
Stream.of(workFlowHandles).forEach(workFlowHandle -> {
doWorkFlowTaskHandler(workFlowHandle, context);
});
}
/**
* 执行工作流任务
*
* @param annotation
* @param context
* @return
*/
private void doWorkFlowTaskHandler(WorkFlowHandle annotation, EvaluationContext context) {
Map<String, Object> variablesMap = parseVariables(annotation, context);
String bizId = parseBizId(annotation, context);
String assignee = parseAssignee(annotation, context);
String cancelReason = parseCancelReason(annotation, context);
switch (parseTaskOperate(annotation)) {
//任务启动
case START:
variablesMap.put(ActivitiConstants.Variables.BIZ_ID, bizId);
Response<ProcessInstance> startRes = workFlowService.startTask(
ActivitiConstants.Deployment.STU_APPLY_WORK_FLOW_KEY,
variablesMap
);
LOGGER.info("流程KEY={},流程变量={},实例ID={},流程操作=startTask,流程结果={}",
ActivitiConstants.Deployment.STU_APPLY_WORK_FLOW_KEY,
JSON.toJSONString(variablesMap),
startRes.getData().getProcessDefinitionId(),
JSON.toJSONString(startRes.getResult()));
Result.assertSuccess(startRes.getResult(), "startTask failed.");
break;
//任务完成
case COMPLETE:
ActivitiConstants.WorkFlowNodeEnum node = parseNode(annotation);
Assert.notNull(node, "node can not be null when complete task.");
Map<String, Object> taskQry = Maps.newHashMap();
taskQry.put(ActivitiConstants.Variables.BIZ_ID, bizId);
Result completeRes = workFlowService.completeTaskByTaskDefinitionKey(
assignee,
node.getKey(),
variablesMap,
taskQry
);
LOGGER.info("流程KEY={},流程节点={},业务ID={},流程操作=completeTaskByTaskDefinitionKey,流程结果={}",
ActivitiConstants.Deployment.STU_APPLY_WORK_FLOW_KEY,
node.getKey(),
bizId,
JSON.toJSONString(completeRes));
Result.assertSuccessOrNoChange(completeRes, "completeTaskByTaskDefinitionKey failed.");
break;
//任务取消
case CANCEL:
Map<String, Object> taskQryVar = Maps.newHashMap();
//不指定bizId则按assignee委派人取消所有任务
if (!StringUtils.isEmpty(bizId)) {
taskQryVar.put(ActivitiConstants.Variables.BIZ_ID, bizId);
}
Result cancelRes = workFlowService.cancelTasksByAssignee(
assignee,
taskQryVar,
cancelReason
);
LOGGER.info("流程KEY={},流程操作=cancelTasksByAssignee,流程结果={}",
ActivitiConstants.Deployment.STU_APPLY_WORK_FLOW_KEY,
JSON.toJSONString(cancelRes));
Result.assertSuccessOrNoChange(cancelRes, "cancelTasksByAssignee failed.");
break;
default:
throw new RuntimeException("taskOperate enum can not match.");
}
}
/**
* 执行目标类方法
*
* @param pjp
* @return
*/
private void doTarget(ProceedingJoinPoint pjp) {
Result result = null;
try {
result = (Result) pjp.proceed();
Result.assertSuccessOrNoChange(result);
} catch (Throwable throwable) {
throwable.printStackTrace();
result = Result.unknowError();
}
Result.assertSuccessOrNoChange(result, "target method failed.");
}
/**
* 环境初始化
*
* @param pjp
* @return
*/
private EvaluationContext initContext(ProceedingJoinPoint pjp) {
//获取到方法形参
String[] params = discoverer.getParameterNames(getDeclaredMethod(pjp));
//获取到方法实参
Object[] args = getArgs(pjp);
//构建spel的context
EvaluationContext context = new StandardEvaluationContext();
IntStream.range(0, params.length).forEach(index -> {
context.setVariable(params[index], args[index]);
});
return context;
}
/**
* 解析bizId
*
* @param annotation
* @param context
* @return
*/
private String parseBizId(WorkFlowHandle annotation, EvaluationContext context) {
String bizId = annotation.bizId();
if (StringUtils.isEmpty(bizId)) return "";
return parser.parseExpression(bizId).getValue(context, String.class);
}
/**
* 解析node
*
* @param annotation
* @return
*/
private ActivitiConstants.WorkFlowNodeEnum parseNode(WorkFlowHandle annotation) {
return annotation.node();
}
/**
* 解析cancelReason
*
* @param annotation
* @param context
* @return
*/
private String parseCancelReason(WorkFlowHandle annotation, EvaluationContext context) {
String cancelReason = annotation.cancelReason();
if (StringUtils.isEmpty(cancelReason)) return "";
return parser.parseExpression(cancelReason).getValue(context, String.class);
}
/**
* 解析assignee
*
* @param annotation
* @param context
* @return
*/
private String parseAssignee(WorkFlowHandle annotation, EvaluationContext context) {
String assignee = annotation.assignee();
return parser.parseExpression(assignee).getValue(context, String.class);
}
/**
* 解析taskOperate
*
* @param annotation
* @return
*/
private ActivitiConstants.TaskOperateEnum parseTaskOperate(WorkFlowHandle annotation) {
return annotation.taskOperate();
}
/**
* 解析variables
*
* @param annotation
* @param context
* @return
*/
private Map<String, Object> parseVariables(WorkFlowHandle annotation, EvaluationContext context) {
Map<String, Object> variablesMap = Maps.newHashMap();
String variables = annotation.variables();
if (StringUtils.isEmpty(variables)) return variablesMap;
//解析map eg:{name:'#req.pin',stuResult:'#req.stuResult'}
Map<String, Object> map = (Map) parser.parseExpression(variables).getValue();
//遍历map={name=#req.name, age=#req.age},把对应的value的形参赋值成实参值
map.forEach((k, spel) -> {
String value = parser.parseExpression(String.valueOf(spel)).getValue(context, String.class);
variablesMap.put(k, value);
});
return variablesMap;
}
}
@WorkFlowHandle(taskOperate = ActivitiConstants.TaskOperateEnum.START,
assignee = "#req.userId",
bizId = "#req.uuid",
variables = "{userId:'#req.userId',dataSource:'#req.dataSource'}"
)
@WorkFlowHandle(taskOperate = ActivitiConstants.TaskOperateEnum.COMPLETE,
assignee = "#req.userId",
bizId = "#req.uuid",
node = ActivitiConstants.WorkFlowNodeEnum.NODE_1
)
@WorkFlowHandle(taskOperate = ActivitiConstants.TaskOperateEnum.CANCEL,
assignee = "#req.userId"
)
@Override
public Result bizMethod(Req req) {
//业务操作
//do something
return Result.success();
}
由于存在外键,表删除需要按照以下顺序进行,第一遍没删除掉,可以再来一遍就干净了,测试环境使用的备记下
DROP TABLE IF EXISTS `ACT_RU_VARIABLE`;
DROP TABLE IF EXISTS `ACT_RU_EXECUTION`;
DROP TABLE IF EXISTS `ACT_RE_PROCDEF`;
DROP TABLE IF EXISTS `ACT_ID_GROUP`;
DROP TABLE IF EXISTS `ACT_GE_BYTEARRAY`;
DROP TABLE IF EXISTS `ACT_RE_DEPLOYMENT`;
DROP TABLE IF EXISTS `ACT_EVT_LOG`;
DROP TABLE IF EXISTS `ACT_GE_PROPERTY`;
DROP TABLE IF EXISTS `ACT_HI_ACTINST`;
DROP TABLE IF EXISTS `ACT_HI_ATTACHMENT`;
DROP TABLE IF EXISTS `ACT_HI_COMMENT`;
DROP TABLE IF EXISTS `ACT_HI_DETAIL`;
DROP TABLE IF EXISTS `ACT_HI_IDENTITYLINK`;
DROP TABLE IF EXISTS `ACT_HI_PROCINST`;
DROP TABLE IF EXISTS `ACT_HI_TASKINST`;
DROP TABLE IF EXISTS `ACT_HI_VARINST`;
DROP TABLE IF EXISTS `ACT_ID_INFO`;
DROP TABLE IF EXISTS `ACT_ID_MEMBERSHIP`;
DROP TABLE IF EXISTS `ACT_ID_USER`;
DROP TABLE IF EXISTS `ACT_PROCDEF_INFO`;
DROP TABLE IF EXISTS `ACT_RE_MODEL`;
DROP TABLE IF EXISTS `ACT_RU_EVENT_SUBSCR`;
DROP TABLE IF EXISTS `ACT_RU_IDENTITYLINK`;
DROP TABLE IF EXISTS `ACT_RU_JOB`;
DROP TABLE IF EXISTS `ACT_RU_TASK`;
https://blog.csdn.net/qq_40933428/article/details/92763639
http://www.mossle.com/docs/activiti/index.html
https://www.activiti.org/quick-start
https://gitee.com/jerryshensjf/ActivitiDemo/tree/master
https://blog.csdn.net/zxxz5201314/article/details/103202794
https://cloud.tencent.com/developer/article/1584828