Activiti工作流与业务整合实战
1. 业务背景
项目开发过程中如果均是简单的数据请求与返回,那么方法调用和业务逻辑是最容易处理的,根据入参返回数据即可,数据的生命周期始于请求,终于数据返回,没有其他。
倘若特定需求场景需要多个接口协作完成一件事,数据流转存在多路由,业务逻辑处理将会呈现复杂化,朴素的数据流控制方式就是定义数据中间态通过硬编码形式来影响数据流向,这种设计在复杂度不深的情况下总是很容易实现,开发成本和沟通成本较低,也不失为一种非常有效的开发设计方式。
而随着业务场景复杂化,流程变更频繁,开发人员会在之前得益的简单设计上发现维护和可拓展性极差,甚至陷入流程泥潭中难以自救,最直接的表现就是接口交互定制化,所有的交互看不到任何业务或故事主线,所有的服务交互都需要最原始的那些开发人员的文档、注释甚至“言传身教”的指导才可以洞察复杂业务的其中一二,这是软件开发中的技术负债和不完善,我们急需一个可以引导完整业务流程的体系或者框架来引导服务交互,来驱动业务数据流转,对数据的出生、中转、停留及最终消亡进行有效控制和监管,让服务有源可溯,有序可遵。关于以上概述都是为了引申出下面项目实践的利器,工作流。
数据流转依赖硬编码,面向接口交互,没有统一司令塔服务进行调度 面向业务数据设计,复杂逻辑的业务中,数据流故事主线不清晰,无法监控也无从溯源
2.技术调研
JBPM vs Activiti选型对比
关于工作流开源框架,一般有JBPM和Activiti,简单检索了下两者对比如下:
Activiti持久层通过MyBatis实现、与Spring融合支持事务,与当前项目技术背景较为符合,且上手较为便捷,参考资料广泛,学习成本较低,加上之前个人项目运用过Activiti前身,对PVM设计模式有一定了解,最终决定采用Activiti作为工作流来进行开发。
Activiti工作流特点
关于Activiti工作流的具体内容这里不做赘述,本文的核心放在Activiti工作流与业务结合的实践,下面是Activiti工作流的一些特点:
数据持久化 支持链式API编程风格,所有的编程参与对象都可通过ProcessEngine获取到 支持流程设计器。可以结合IDEA中的ActiBpm等插件进行可视化流程设计,它最终转换的是bpm文件,是一个类xml的流程配置文件 原生支持Spring 分离运行时与历史数据 Activiti是基于单库单表的持久化
3.流程设计
目前负责的项目是一个关于用户认证相关的业务,简单描述认证业务流程如下:
工作流起始 工作流的开始和结束,是整个工作流程的起点和重点
工作流节点 工作流的核心节点和衔接,每个节点是故事主线的主要构成部分,代表一个聚合的业务逻辑,每个节点根据预定义走向进行数据驱动
工作流路由 工作流路由分发,根据前置数据来进行工作流走向的决策,从而影响后续节点的流转
数据中间态 除了开始、结束两个节点,被虚线包括的部分都是数据的中间态,无论业务数据和工作流数据,此时呈现的最大特点就是数据的不稳定性,业务处理中随时都可能根据外部业务驱动产生数据和业务流程的向下继续、向上回溯、分叉决策等,也是工作流中最活跃的部分
4.架构设计
Service业务逻辑层
Activiti Service 封装了工作流基础API,这里主要用到Activiti的
RepositoryService
、RuntimeService
、TaskService
三个服务类,RepositoryService
提供了服务流程部署功能,RuntimeService
提供了运行时服务这里主要涉及任务启动,TaskService提供了任务完成、任务取消等流程驱动的方法支持。这里是基于Activiti原生API进行了定制化任务支持,类似一个门面服务,主要是用来融合Activiti原生服务和实际项目业务需求的APIWorkFlowAspect 通过Aop切面将工作流逻辑进行抽离,保证与业务层具体逻辑方法的隔离实现解耦;调用底层
Activiti Service
是提供的服务接口完成工作流驱动@WorkFlowHandle 通过Spel解析
Annotation
注解形参,支持Map类型复杂数据结构传参,结合Aop进行实参映射绑定,解决切面层与业务层参数传递问题;通过@Repeatable
支持业务方法的重复注解,为工作流作用业务方法的灵活配置的可扩展提供支持
Dao数据持久层
水平拆分 根据业务数据的库表设计进行工作流持久化改造,按照用户userId进行工作流数据水平拆分
事务支持 由于切分键一致,使得工作流数据与业务数据分离的同时能够支持数据库事务,保证数据完整一致性,减少开发复杂度
5.项目实战
5.1 maven配置
目前Activiti已进入7.0.0+,翻阅了大量网上资料,该版本输出的时间较少,大部分还集中在5或6,这里基于可用可操作的思想选用了介绍参考资料详实的5.22.0进行开发,每个大版本变更差异较大,其他版本根据实践需要进行升级或取舍
<activiti.version>5.22.0</activiti.version>
<!-- activiti -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-engine</artifactId>
<version>${activiti.version}</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring</artifactId>
<version>${activiti.version}</version>
</dependency>
5.2 持久化改造
分库分表
Activiti默认是单库单表的,而我们现有项目是分库分表的,路由策略是根据用户ID进行水平切分的。
对Activiti的持久化存储进行分库分表改造基于以下两点考虑:
业务数据分库分表,Activiti工作流数据的流转肯定是要和业务数据做联动的,按照同样语义即用户ID进行水平切换路由可以轻松实现数据库事务,减少实现数据一致性带来的设计问题。
业务表做了分库分表的原因就是用户体量大,持久化数据庞大,尽管读多写少且写入量单库单表在一段时间内不会对业务性能产生丝毫影响,但是为了扩展性更好,也为了规避后续变更难度,决定对Activiti工作流数据持久化也进行水平拆分。
字段及索引长度适配
为了保证数据完整性,Activiti默认创建表存在大量的外键约束,在生产环境下可以根据自身开发需要对这些外键进行去除,从而提高库表查询和处理性能。关于数据完整性可以通过应用程序层面进行保证和处理。
Activiti的库表设计都是基于可用性和普遍适配性进行设计的,个别字段如存在varchar(4000)
来记录异常信息,根据自身业务存储及性能考虑进行了部分字段基于可用性的缩减;还有部分表存在大量的联合索引,由于字段长度过长导致联合索引存储大于736字节,这是MySQL官方推荐的索引最大值,超过该值可能会产生性能问题,项目中没有对MySQL默认限制进行修改,而是根据项目情况缩减了联合索引字段的长度。
5.3 部署bpmn更新问题
在实际项目环境下,没有bpmn流程调整我们是不需要频繁进行bpmn流程部署的,每次新流程部署都会更新刷下流程ID、实例ID等,而且数据也会产生变更调整,而我们的需求是在需要更新的时候更新,不需要更新的时候复用即可。
因此在服务启动时,我这里通过时间戳作为版本号,每次启动Spring服务时进行bpmn部署检查,如果应用中的版本号未进行变更则不刷新Activiti部署信息保持之前部署快照,否则创建并部署新bpmn流程进行部署创建,更新部署快照信息,下面放一段demo代码,由于项目是分库分表的,每次启动服务会轮询每个分库的deploy信息情况。
/**
* @author: guanjian
* @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 <p>
* 1、根据分库数量(dbRouter.getDbNumber)进行bpmn流程定义部署
* 2、根据WORK_FLOW_DEPLOY进行部署,每次部署进行防重判断,变更WORK_FLOW_DEPLOY则重新部署即启用新流程
* WORK_FLOW_DEPLOY一致则不进行重新部署,复用原流程
* <p>
*/
@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());
}
}
5.4 Activiti核心服务
Activiti主要有以下几个核心服务,我们这里最重要的是使用到了RepositoryService
、RuntimeService
、TaskService
,RepositoryService
用来进行服务部署,RuntimeService
用来启动任务管理任务实例,TaskService
用来进行任务的查询、流转(完成、取消)等操作。推荐:Java面试题
5.5 整合Spring
与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>
5.6 工作流常量定义
/**
* @author: guanjian
* @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,
;
}
}
5.7 工作流注解@WorkFlowHandle & @WorkFlowHandles
@WorkFlowHandle
是支持工作流操作的注解,@WorkFlowHandles
则是借助JDK1.8的@Repeatable
特性支持重复注解,使得@WorkFlowHandle
可以重复作用在业务Service方法上进行工作流操作。推荐:Java面试题
@WorkFlowHandle
的定义如下:
这里的注解字段定义可以根据具体业务进行抽象和具体,以上仅供参考。
/**
* @author: guanjian
* @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
* @description: 工作流处理
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface WorkFlowHandles {
WorkFlowHandle[] value();
}
5.8 工作流切面 WorkFlowAspect
WorkFlowAspect
是以切面形式存在的工作流业务,把涉及工作流相关的元素数据抽离到切面中进行集中处理,通过注册与业务逻辑进行融合,代码隔离减少了逻辑耦合和混杂,通过以下脚手架的拼接完成了该功能的实现,汇总如下:
/**
* @author: guanjian
* @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;
}
}
5.9 工作流与业务Service融合
@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();
}
6. 其他
表删除顺序
由于存在外键,表删除需要按照以下顺序进行,第一遍没删除掉,可以再来一遍就干净了,测试环境使用的备记下
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`;
7. 参考资料
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
感谢阅读,希望对你有所帮助 :)
来源:blog.csdn.net/u013161278/article/details/110005489
END
往期精彩12 个超有用的 Java 开源系统 |建议收藏
Windows 10 将于 12月13日 停止服务!
Spring 中获取 Bean 的八种方式,你get了几种?
Spring Boot 整合 Groovy 脚本,实现动态编程
关注后端面试那些事,回复【2022面经】
获取最新大厂Java面经