Fork me on GitHub

Activiti(1)--工作流引擎

前段时间入职字节跳动, 目前负责 Lark 工作流审批功能的开发, 选用工作流引擎 Activiti 进行开发, 因此在此记录下对 Activiti 的学习过程.

概念

工作流引擎是用来驱动业务, 按照流程图次逐步流转的核心框架, 在复杂多变的场景下采用工作流引擎可以大大降低业务部署成本. 通过标准的业务流程模型作为业务与开发工作的桥梁, 有效减少业务团队与技术交流的障碍.

工作流引擎最早用于企业 OA, CRM, 流程审批等系统的流程审批.
现在的工作流引擎已经大量运用到互联网电商, 金融出行, 中台支撑等.

工作流引擎在互联网公司快速盛行, 掌握工作流引擎技术可以提升技术架构和业务建模能力.

目录:

  • 工作流入门
  • Activiti 6.0 源码浅析
  • Activiti 6.0 引擎配置
  • Activiti 6.0 核心 API
  • 数据设计与模型映射
  • BPMN 2.0 规范
  • 集成 SpringBoot 2.0
  • 搭建工作流平台

1. 工作流入门

1.1 工作流介绍

1.1.1 出差流程

审批业务场景:

审批流程模型化:

从一个开始节点, 经过多个任务节点和分支节点, 最终流向结束节点.

1.1.2 电商购物流程

抽象成泳道图:

节点 抽象名称 功能
电商购物流程 泳池(Pool)
用户/电商平台/仓储物流 泳道(Line)
校验库存 服务任务(Service Task) 不需要人工参与, 需要系统自动化完成的操作节点

1.1.3 工作流是什么

工作流:

是对工作流程以及各个步骤之间的业务规则的抽象, 概括描述.

工作流建模:

将工作流程中的工作如何前后组织在一起的逻辑和规则, 在计算机中以恰当的模型表达并对其实施计算.

要解决的问题:

为实现某个业务目标, 利用计算机在多个参与者之间按某种预定规则自动传递文档, 信息或任务.

关键词 概念
工作流管理系统 处理工作流的电脑软件系统, 主要功能是通过计算机技术的支持去定义, 执行和管理工作流, 协调工作流执行过程中工作之间以及群体之间的信息交互
计算机支持的协同工作 研究一个群体如何在计算机的帮助下实现协同工作的, 工作流属于计算机支持的协同工作的一部分
工作流管理联盟 工作流技术标准化的工业组织, 发布了用于工作流管理系统之间互操作的工作流参考模型, 并相继制定了一些列工业标准

1.1.4 为什么需要工作流

日常开发中经常遇到的问题:

  1. 产品需求遗漏, 开发上线之后需求经常改;
  2. 业务代码复杂, 开发时间紧迫;
  3. 代码后期维护不足, 逐渐难以维护.

使用工作流能够带来的改变:

  1. 可以快速响应, 灵活调整线上产品流程;
  2. 业务和开发基于流程模型沟通, 基于业务建模快速部署;
  3. 流程可视化, 方便查看流程的运行进展.

使用工作流对团队的作用:

  1. 提高效率, 减少等待;
  2. 规范行为, 落实制度;
  3. 协同内外, 快速响应;
  4. 监控全面, 提升执行.

1.2 工作流技术选型

二者都是成熟的工作流框架

jBPM Activiti
Hibernate ByBatis
Drools Flow JBPM4
JPA Spring
Message RESTful

1.3 Activiti6.0 快速体验

1.3.1 准备物料

  • Activiti 软件包: activiti-6.0.0.zip
  • JDK
  • Servlet 容器 (如 Tomcat)

安装 sdkman

1
2
3
4
5
curl -s "https://get.sdkman.io" | bash

source "$HOME/.sdkman/bin/sdkman-init.sh"

sdk version

安装 JDK

1
2
3
4
5
sdk install java 8u161-oracle

java -version

echo $JAVA_HOME

安装 Tomcat

1
2
3
4
5
6
7
wget http://mirror.bit.edu.cn/apache/tomcat/tomcat/8/v8.0.50/bin/apache-tomcat-8.0.50.zip

tar -zxvf apache-tomcat-8.0.50.zip

./apache-tomcat-8.0.50/bin/startup.sh

jdp-mlv

部署 Activiti

1
2
3
4
5
6
wget https://github.com/Activiti/Activiti/releases/download/activiti-6.0.0/activiti-6.0.0.zip

tar -zxvf activiti-6.0.0.zip

cp activiti-6.0.0/wars/activiti-app.war apache-tomcat-8.0.50/webapps
cp activiti-6.0.0/wars/activiti-admin.war apache-tomcat-8.0.50/webapps

此时打开浏览器, 输入 http://localhost:8080/activiti-app 即可进入流程引擎的登录界面

账号: admin
密码: test

1.3.2 设计一个审批流程

设计如下流程:

开始 -> TL 审批 -> HR 审批 -> 结束

流程参与者

ID Email Name
admin admin Administrator
userdev userdev@126.com userdevDEV
userhr userhr@126.com userhrHR
usertl usertl@126.com usertlTL

2. 源码概述

1
2
3
4
5
git clone git@github.com:DestinyWang/Activiti.git

git checkout -b study6 activiti-6.0.0

mvn clean test-compile
路径 功能
Activiti/activiti-engine/src/main/java/org/activiti/engine/cfg Activiti 的启动依赖 activiti.cfg.xml, 在该目录完成
Activiti/activiti-engine/src/main/java/org/activiti/engine/compatibility Activiti 从 5 升级到 6 的时候有部分不兼容, 在该目录完成适配
Activiti/activiti-engine/src/main/java/org/activiti/engine/debug 调试相关目录
Activiti/activiti-engine/src/main/java/org/activiti/engine/delegate 需要制定的节点 Task 都需要实现 JavaDelegate
Activiti/activiti-engine/src/main/java/org/activiti/engine/event 定义了事件和监听机制
Activiti/activiti-engine/src/main/java/org/activiti/engine/form 通用表单
Activiti/activiti-engine/src/main/java/org/activiti/engine/history 历史数据归档
Activiti/activiti-engine/src/main/java/org/activiti/engine/identity 身份认证相关操作
Activiti/activiti-engine/src/main/java/org/activiti/engine/impl 各个接口层的实现
Activiti/activiti-engine/src/main/java/org/activiti/engine/logging LogMDC 将重要的变量(如流程 id 放在上下文, logback 可以打印出来)
Activiti/activiti-engine/src/main/java/org/activiti/engine/management 管理相关
Activiti/activiti-engine/src/main/java/org/activiti/engine/parse 流程文件是 xml, 需要解析和验证
Activiti/activiti-engine/src/main/java/org/activiti/engine/query 抽象了一些查询接口, 基于 mybatis
Activiti/activiti-engine/src/main/java/org/activiti/engine/repository 抽象流程部署到数据库的过程
Activiti/activiti-engine/src/main/java/org/activiti/engine/runtime 与 history 相对应, 是流程在流转过程中的数据
Activiti/activiti-engine/src/main/java/org/activiti/engine/task 每个流程在需要人工处理的时候都会对应一个 task
Activiti/activiti-engine/src/main/java/org/activiti/engine/test 支持集成测试的帮助类

核心模块:

  • module/activiti-engine: 核心引擎
  • module/activiti-spring: Spring 集成模块
  • module/activiti-spring-boot: SpringBoot 集成模块
  • module/activiti-rest: 对外提供 rest api 模块
  • module/activiti-form-engine: 表单引擎模块
  • module/activiti-ldap: 集成 ldap 用户模块

Activiti-engine 依赖的模块:

  • bpmn-converter: 模型转换
  • process-validation: 流程校验
  • image-generator: 流程图绘制(BPMN 转 PNG)
  • dmn-api: 决策标准
  • form-api/form-model: form 表单相关

2.1 基于源码运行 activiti-app

2.1.1 启动 activiti-app

1
2
3
4
5
cd modules/activiti-ui/activiti-app

mvn clean tomcat7:run

open http://localhost:9999/activi-app

2.2 剖析 activiti-app

activiti-ui 的组成:

  • activiti-app: 集成发布的 war 工程
  • activiti-app-conf: UI 独立域业务外的配置
  • activiti-app-logic: UI 的业务逻辑
  • activiti-app-rest: 提供接口的 rest api

3. HelloWorld

image

  • 填写审批信息: 姓名, 时间, 是否提交
  • 主管审批: 审批结果, 备注
  • 审批结果, 备注

3.1 在 IDEA 中完成流程图的设计并配置

image

配置点:

  1. 节点 id, 名称;
  2. 对每个网关的分支做判断(基于填写信息);
  3. Task 节点接收的表单信息.

3.1.2 Task 节点接收的表单信息

填写申请信息

Id Name Type Expression Variable Default Date Pattern Readable Writable Required Values
message 申请信息 string True
name 申请人姓名 string True
submitTime 提交时间 date yyyy-MM-dd True
submitType 确认申请 string True

主管审批

Id Name Type Expression Variable Default Date Pattern Readable Writable Required Values
tlApprove 主管审批结果 string false
tlMessage 主管审批备注 string true

人事审批

Id Name Type Expression Variable Default Date Pattern Readable Writable Required Values
hrApprove 人事审批结果 string true
hrMessage 人事审批备注 string true

3.1.3 排他网关配置

排他网关需要对流入网关的某个值做判断, 从而决定流程后续的流向

flow3 配置

${submitType=="Y" || submitType=="y"}

flow4 配置

${submitType=="N" || submitType=="n"}

flow6 配置

${tlApprove == "Y" || tlApprove == "y"}

flow7 配置

${tlApprove == "N" || tlApprove == "n"}

flow9 配置

${hrApprove == "Y" || tlApprove == "y"}

flow10 配置

${hrApprove == "N" || tlApprove == "n"}

配置后的流程图

image

3.2 helloworld程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
public class DemoMain {

private static final Logger logger = LoggerFactory.getLogger(DemoMain.class);

public static void main(String[] args) throws ParseException {
logger.info("----- 启动我们的程序 -----");
// 1. 创建流程引擎
ProcessEngine processEngine = getProcessEngine();

// 2. 部署流程定义文件
ProcessDefinition processDefinition = getProcessDefinition(processEngine);

// 3. 启动运行流程
ProcessInstance processInstance = getProcessInstance(processEngine, processDefinition);

// 4. 处理流程任务
processTask(processEngine, processInstance);
logger.info("----- 结束我们的程序 -----");
}

/**
* 处理流程任务
*
* @param processEngine 流程引擎
* @param processInstance 流程实例
* @throws ParseException
*/
private static void processTask(ProcessEngine processEngine, ProcessInstance processInstance) throws ParseException {
Scanner scanner = new Scanner(System.in);
while (processInstance != null && !processInstance.isEnded()) {
logger.info("processInstanceId: [{}]", processInstance.getId());
logger.info("processInstance.processInstanceId: [{}]", processInstance.getProcessInstanceId());
TaskService taskService = processEngine.getTaskService();
List<Task> list = taskService.createTaskQuery().list();
logger.info("待处理任务数量: [{}]", list.size());
for (Task task : list) {
logger.info("待处理任务: [{}]", task.getName());
Map<String, Object> variables = getVariables(processEngine, scanner, task);
taskService.complete(task.getId(), variables);
processInstance = processEngine.getRuntimeService()
.createProcessInstanceQuery()
.processInstanceId(processInstance.getId())
.singleResult();
logger.info("当前 ProcessInstance :{}", processInstance);
}
}
scanner.close();
}

/**
* 获取变量
*
* @param processEngine
* @param scanner
* @param task
* @return
* @throws ParseException
*/
private static Map<String, Object> getVariables(ProcessEngine processEngine, Scanner scanner, Task task) throws ParseException {
FormService formService = processEngine.getFormService();
TaskFormData taskFormData = formService.getTaskFormData(task.getId());
List<FormProperty> formProperties = taskFormData.getFormProperties();
Map<String, Object> variables = Maps.newHashMap();
for (FormProperty property : formProperties) {
String line = null;
if (StringFormType.class.isInstance(property.getType())) {
// 如果是 String 类型, 不需要做任何格式化
logger.info("请输入 [{}] ?", property.getName());
line = scanner.nextLine();
variables.put(property.getId(), line);
} else if (DateFormType.class.isInstance(property.getType())) {
// 如果是日期类型
logger.info("请输入 [{}] ?, 格式 (yyyy-MM-dd)", property.getName());
line = scanner.nextLine();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date date = simpleDateFormat.parse(line);
variables.put(property.getId(), date);
} else {
logger.info("类型不支持: {}", property.getType());
}
logger.info("您输入的内容是 [{}]", line);
}
return variables;
}

private static ProcessInstance getProcessInstance(ProcessEngine processEngine, ProcessDefinition processDefinition) {
RuntimeService runtimeService = processEngine.getRuntimeService();
ProcessInstance processInstance = runtimeService.startProcessInstanceById(processDefinition.getId());
logger.info("启动流程: [{}]", processInstance.getProcessDefinitionKey());
return processInstance;
}

private static ProcessDefinition getProcessDefinition(ProcessEngine processEngine) {
RepositoryService repositoryService = processEngine.getRepositoryService();
DeploymentBuilder deploymentBuilder = repositoryService.createDeployment();
deploymentBuilder.addClasspathResource("SecondApprove.bpmn20.xml");
Deployment deployment = deploymentBuilder.deploy();

String deploymentId = deployment.getId();
// deploymentId: 1
logger.info("deploymentId: [{}]", deploymentId);
ProcessDefinition processDefinition = repositoryService.
createProcessDefinitionQuery().
deploymentId(deploymentId)
.singleResult();

// processDefinition.getId() 是 SecondApprove:1:4, 根据部署 id 和流程 id 组装出的数据
logger.info("流程定义文件: [{}], 流程 id: [{}]", processDefinition.getName(), processDefinition.getId());
return processDefinition;
}

private static ProcessEngine getProcessEngine() {
ProcessEngineConfiguration configuration = ProcessEngineConfiguration.createStandaloneInMemProcessEngineConfiguration();
ProcessEngine processEngine = configuration.buildProcessEngine();
String name = processEngine.getName();
String version = ProcessEngine.VERSION;

logger.info("流程引擎名称: [{}], 版本: [{}]", name, version);
return processEngine;
}

}

4. Activiti 引擎配置

4.1 流程引擎配置

流程引擎配置的载体就是 ProcessEngineConfiguration 及其子类, Activiti 是通过 activiti.cfg.xml 来完成配置

然后构建出流程引擎 ProcessEngine, 最终获取业务开发中需要的各个 Service.

image

ProcessorEngineConfiguration:

  1. 查找并解析 XML 配置文件 activiti.cfg.xml
  2. 提供多个静态方法创建配置对象
  3. 实现几个基于不同场景的子类, 配置方式灵活
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<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.xsd">
<!-- 配置 ProcessEngineConfiguration -->
<bean id="processEngineConfiguration" class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration">
<!-- 配置数据库连接 -->
<property name="jdbcDriver" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl"
value="jdbc:mysql://localhost:3306/activitiDB?createDatabaseIfNotExist=true&amp;useUnicode=true&amp;characterEncoding=utf8"/>
<property name="jdbcUsername" value="root"/>
<property name="jdbcPassword" value="root"/>

<!-- false: 不会自动创建表, 没有表, 则抛异常 -->
<!-- create-drop: 先删除, 再创建表 -->
<!-- true: 没有表时,自动创建-->
<property name="databaseSchemaUpdate" value="true"/>
</bean>
</beans>

ProcessEngineConfigurationImpl: 抽象类, 配置了 ProcessEngineConfiguration 大部分属性;
StandaloneProcessEngineConfiguration: 独立部署运行, 可以通过 new 的方式创建;
SpringProcessEngineConfiguration: 完成与 Spring 的集成, 同时扩展了数据源配置, 事务, 自动装载部署文件的目录.

4.2 数据库配置

  • 缺省配置默认使用 H2 内存数据库;
  • 配置 JDBC 属性, 使用 MyBatis 提供的连接池;
  • 配置 DataSource, 可自选第三方实现.

配置 JDBC 属性使用 MyBatis 提供的连接池

基本属性 连接池配置
jdbcUrl jdbcMaxActiveConnections(最大活跃连接数)
jdbcDriver jdbcMaxIdleConnections(最大空闲连接数)
jdbcUsername jdbcMaxCheckoutTime(最大)
jdbcPassword jdbcMaxWaitTIme(最大等待时间)

配置第三方实现的 DataSource

  • Druid: 为监控而生的数据库连接池
  • Dbcp: Tomcat 自带
  • HikariCP: 极速数据源连接池, Spring 默认

4.2.1 Druid 数据源连接池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<bean id="processEngineConfiguration" class="org.activiti.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">
<!-- 配置数据库连接 -->
<property name="dataSource" ref="dataSource"/>
<!-- false: 不会自动创建表, 没有表, 则抛异常 -->
<!-- create-drop: 先删除, 再创建表 -->
<!-- true: 没有表时,自动创建-->
<property name="databaseSchemaUpdate" value="true"/>
</bean>

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/activitiDB"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
<property name="initialSize" value="1"/>
<property name="maxActive" value="20"/>
<property name="filters" value="stat,slf4f"/>
</bean>

4.2.2 数据库更新策略

  1. 配置 databaseSchemaUpdate:
    • false: 启动时检查数据库版本, 发生不匹配则抛出异常(线上默认)
    • true: 启动时自动检查并更新数据库表, 不存在会创建(开发环境默认)
    • create-drop: 启动时创建数据库表结构, 结束时删除表结构

4.3 日志和数据记录配置

4.3.1 日志组件的关系及 MDC

日志分类 描述 分类内容
日志门面 直接应用在程序中记录日志的组件 slf4j, commons-logging, log4j
日志实现 日志门面不能直接打日志, 需要日志实现 logback, log4j, log4j2, Java util logging
桥接方式 有些特殊需求, 例如需要 slf4j 作为门面, 但需要以 log4j 作为实现 slf4j-log4j12, slf4j-jdk14, …
改变依赖 将原有门面的功能委托给其他实现, 主要用于解决历史软件内部依赖的改变 jcl-over-slf4j, log4j-over-slf4j, …

配置开启 MDC(Mapped Diagnostic Context): 可以理解为将上下文数据存储在 ThreadLocal 中

  • 默认没有开启, 需要手动设置 LogMDC.setMDCEnable(true)
  • 配置 logback.xml, 日志模板添加 %X{mdcProcessInstanceID}, 即打印当前 instance 的 id
  • 流程只有在执行过程出现异常的时候才会记录 MDC 信息

4.3.1.1 默认日志输出

测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ConfigMDCTest {

/**
* 自动构建 ProcessEngine
*/
@Rule
public ActivitiRule activitiRule = new ActivitiRule();

@Test
@Deployment(resources = {"my-process.bpmn20.xml"})
public void test() {
ProcessInstance processInstance = activitiRule.getRuntimeService().startProcessInstanceByKey("my-process");
assertNotNull(processInstance);
Task task = activitiRule.getTaskService().createTaskQuery().singleResult();
assertEquals("Activiti is awesome!", task.getName());
activitiRule.getTaskService().complete(task.getId());
}
}
logback.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="utf-8" ?>
<configuration debug="false" scan="true" scanPeriod="30 seconds">
<property name="log.dir" value="target/logs"/>
<property name="encoding" value="UTF-8"/>
<property name="plain" value="%msg%n"/>
<property name="std" value="%d{HH:mm:ss.SSS} [%thread] [%level] %msg %X{user} %logger{10}.%M:%L%n"/>
<property name="normal" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{10}.%M:%L - %msg%n"/>

<!-- 控制台输出 -->
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${std}</pattern>
<charset>${encoding}</charset>
</encoder>
</appender>

...

<root>
<appender-ref ref="stdout"/>
<appender-ref ref="file"/>
</root>
</configuration>
日志输出
00:19:43.624 [main] [INFO] Loading XML bean definitions from class path resource [activiti.cfg.xml]  o.s.b.f.x.XmlBeanDefinitionReader.loadBeanDefinitions:316
00:19:45.162 [main] [INFO] Activiti 5 compatibility handler implementation not found or error during instantiation : org.activiti.compatibility.DefaultActiviti5CompatibilityHandler. Activiti 5 backwards compatibility disabled.  o.a.e.c.DefaultActiviti5CompatibilityHandlerFactory.createActiviti5CompatibilityHandler:38
00:19:45.174 [main] [INFO] performing create on engine with resource org/activiti/db/create/activiti.mysql.create.engine.sql  o.a.e.i.d.DbSqlSession.executeSchemaResource:1147
00:19:45.176 [main] [INFO] Found MySQL: majorVersion=5 minorVersion=7  o.a.e.i.d.DbSqlSession.executeSchemaResource:1162
00:19:46.466 [main] [INFO] performing create on history with resource org/activiti/db/create/activiti.mysql.create.history.sql  o.a.e.i.d.DbSqlSession.executeSchemaResource:1147
00:19:46.466 [main] [INFO] Found MySQL: majorVersion=5 minorVersion=7  o.a.e.i.d.DbSqlSession.executeSchemaResource:1162
00:19:46.922 [main] [INFO] performing create on identity with resource org/activiti/db/create/activiti.mysql.create.identity.sql  o.a.e.i.d.DbSqlSession.executeSchemaResource:1147
00:19:46.922 [main] [INFO] Found MySQL: majorVersion=5 minorVersion=7  o.a.e.i.d.DbSqlSession.executeSchemaResource:1162
00:19:47.035 [main] [INFO] ProcessEngine default created  o.a.e.i.ProcessEngineImpl.<init>:87

4.3.1.2 MDC 日志输出

测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ConfigMDCTest {

@Rule
public ActivitiRule activitiRule = new ActivitiRule();

@Test
@Deployment(resources = {"my-process.bpmn20.xml"})
public void test() {
// 开启 MDC, 整个过程在正常情况下是不会激活 MDC 的
LogMDC.setMDCEnabled(true);
ProcessInstance processInstance = activitiRule.getRuntimeService().startProcessInstanceByKey("my-process");
assertNotNull(processInstance);
Task task = activitiRule.getTaskService().createTaskQuery().singleResult();
assertEquals("Activiti is awesome!", task.getName());
activitiRule.getTaskService().complete(task.getId());
}
}
BPMN 流程图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>

<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema"
expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
<process id="my-process">
<startEvent id="start"/>
<sequenceFlow id="flow1" sourceRef="start" targetRef="someTask"/>
<!-- org.destiny.activiti.delegate.MDCErrorDelegate 是一个会自动抛出异常 "test only" 的JavaDelegate -->
<serviceTask id="someTask" activiti:class="org.destiny.activiti.delegate.MDCErrorDelegate"/>
<sequenceFlow id="flow2" sourceRef="someTask" targetRef="end"/>
<endEvent id="end"/>
</process>
</definitions>
logback
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="utf-8" ?>
<configuration debug="false" scan="true" scanPeriod="30 seconds">
<property name="log.dir" value="target/logs"/>
<property name="encoding" value="UTF-8"/>
<property name="plain" value="%msg%n"/>
<property name="std" value="%d{HH:mm:ss.SSS} [%thread] [%level] %msg %X{user} %logger{10}.%M:%L%n"/>

<!--
- MDC 配置:
- ProcessDefinitionId: 流程定义 id
- executionId:
- mdcProcessInstanceId: 流程实例 id
- mdcBusinessKey: 业务 key
-->
<property name="mdc" value="%d{HH:mm:ss.SSS} [%thread] [%level] %msg ProcessDefinitionId=%X{mdcProcessDefinitionID}
executionId=%X{mdcExecutionId} mdcProcessInstanceId=%X{mdcProcessInstanceId} mdcBusinessKey=%X{mdcBusinessKey} %logger{10}.%M:%L%n"/>

<property name="normal" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{10}.%M:%L - %msg%n"/>

<!-- 控制台输出 -->
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- 此处将默认值输出由 std 改为 mdc -->
<!--<pattern>${std}</pattern>-->
<pattern>${mdc}</pattern>
<!--<pattern>${mdc}</pattern>-->
<charset>${encoding}</charset>
</encoder>
</appender>

...

<root>
<appender-ref ref="stdout"/>
<appender-ref ref="file"/>
</root>
</configuration>
日志输出
00:32:57.204 [main] [INFO] Loading XML bean definitions from class path resource [activiti.cfg.xml] ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.s.b.f.x.XmlBeanDefinitionReader.loadBeanDefinitions:316
00:32:58.659 [main] [INFO] Activiti 5 compatibility handler implementation not found or error during instantiation : org.activiti.compatibility.DefaultActiviti5CompatibilityHandler. Activiti 5 backwards compatibility disabled. ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.a.e.c.DefaultActiviti5CompatibilityHandlerFactory.createActiviti5CompatibilityHandler:38
00:32:58.671 [main] [INFO] performing create on engine with resource org/activiti/db/create/activiti.mysql.create.engine.sql ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.d.DbSqlSession.executeSchemaResource:1147
00:32:58.673 [main] [INFO] Found MySQL: majorVersion=5 minorVersion=7 ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.d.DbSqlSession.executeSchemaResource:1162
00:33:00.219 [main] [INFO] performing create on history with resource org/activiti/db/create/activiti.mysql.create.history.sql ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.d.DbSqlSession.executeSchemaResource:1147
00:33:00.220 [main] [INFO] Found MySQL: majorVersion=5 minorVersion=7 ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.d.DbSqlSession.executeSchemaResource:1162
00:33:00.657 [main] [INFO] performing create on identity with resource org/activiti/db/create/activiti.mysql.create.identity.sql ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.d.DbSqlSession.executeSchemaResource:1147
00:33:00.657 [main] [INFO] Found MySQL: majorVersion=5 minorVersion=7 ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.d.DbSqlSession.executeSchemaResource:1162
00:33:00.771 [main] [INFO] ProcessEngine default created ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.ProcessEngineImpl.<init>:87
00:33:00.932 [main] [INFO] MDCErrorDelegate.execute ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.d.MDCErrorDelegate.execute:24
00:33:00.935 [main] [ERROR] Error while closing command context ProcessDefinitionId=my-process:1:3     executionId=5 mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.i.CommandContext.logException:122
java.lang.RuntimeException: test only
...

可以看到, 在 ERROR 行中, 打印出了 MDC 信息

4.3.1.3 使用拦截器让每个流程节点都把 MDC 信息打印出来

新建拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MDCCommandInvoker extends DebugCommandInvoker {

/**
* 先判断可运行的对象是不是 Activiti 支持的 Operation
* 如果是, 将它强转, 并取出执行对象并输出
*
* @param runnable
*/
@Override
public void executeOperation(Runnable runnable) {
boolean mdcEnabled = LogMDC.isMDCEnabled();
LogMDC.setMDCEnabled(true);
if (runnable instanceof AbstractOperation) {
AbstractOperation operation = (AbstractOperation) runnable;
if (operation.getExecution() != null) {
// 如果是可操作对象, 将该信息放入 MDC 上下文对象
LogMDC.putMDCExecution(operation.getExecution());
}
}

super.executeOperation(runnable);
LogMDC.clear();
if (!mdcEnabled) {
// 如果 MDC 原本不生效, 需要将 MDC 重新置为 false
LogMDC.setMDCEnabled(false);
}
}
}
配置该拦截器

修改默认配置文件 activiti.cfg.xml, 新增该 MDCCommandInvoker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<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.xsd">
<!-- 配置 ProcessEngineConfiguration -->
<bean id="processEngineConfiguration" class="org.activiti.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">
<!-- 配置数据库连接 -->
<property name="jdbcDriver" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl"
value="jdbc:mysql://localhost:3306/activitiDB?createDatabaseIfNotExist=true&amp;useUnicode=true&amp;characterEncoding=utf8&amp;useSSL=false"/>
<property name="jdbcUsername" value="root"/>
<property name="jdbcPassword" value="123456"/>

<property name="databaseSchemaUpdate" value="true"/>
<property name="commandInvoker" ref="commandInvoker"/>
</bean>

<bean id="commandInvoker" class="org.destiny.activiti.interceptor.MDCCommandInvoker"/>
</beans>
最终产出日志
00:56:35.631 [main] [INFO] Loading XML bean definitions from class path resource [activiti_mdc.cfg.xml] ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.s.b.f.x.XmlBeanDefinitionReader.loadBeanDefinitions:316
00:56:37.141 [main] [INFO] Activiti 5 compatibility handler implementation not found or error during instantiation : org.activiti.compatibility.DefaultActiviti5CompatibilityHandler. Activiti 5 backwards compatibility disabled. ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.a.e.c.DefaultActiviti5CompatibilityHandlerFactory.createActiviti5CompatibilityHandler:38
00:56:37.153 [main] [INFO] performing create on engine with resource org/activiti/db/create/activiti.mysql.create.engine.sql ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.d.DbSqlSession.executeSchemaResource:1147
00:56:37.154 [main] [INFO] Found MySQL: majorVersion=5 minorVersion=7 ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.d.DbSqlSession.executeSchemaResource:1162
00:56:38.655 [main] [INFO] performing create on history with resource org/activiti/db/create/activiti.mysql.create.history.sql ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.d.DbSqlSession.executeSchemaResource:1147
00:56:38.656 [main] [INFO] Found MySQL: majorVersion=5 minorVersion=7 ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.d.DbSqlSession.executeSchemaResource:1162
00:56:39.109 [main] [INFO] performing create on identity with resource org/activiti/db/create/activiti.mysql.create.identity.sql ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.d.DbSqlSession.executeSchemaResource:1147
00:56:39.110 [main] [INFO] Found MySQL: majorVersion=5 minorVersion=7 ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.d.DbSqlSession.executeSchemaResource:1162
00:56:39.218 [main] [INFO] ProcessEngine default created ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.ProcessEngineImpl.<init>:87
00:56:39.403 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.ContinueProcessOperation : ProcessDefinitionId=my-process:1:3     executionId=5 mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.i.DebugCommandInvoker.executeOperation:33
00:56:39.407 [main] [INFO] 
4 (process instance)
└── 5 : start (StartEvent, parent id 4 (active)
 ProcessDefinitionId=my-process:1:3     executionId=5 mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.i.DebugCommandInvoker.executeOperation:34
00:56:39.409 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.TakeOutgoingSequenceFlowsOperation : ProcessDefinitionId=my-process:1:3     executionId=5 mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.i.DebugCommandInvoker.executeOperation:33
00:56:39.410 [main] [INFO] 
4 (process instance)
└── 5 : start (StartEvent, parent id 4 (active)
 ProcessDefinitionId=my-process:1:3     executionId=5 mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.i.DebugCommandInvoker.executeOperation:34
00:56:39.411 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.ContinueProcessOperation : ProcessDefinitionId=my-process:1:3     executionId=5 mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.i.DebugCommandInvoker.executeOperation:33
00:56:39.412 [main] [INFO] 
4 (process instance)
└── 5 : start -> someTask, parent id 4 (active)
 ProcessDefinitionId=my-process:1:3     executionId=5 mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.i.DebugCommandInvoker.executeOperation:34
00:56:39.412 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.ContinueProcessOperation : ProcessDefinitionId=my-process:1:3     executionId=5 mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.i.DebugCommandInvoker.executeOperation:33
00:56:39.412 [main] [INFO] 
4 (process instance)
└── 5 : someTask (UserTask, parent id 4 (active)
 ProcessDefinitionId=my-process:1:3     executionId=5 mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.i.DebugCommandInvoker.executeOperation:34
00:56:39.497 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.TriggerExecutionOperation : ProcessDefinitionId=my-process:1:3     executionId=5 mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.i.DebugCommandInvoker.executeOperation:33
00:56:39.501 [main] [INFO] 
4 (process instance)
└── 5 : someTask (UserTask, parent id 4 (active)
 ProcessDefinitionId=my-process:1:3     executionId=5 mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.i.DebugCommandInvoker.executeOperation:34
00:56:39.502 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.TakeOutgoingSequenceFlowsOperation : ProcessDefinitionId=my-process:1:3     executionId=5 mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.i.DebugCommandInvoker.executeOperation:33
00:56:39.503 [main] [INFO] 
4 (process instance)
└── 5 : someTask (UserTask, parent id 4 (active)
 ProcessDefinitionId=my-process:1:3     executionId=5 mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.i.DebugCommandInvoker.executeOperation:34
00:56:39.504 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.ContinueProcessOperation : ProcessDefinitionId=my-process:1:3     executionId=5 mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.i.DebugCommandInvoker.executeOperation:33
00:56:39.504 [main] [INFO] 
4 (process instance)
└── 5 : someTask -> end, parent id 4 (active)
 ProcessDefinitionId=my-process:1:3     executionId=5 mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.i.DebugCommandInvoker.executeOperation:34
00:56:39.505 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.ContinueProcessOperation : ProcessDefinitionId=my-process:1:3     executionId=5 mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.i.DebugCommandInvoker.executeOperation:33
00:56:39.505 [main] [INFO] 
4 (process instance)
└── 5 : end (EndEvent, parent id 4 (active)
 ProcessDefinitionId=my-process:1:3     executionId=5 mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.i.DebugCommandInvoker.executeOperation:34
00:56:39.505 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.TakeOutgoingSequenceFlowsOperation : ProcessDefinitionId=my-process:1:3     executionId=5 mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.i.DebugCommandInvoker.executeOperation:33
00:56:39.506 [main] [INFO] 
4 (process instance)
└── 5 : end (EndEvent, parent id 4 (active)
 ProcessDefinitionId=my-process:1:3     executionId=5 mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.i.DebugCommandInvoker.executeOperation:34
00:56:39.507 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.EndExecutionOperation : ProcessDefinitionId=my-process:1:3     executionId=5 mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.i.DebugCommandInvoker.executeOperation:33
00:56:39.507 [main] [INFO] 
4 (process instance)
└── 5 : end (EndEvent, parent id 4 (active)
 ProcessDefinitionId=my-process:1:3     executionId=5 mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.i.DebugCommandInvoker.executeOperation:34

可以看到最终所有级别的日志都输出了 MDC 信息

4.3.2 配置历史记录级别

配置 HistoryLevel:

  • none: 不记录历史记录, 性能高, 流程结束后不可读取;
  • activiti: 归档流程实例和活动实例, 流程变量不同步;
  • audit: 默认值, 在 activiti 基础上同步变量值, 保存表单属性;
  • full: 性能较差, 记录所有实例和变量细节变化.

4.3.3 配置基于 DB 的事件日志

配置 Event Logging

  • 实验性的事件记录机制, 性能影响较大;
  • 开启默认记录所有数据的变化过程, 导致表记录快速增长;
  • 日志内容基于 JSON 格式, 建议存入 MongoDB, Elastic Search;

4.4 命令拦截器的配置

4.4.1 命令模式与责任链模式

4.4.1.1 Command

命令拦截器使用命令模式实现, 多个拦截器会组成一个拦截器链, 实现了责任链模式

image

  • Command: 命令接口
  • ConcreteCommand: 命令实现, 构造命令的时候, 需要传入接受者, 即 Received
  • Receiver: Client 在实现 Command 接口的时候传入
  • Invoker: 调用者, 最终调用 ConcreteCommand 对象

image

4.4.1.2 Chain of Responsibility

image

customPre, default, customPost 中 execute() 的实现基本都是调用了 nextexecute(), 只有 CommandInvoker 真正完成了执行器.

  • CustomPre: default 之前的拦截器
  • default: Activiti 默认的 CommandInterceptor
  • CustomPost: default 之后的拦截器
  • CommandInvoker: 最终的命令执行者

4.4.2 命令拦截器的配置

  • 配置Interceptor
    • customProCommandInterceptors: 配置在默认拦截器之前
    • customPostCommandInterceptors: 配置在默认拦截器之后
    • commandInvoker: 配置在最后的执行器

4.4.3 示例

需求

实现一个可以统计所有命令完成时间的拦截器

拦截器实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class DurationCommandInterceptor extends AbstractCommandInterceptor {

private static final Logger logger = LoggerFactory.getLogger(DurationCommandInterceptor.class);

@Override
public <T> T execute(CommandConfig config, Command<T> command) {
// 记录当前时间
long start = System.currentTimeMillis();
try {
return this.getNext().execute(config, command);
} finally {
long duration = System.currentTimeMillis() - start;
logger.info("{} 执行时长: {} 毫秒", command.getClass().getSimpleName(), duration);
}
}
}

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<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.xsd">
<!-- 配置 ProcessEngineConfiguration -->
<bean id="processEngineConfiguration" class="org.activiti.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">
<property name="databaseSchemaUpdate" value="true"/>
<property name="commandInvoker" ref="commandInvoker"/>

<property name="customPreCommandInterceptors">
<list>
<bean class="org.destiny.activiti.interceptor.DurationCommandInterceptor"/>
</list>
</property>
</bean>

<bean id="commandInvoker" class="org.destiny.activiti.interceptor.MDCCommandInvoker"/>
</beans>

日志输出

10:10:09.371 [main] [INFO] SchemaOperationsProcessEngineBuild 执行时长: 113 毫秒 ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.i.DurationCommandInterceptor.execute:33
10:10:09.372 [main] [INFO] ProcessEngine default created ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.a.e.i.ProcessEngineImpl.<init>:87
10:10:09.391 [main] [INFO] ValidateExecutionRelatedEntityCountCfgCmd 执行时长: 14 毫秒 ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.i.DurationCommandInterceptor.execute:33
10:10:09.394 [main] [INFO]  执行时长: 1 毫秒 ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.i.DurationCommandInterceptor.execute:33
10:10:09.401 [main] [INFO] GetNextIdBlockCmd 执行时长: 4 毫秒 ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.i.DurationCommandInterceptor.execute:33
10:10:09.508 [main] [INFO] GetProcessDefinitionInfoCmd 执行时长: 2 毫秒 ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.i.DurationCommandInterceptor.execute:33
10:10:09.513 [main] [INFO] DeployCmd 执行时长: 116 毫秒 ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.i.DurationCommandInterceptor.execute:33
10:10:09.616 [main] [INFO] CompleteTaskCmd 执行时长: 22 毫秒 ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.i.DurationCommandInterceptor.execute:33
10:10:09.630 [main] [INFO] DeleteDeploymentCmd 执行时长: 14 毫秒 ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.i.DurationCommandInterceptor.execute:33

4.5 作业执行器 Job Executor

4.5.1 作业执行器的配置

  • asyncExecutorActivate: 激活作业执行器

  • asyncExecutorXXX: 异步执行器的属性配置

  • asyncExecutor: 异步执行器 bean

  • 定时开始事件

    • timeDate: 指定启动时间
    • timeDuration: 指定持续时间间隔后执行
    • timeCycle:R5/P1DT1H 指定时间段后周期执行

4.5.2 配置自定义线程池

  • corePoolSize: 核心线程数
  • maxPoolSize: 最大线程数
  • queueCapacity: 阻塞队列大小

如果核心线程数没满, 每当有一个任务, 不管原有线程是否空闲都会开启一个新的线程去执行, 直到达到核心线程数;
如果所有核心线程都在运行, 每当有一个任务, 会先放在阻塞队列等待, 直到核心线程执行完上一个任务, 会取阻塞队列第一个任务继续执行;
如果队列已满, 会继续创建新的线程, 直到达到最大线程数;
如果最大线程数和队列都已满, 此时会执行拒绝策略.

4.5.3 流程定义定时启动流程

4.5.4 Demo

配置异步执行器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<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.xsd">
<!-- 配置 ProcessEngineConfiguration -->
<bean id="processEngineConfiguration" class="org.activiti.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">
<property name="databaseSchemaUpdate" value="true"/>
<property name="commandInvoker" ref="commandInvoker"/>
<!-- 打开异步执行器 -->
<property name="asyncExecutorActivate" value="true"/>
<property name="asyncExecutor" value="asyncExecutor"/>
<!-- 事件监听 -->
<property name="eventListeners">
<list>
<bean class="org.destiny.activiti.listener.JobEventListener"/>
</list>
</property>
</bean>

<bean id="asyncExecutor" class="org.activiti.engine.impl.asyncexecutor.DefaultAsyncJobExecutor">
<property name="executorService" ref="executorService"/>
</bean>

<bean id="executorService" class="org.springframework.scheduling.concurrent.ThreadPoolExecutorFactoryBean">
<property name="threadNamePrefix" value="activiti-job-"/>
<property name="corePoolSize" value="5"/>
<property name="maxPoolSize" value="20"/>
<property name="queueCapacity" value="100"/>
<property name="rejectedExecutionHandler">
<bean class="java.util.concurrent.ThreadPoolExecutor$AbortPolicy"/>
</property>
</bean>

<bean id="commandInvoker" class="org.destiny.activiti.interceptor.MDCCommandInvoker"/>
</beans>

流程定义文件

1
2
3
4
5
6
7
8
9
10
11
12
13
<process id="my-process">
<!--<startEvent id="start"/>-->
<startEvent id="start">
<timerEventDefinition>
<!-- 每 10 秒执行一次, 共执行 5 次 -->
<timeCycle>R5/PT10S</timeCycle>
</timerEventDefinition>
</startEvent>
<sequenceFlow id="flow1" sourceRef="start" targetRef="someTask"/>
<userTask id="someTask" name="Activiti is awesome!"/>
<sequenceFlow id="flow2" sourceRef="someTask" targetRef="end"/>
<endEvent id="end"/>
</process>

添加监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class JobEventListener implements ActivitiEventListener {

public static final Logger logger = LoggerFactory.getLogger(JobEventListener.class);
@Override

public void onEvent(ActivitiEvent event) {
ActivitiEventType eventType = event.getType();
String name = eventType.name();

if (name.startsWith("TIMER") || name.startsWith("JOB")) {
logger.info("监听 Job 事件: {} \t {}", eventType, event.getProcessInstanceId());
}
}

@Override
public boolean isFailOnException() {
return false;
}
}

日志输出

11:34:50.048 [main] [INFO] 监听 Job 事件: TIMER_SCHEDULED      null ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.l.JobEventListener.onEvent:34
11:34:50.056 [main] [INFO] start ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.c.ConfigJobTest.test:33
11:34:50.080 [main] [INFO] 定时任务 TimerJobEntity [id=4], 默认重试次数: 3 ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.c.ConfigJobTest.test:36
11:34:50.080 [main] [INFO] jobList.size: 1 ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.c.ConfigJobTest.test:38
11:35:09.981 [activiti-job-1] [INFO] 监听 Job 事件: TIMER_FIRED      null ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.l.JobEventListener.onEvent:34
11:35:09.990 [activiti-job-1] [INFO] 监听 Job 事件: TIMER_SCHEDULED      null ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.l.JobEventListener.onEvent:34
11:35:09.990 [activiti-job-1] [INFO] 监听 Job 事件: JOB_EXECUTION_SUCCESS      null ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.l.JobEventListener.onEvent:34
11:35:19.950 [activiti-job-2] [INFO] 监听 Job 事件: TIMER_FIRED      null ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.l.JobEventListener.onEvent:34
11:35:19.951 [activiti-job-2] [INFO] 监听 Job 事件: TIMER_SCHEDULED      null ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.l.JobEventListener.onEvent:34
11:35:19.951 [activiti-job-2] [INFO] 监听 Job 事件: JOB_EXECUTION_SUCCESS      null ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.l.JobEventListener.onEvent:34
11:35:29.958 [activiti-job-3] [INFO] 监听 Job 事件: TIMER_FIRED      null ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.l.JobEventListener.onEvent:34
11:35:29.959 [activiti-job-3] [INFO] 监听 Job 事件: TIMER_SCHEDULED      null ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.l.JobEventListener.onEvent:34
11:35:29.960 [activiti-job-3] [INFO] 监听 Job 事件: JOB_EXECUTION_SUCCESS      null ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.l.JobEventListener.onEvent:34
11:35:39.966 [activiti-job-4] [INFO] 监听 Job 事件: TIMER_FIRED      null ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.l.JobEventListener.onEvent:34
11:35:39.970 [activiti-job-4] [INFO] 监听 Job 事件: TIMER_SCHEDULED      null ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.l.JobEventListener.onEvent:34
11:35:39.971 [activiti-job-4] [INFO] 监听 Job 事件: JOB_EXECUTION_SUCCESS      null ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.l.JobEventListener.onEvent:34
11:35:49.975 [activiti-job-5] [INFO] 监听 Job 事件: TIMER_FIRED      null ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.l.JobEventListener.onEvent:34
11:35:49.975 [activiti-job-5] [INFO] 监听 Job 事件: JOB_EXECUTION_SUCCESS      null ProcessDefinitionId=     executionId= mdcProcessInstanceId= mdcBusinessKey= o.d.a.l.JobEventListener.onEvent:34

4.6 Activiti 与 Spring 集成

4.6.1 集成 Spring 配置

  • 添加依赖:
1
2
3
4
5
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring</artifactId>
<version>6.0.0</version>
</dependency>
  • 基于 Spring 的默认配置: activiti-context.xml, 如果配置该文件, Activiti 在启动过程中就会查找基于 Spring 的 ProcessEngineConfiguration 对象;
  • Activiti 核心服务注入 Spring 容器

4.6.2 基于 Spring 对 Activiti 的管理

4.6.2.1 集成 Spring 事务管理器

activiti-context.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<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.xsd">
<!-- 配置 ProcessEngineConfiguration -->
<bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration">
<!-- Spring 需要单独配置 DataSource -->
<property name="dataSource" ref="dataSource"/>
<property name="transactionManager" ref="transactionManager"/>
<property name="databaseSchemaUpdate" value="true"/>
</bean>

<!-- 数据源 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="driverClassName" value="org.h2.Driver"/>
<property name="url" value="jdbc:h2:mem:activiti"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>

<!-- 事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

<!-- 流程引擎对象 -->
<bean id="processEngine" class="org.activiti.spring.ProcessEngineFactoryBean">
<property name="processEngineConfiguration" ref="processEngineConfiguration"/>
</bean>

<!-- 将服务暴露给 Spring -->
<bean id="runtimeService" factory-bean="processEngine" factory-method="getRuntimeService"/>
<bean id="repositoryService" factory-bean="processEngine" factory-method="getRepositoryService"/>
<bean id="formService" factory-bean="processEngine" factory-method="getFormService"/>
<bean id="taskService" factory-bean="processEngine" factory-method="getTaskService"/>
<bean id="historyService" factory-bean="processEngine" factory-method="getHistoryService"/>

<!-- 配置 activitiRule 用于测试 -->
<bean id="activitiRule" class="org.activiti.engine.test.ActivitiRule">
<property name="processEngine" ref="processEngine"/>
</bean>
</beans>

4.6.2.2 定义文件表达式中使用 Spring Bean

HelloBean
1
2
3
4
5
6
7
8
public class HelloBean {

private static final Logger logger = LoggerFactory.getLogger(HelloBean.class);

public void sayHello() {
logger.info("sayHello");
}
}
my-process-spring.bpmn20.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema"
expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
<process id="my-process">

<startEvent id="start"/>
<sequenceFlow id="flow1" sourceRef="start" targetRef="someTask"/>

<userTask id="someTask" name="Activiti is awesome!"/>
<sequenceFlow id="flow2" sourceRef="someTask" targetRef="helloBean"/>
<!-- 从 Spring 容器中查找 Hello bean, 并且调用 sayHello() 方法 -->
<serviceTask id="helloBean" activiti:expression="${helloBean.sayHello()}"/>
<sequenceFlow id="flow3" sourceRef="helloBean" targetRef="end"/>

<endEvent id="end"/>
</process>
</definitions>
测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:activiti-context.xml"})
public class ConfigSpringTest {

public static final Logger logger = LoggerFactory.getLogger(ConfigSpringTest.class);

@Rule
@Autowired
public ActivitiRule activitiRule;

@Autowired
private RuntimeService runtimeService;

@Autowired
private TaskService taskService;

@Test
@Deployment(resources = {"my-process-spring.bpmn20.xml"})
public void test() {
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("my-process");
Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
taskService.complete(task.getId());

logger.info("processInstance: [{}]", processInstance);
logger.info("task: [{}]", task);

}
}

4.6.2.3 自动部署资源文件

4.6.3 基于 Spring 的流程单元测试

  • 添加依赖 spring-test
  • 辅助测试 Rule: ActivitiRule
  • 辅助测试 TestCase: SpringActivitiTestCase

5. Activiti 核心 API

服务名称 功能
RepositoryServie 负责对静态文件的管理, 涉及部署对象和资源对象, 其二者是一对多的关系
RuntimeService 负责对流程进行控制的服务, 可以对流程实例完成启动, 暂停, 挂起等操作
TaskService 负责管理运行中的 UserTask(人工任务)
IdentityService 负责对用户和用户组的管理
FormService 负责解析流程定义中的表单, 对表单的数据类型做渲染
HistoryService 提供了对运行结束的流程实例的查询和删除操作
ManagementService 提供了对流程引擎基础的管理, 提供对定时任务 Job 的管理, 获取表结构, 表明的操作
DynamicBpmnService 提供了对动态的流程定义模型做修改

5.1 RepositoryService

  • 管理流程定义文件 xml 及静态资源服务
  • 对特定的流程的暂停和激活
  • 流程定义启动权限管理
  • 部署文件构造器 DeploymentBuilder
  • 部署文件查询器 DeploymentQuery
  • 流程定义文件查询对象 ProcessDefinitionQuery
  • 流程部署文件对象 Deployment
  • 流程定义文件对象 ProcessDefinition
  • 流程定义的 Java 格式 BpmnModel

RepositoryService API:

方法名 功能
createDeployment 添加资源文件
deleteDeployment 删除资源文件
setDeploymentCategory 指定分类名称
createProcessDefinitionQuery 创建流程定义查询对象
createNativeProcessDefinitionQuery 通过 SQL 查询流程定义对象
suspendProcessDefinitionByXX 通过某些条件暂停/挂起流程定义对象, 使之不能再生成新的流程实例
activateProcessDefinitionByXX 通过某些条件激活流程定义对象, 使之可以继续生成新的流程实例
getProcssDiagram 获取流程图的数据流
getBpmnModel 获取 BpmnModel 对象
addCandidateStarterUser 设置某个流程文件只能由指定的用户去启动
addCandidateStarterGroup 设置某个流程文件只能由指定的用户组去启动

5.1.1 ProcessDefinitionId 的含义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Test
public void testRepository() {
RepositoryService repositoryService = activitiRule.getRepositoryService();
DeploymentBuilder deploymentBuilder1 = repositoryService.createDeployment();
deploymentBuilder1 // 一个部署对象就记录了一次部署
.name("测试部署资源1") // 设置名称
.addClasspathResource("org/destiny/activiti/my-process.bpmn20.xml")
.addClasspathResource("org/destiny/activiti/SecondApprove.bpmn20.xml")
.deploy(); // 完成部署

DeploymentBuilder deploymentBuilder2 = repositoryService.createDeployment();
deploymentBuilder2 // 一个部署对象就记录了一次部署
.name("测试部署资源2") // 设置名称
.addClasspathResource("org/destiny/activiti/my-process.bpmn20.xml")
.addClasspathResource("org/destiny/activiti/SecondApprove.bpmn20.xml")
.deploy(); // 完成部署

// 查询部署对象
List<Deployment> deploymentList = repositoryService.createDeploymentQuery()
.orderByDeploymenTime().asc()
.list();

logger.info("size of deploymentList: {}", deploymentList.size());
for (Deployment deployment : deploymentList) {
logger.info("deployment: {}", deployment);
}


// 流程定义
List<ProcessDefinition> processDefinitionList = repositoryService
.createProcessDefinitionQuery()
.orderByProcessDefinitionKey().asc()
.listPage(0, 100);
for (ProcessDefinition processDefinition : processDefinitionList) {
logger.info("processDefinition: {}, version: {}, key: {}, name: {}",
processDefinition, processDefinition.getVersion(), processDefinition.getKey(), processDefinition.getName());
}
}

生成的日志:

08:42:45,507 [main] INFO  org.springframework.beans.factory.xml.XmlBeanDefinitionReader  - Loading XML bean definitions from class path resource [activiti.cfg.xml]
08:42:47,193 [main] INFO  org.activiti.engine.compatibility.DefaultActiviti5CompatibilityHandlerFactory  - Activiti 5 compatibility handler implementation not found or error during instantiation : org.activiti.compatibility.DefaultActiviti5CompatibilityHandler. Activiti 5 backwards compatibility disabled.
08:42:47,207 [main] INFO  org.activiti.engine.impl.db.DbSqlSession  - performing create on engine with resource org/activiti/db/create/activiti.h2.create.engine.sql
08:42:47,268 [main] INFO  org.activiti.engine.impl.db.DbSqlSession  - performing create on history with resource org/activiti/db/create/activiti.h2.create.history.sql
08:42:47,274 [main] INFO  org.activiti.engine.impl.db.DbSqlSession  - performing create on identity with resource org/activiti/db/create/activiti.h2.create.identity.sql
08:42:47,280 [main] INFO  org.activiti.engine.impl.ProcessEngineImpl  - ProcessEngine default created
08:42:49,736 [main] INFO  org.destiny.activiti.coreapi.RepositoryServiceTest  - size of deploymentList: 2
08:42:49,736 [main] INFO  org.destiny.activiti.coreapi.RepositoryServiceTest  - deployment: DeploymentEntity[id=1, name=测试部署资源1]
08:42:49,736 [main] INFO  org.destiny.activiti.coreapi.RepositoryServiceTest  - deployment: DeploymentEntity[id=7, name=测试部署资源2]
08:42:49,742 [main] INFO  org.destiny.activiti.coreapi.RepositoryServiceTest  - processDefinition: ProcessDefinitionEntity[SecondApprove:1:5], version: 1, key: SecondApprove, name: 二级审批
08:42:49,742 [main] INFO  org.destiny.activiti.coreapi.RepositoryServiceTest  - processDefinition: ProcessDefinitionEntity[SecondApprove:2:11], version: 2, key: SecondApprove, name: 二级审批
08:42:49,742 [main] INFO  org.destiny.activiti.coreapi.RepositoryServiceTest  - processDefinition: ProcessDefinitionEntity[my-process:1:6], version: 1, key: my-process, name: null
08:42:49,743 [main] INFO  org.destiny.activiti.coreapi.RepositoryServiceTest  - processDefinition: ProcessDefinitionEntity[my-process:2:12], version: 2, key: my-process, name: null

两个 DeploymentEntity, 一个 id 为 1, 一个 id 为 7, id 的设置使用全局自增, 说明在两个 Deployment 对象的部署过程中插入了 6 条记录:

  1. 1 个部署记录;
  2. 2 个流程定义记录;
  3. 2 个 xml 文件对应的数据流记录;
  4. 1 个流程定义所生成的图片记录;(my-process 没有生成图片)

5.1.2 流程挂起/激活

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
@org.activiti.engine.test.Deployment(resources = "org/destiny/activiti/my-process.bpmn20.xml")
public void testSuspend() {
RepositoryService repositoryService = activitiRule.getRepositoryService();
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().singleResult();
String processDefinitionId = processDefinition.getId();
logger.info("processDefinitionId: {}", processDefinitionId);

repositoryService.suspendProcessDefinitionById(processDefinitionId);

try {
logger.info("开始启动");
activitiRule.getRuntimeService().startProcessInstanceById(processDefinitionId);
logger.info("启动成功");
} catch (Exception e) {
logger.error("启动失败, 原因: {}", e.getMessage());
}

repositoryService.activateProcessDefinitionById(processDefinitionId);
logger.info("激活后开始启动");
activitiRule.getRuntimeService().startProcessInstanceById(processDefinitionId);
logger.info("激活后启动成功");
}

输出日志:

09:12:42,071 [main] INFO  org.springframework.beans.factory.xml.XmlBeanDefinitionReader  - Loading XML bean definitions from class path resource [activiti.cfg.xml]
09:12:43,614 [main] INFO  org.activiti.engine.compatibility.DefaultActiviti5CompatibilityHandlerFactory  - Activiti 5 compatibility handler implementation not found or error during instantiation : org.activiti.compatibility.DefaultActiviti5CompatibilityHandler. Activiti 5 backwards compatibility disabled.
09:12:43,627 [main] INFO  org.activiti.engine.impl.db.DbSqlSession  - performing create on engine with resource org/activiti/db/create/activiti.h2.create.engine.sql
09:12:43,682 [main] INFO  org.activiti.engine.impl.db.DbSqlSession  - performing create on history with resource org/activiti/db/create/activiti.h2.create.history.sql
09:12:43,688 [main] INFO  org.activiti.engine.impl.db.DbSqlSession  - performing create on identity with resource org/activiti/db/create/activiti.h2.create.identity.sql
09:12:43,692 [main] INFO  org.activiti.engine.impl.ProcessEngineImpl  - ProcessEngine default created
09:12:43,882 [main] INFO  org.destiny.activiti.coreapi.RepositoryServiceTest  - processDefinitionId: my-process:1:3
09:12:43,887 [main] INFO  org.destiny.activiti.coreapi.RepositoryServiceTest  - 开始启动
09:12:43,893 [main] ERROR org.activiti.engine.impl.interceptor.CommandContext  - Error while closing command context
org.activiti.engine.ActivitiException: Cannot start process instance. Process definition null (id = my-process:1:3) is suspended
    at org.activiti.engine.impl.util.ProcessInstanceHelper.createAndStartProcessInstance(ProcessInstanceHelper.java:67)
    at org.activiti.engine.impl.util.ProcessInstanceHelper.createAndStartProcessInstance(ProcessInstanceHelper.java:51)
    at org.activiti.engine.impl.cmd.StartProcessInstanceCmd.createAndStartProcessInstance(StartProcessInstanceCmd.java:109)
    at org.activiti.engine.impl.cmd.StartProcessInstanceCmd.execute(StartProcessInstanceCmd.java:102)
    at org.activiti.engine.impl.cmd.StartProcessInstanceCmd.execute(StartProcessInstanceCmd.java:37)
    at org.activiti.engine.impl.interceptor.CommandInvoker$1.run(CommandInvoker.java:37)
    at org.activiti.engine.impl.interceptor.CommandInvoker.executeOperation(CommandInvoker.java:78)
    at org.activiti.engine.impl.interceptor.CommandInvoker.executeOperations(CommandInvoker.java:57)
    at org.activiti.engine.impl.interceptor.CommandInvoker.execute(CommandInvoker.java:42)
    at org.activiti.engine.impl.interceptor.TransactionContextInterceptor.execute(TransactionContextInterceptor.java:48)
    at org.activiti.engine.impl.interceptor.CommandContextInterceptor.execute(CommandContextInterceptor.java:63)
    at org.activiti.engine.impl.interceptor.LogInterceptor.execute(LogInterceptor.java:29)
    at org.activiti.engine.impl.cfg.CommandExecutorImpl.execute(CommandExecutorImpl.java:44)
    at org.activiti.engine.impl.cfg.CommandExecutorImpl.execute(CommandExecutorImpl.java:39)
    at org.activiti.engine.impl.RuntimeServiceImpl.startProcessInstanceById(RuntimeServiceImpl.java:114)
    at org.destiny.activiti.coreapi.RepositoryServiceTest.testSuspend(RepositoryServiceTest.java:86)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.activiti.engine.test.ActivitiRule$1.evaluate(ActivitiRule.java:116)
    at org.junit.rules.RunRules.evaluate(RunRules.java:20)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:160)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
09:12:43,895 [main] ERROR org.destiny.activiti.coreapi.RepositoryServiceTest  - 启动失败, 原因: Cannot start process instance. Process definition null (id = my-process:1:3) is suspended
09:12:43,897 [main] INFO  org.destiny.activiti.coreapi.RepositoryServiceTest  - 激活后开始启动
09:12:43,923 [main] INFO  org.destiny.activiti.coreapi.RepositoryServiceTest  - 激活后启动成功

5.1.3 绑定用户/用户组

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 测试用户组
* repositoryService 只提供了构建关系的方式, 具体的校验逻辑需要自己完成
* 可以取出用户/用户组信息, 自行通过逻辑判断
*/
@Test
@org.activiti.engine.test.Deployment(resources = "org/destiny/activiti/my-process.bpmn20.xml")
public void testCandidateStarter() {
RepositoryService repositoryService = activitiRule.getRepositoryService();
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().singleResult();
String processDefinitionId = processDefinition.getId();
logger.info("processDefinitionId: {}", processDefinitionId);

// userId/groupM 是对应的用户/用户组管理服务中创建的 id
repositoryService.addCandidateStarterUser(processDefinitionId, "user");
repositoryService.addCandidateStarterGroup(processDefinitionId, "groupM");

List<IdentityLink> identityLinkList = repositoryService.getIdentityLinksForProcessDefinition(processDefinitionId);
for (IdentityLink identityLink : identityLinkList) {
logger.info("删除前: identityLink: [{}]", identityLink);
}

repositoryService.deleteCandidateStarterGroup(processDefinitionId, "groupM");
repositoryService.deleteCandidateStarterUser(processDefinitionId, "user");

List<IdentityLink> identityLinkList1 = repositoryService.getIdentityLinksForProcessDefinition(processDefinitionId);
for (IdentityLink identityLink : identityLinkList1) {
logger.info("删除后: identityLink: [{}]", identityLink);
}
}

日志输出:

10:08:35,380 [main] INFO  org.springframework.beans.factory.xml.XmlBeanDefinitionReader  - Loading XML bean definitions from class path resource [activiti.cfg.xml]
10:08:36,784 [main] INFO  org.activiti.engine.compatibility.DefaultActiviti5CompatibilityHandlerFactory  - Activiti 5 compatibility handler implementation not found or error during instantiation : org.activiti.compatibility.DefaultActiviti5CompatibilityHandler. Activiti 5 backwards compatibility disabled.
10:08:36,796 [main] INFO  org.activiti.engine.impl.db.DbSqlSession  - performing create on engine with resource org/activiti/db/create/activiti.h2.create.engine.sql
10:08:36,842 [main] INFO  org.activiti.engine.impl.db.DbSqlSession  - performing create on history with resource org/activiti/db/create/activiti.h2.create.history.sql
10:08:36,846 [main] INFO  org.activiti.engine.impl.db.DbSqlSession  - performing create on identity with resource org/activiti/db/create/activiti.h2.create.identity.sql
10:08:36,849 [main] INFO  org.activiti.engine.impl.ProcessEngineImpl  - ProcessEngine default created
10:08:37,009 [main] INFO  org.destiny.activiti.coreapi.RepositoryServiceTest  - processDefinitionId: my-process:1:3
10:08:37,016 [main] INFO  org.destiny.activiti.coreapi.RepositoryServiceTest  - 删除前: identityLink: [IdentityLinkEntity[id=4, type=candidate, userId=user, processDefId=my-process:1:3]]
10:08:37,016 [main] INFO  org.destiny.activiti.coreapi.RepositoryServiceTest  - 删除前: identityLink: [IdentityLinkEntity[id=5, type=candidate, groupId=groupM, processDefId=my-process:1:3]]

5.2 RuntimeService 流程运行控制服务

功能:

  • 启动流程及对流程数据的控制
  • 流程实例(ProcessInstance)与执行流(Execution)查询(当创建实例的时候, 一般也会创建一个执行流)
  • 触发流程操作, 接受信号的消息

Runtime 启动流程及变量管理:

  • 启动流程的常用方式(id, key, message)
  • 启动流程可选参数:
    • businessKey
    • variables
    • tenantId
  • 变量(variables)的设置和获取

5.2.1 基本操作

5.2.1.1 根据流程定义 key 启动流程

每次流程部署时, 对应 ProcessDefintion 的 id 和 version 都会改变, 根据 ProcessDefintionKey 默认取最后一个版本的数据

1
2
3
4
5
6
7
8
9
10
11
@Test
@Deployment(resources = {"org/destiny/activiti/my-process.bpmn20.xml"})
public void testStartProcess() {
RuntimeService runtimeService = activitiRule.getRuntimeService();
Map<String, Object> variables = Maps.newHashMap();
variables.put("key1", "value1");

ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("my-process", variables);

log.info("processInstance: {}", processInstance);
}

5.2.1.2 根据流程定义 id

使用 ProcessDefintionId 进行获取的时候, 需要先通过 RepositoryService 获取到对应的 id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 根据流程定义 id 启动流程
*/
@Test
@Deployment(resources = {"org/destiny/activiti/my-process.bpmn20.xml"})
public void testStartProcessById() {
RepositoryService repositoryService = activitiRule.getRepositoryService();
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().singleResult();
RuntimeService runtimeService = activitiRule.getRuntimeService();
Map<String, Object> variables = Maps.newHashMap();
variables.put("key1", "value1");
ProcessInstance processInstance = runtimeService.startProcessInstanceById(processDefinition.getId(), variables);
log.info("processInstance: {}", processInstance);
}

5.2.1.3 通过 ProcessInstanceBuilder 完成流程的设置以及启动

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
@Deployment(resources = {"org/destiny/activiti/my-process.bpmn20.xml"})
public void testProcessBuilder() {
RuntimeService runtimeService = activitiRule.getRuntimeService();
Map<String, Object> variables = Maps.newHashMap();
variables.put("key1", "value1");
ProcessInstance processInstance = runtimeService.createProcessInstanceBuilder()
.businessKey("businessKey001")
.processDefinitionKey("my-process")
.variables(variables)
.start();
log.info("processInstance: {}", processInstance);
}

5.2.1.4 设置和获取流程变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
@Deployment(resources = {"org/destiny/activiti/my-process.bpmn20.xml"})
public void testVariables() {
RuntimeService runtimeService = activitiRule.getRuntimeService();
Map<String, Object> variables = Maps.newHashMap();
variables.put("key1", "value1");
variables.put("key2", "value2");
variables.put("key3", "value3");
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("my-process", variables);
log.info("processInstance: {}", processInstance);
// 覆盖原有内容
runtimeService.setVariable(processInstance.getId(), "key3", "newValue4");
runtimeService.setVariable(processInstance.getId(), "key4", "value4");
// 根据流程实例 id 获取流程变量
Map<String, Object> map = runtimeService.getVariables(processInstance.getId());
log.info("variable map: {}", map);
}

日志输出:

11:55:06.844 [main] [INFO] variable map: {key1=value1, key2=value2, key3=newValue4, key4=value4}  o.d.a.c.RuntimeServiceTest.testVariables:94

5.2.1.5 对流程实例的查询

1
2
3
4
5
6
7
8
9
10
11
12
@Test
@Deployment(resources = {"org/destiny/activiti/my-process.bpmn20.xml"})
public void testProcessInstanceQuery() {
RuntimeService runtimeService = activitiRule.getRuntimeService();
Map<String, Object> variables = Maps.newHashMap();
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("my-process", variables);
log.info("processInstance: {}", processInstance);
ProcessInstance processInstance1 = runtimeService.createProcessInstanceQuery()
.processInstanceId(processInstance.getId())
.singleResult();
log.info("processInstance1: {}", processInstance1);
}

5.2.1.6 对执行流的查询

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
@Deployment(resources = {"org/destiny/activiti/my-process.bpmn20.xml"})
public void testExecutionQuery() {
RuntimeService runtimeService = activitiRule.getRuntimeService();
Map<String, Object> variables = Maps.newHashMap();
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("my-process", variables);
log.info("processInstance: {}", processInstance);
List<Execution> executionList = runtimeService.createExecutionQuery()
.listPage(0, 100);
for (Execution execution : executionList) {
log.info("execution: {}", execution);
}
}

5.2.2 流程实例与执行流

  • 流程实例(ProcessInstance) 表示一次工作流业务的数据实体, 当每次启动流程的时候, 生成一个流程实例
  • 执行流(Execution) 表示流程实例中具体的执行路径, 如果简单的流程只有一条执行路径, 那么此时流程实例和执行流是一对一的关系
  • 流程实例接口继承与执行流

5.2.3 流程触发

  • 使用 trigger 触发 receiveTask 节点
  • 触发信号捕获事件 singalEventRecivied(信号可以全局发送)
  • 触发消息捕获事件 messageEventReceived(消息只能针对某一个流程实例)

5.2.3.1 流程触发 trigger

流程配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema"
expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
<process id="my-process">
<startEvent id="start"/>
<sequenceFlow id="flow1" sourceRef="start" targetRef="someTask"/>
<!--<userTask id="someTask" name="Activiti is awesome!"/>-->
<receiveTask id="someTask"/>
<sequenceFlow id="flow2" sourceRef="someTask" targetRef="end"/>
<endEvent id="end"/>
</process>

</definitions>
测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
@Deployment(resources = {"org/destiny/activiti/my-process-trigger.bpmn20.xml"})
public void testTrigger() {
RuntimeService runtimeService = activitiRule.getRuntimeService();
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("my-process");
// 开始流程后流程实例就会在 receiveTask 节点等待处理
Execution execution = runtimeService.createExecutionQuery()
.activityId("someTask")
.singleResult();
log.info("execution: {}", execution);
runtimeService.trigger(execution.getId());
// 再次查询
execution = runtimeService.createExecutionQuery()
.activityId("someTask")
.singleResult();
log.info("execution: {}", execution);
}

输出日志

1
2
14:59:58.256 [main] [INFO] execution: Execution[ id '5' ] - activity 'someTask - parent '4'  o.d.a.c.RuntimeServiceTest.testTrigger:137
14:59:58.291 [main] [INFO] execution: null o.d.a.c.RuntimeServiceTest.testTrigger:142]

当完成了触发之后, 执行对象已经执行完成

5.2.3.2 流程触发 singalEventReceived

流程开始后, 流程会暂停在中间节点(SingalCatchingEvent), 当它获取到信号时间的时候, 才会继续流转

流程定义文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema"
expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
<signal id="signalStart" name="my-signal"/>
<process id="my-process">
<startEvent id="start"/>
<sequenceFlow id="flow1" sourceRef="start" targetRef="signal-received"/>
<!-- 定义捕获边界事件, 当该节点接收到 my-signal 信号后会继续向后流转 -->
<intermediateCatchEvent id="signal-received">
<signalEventDefinition signalRef="signalStart"/>
</intermediateCatchEvent>
<!--<userTask id="someTask" name="Activiti is awesome!"/>-->
<sequenceFlow id="flow2" sourceRef="signal-received" targetRef="end"/>
<endEvent id="end"/>
</process>
</definitions>
测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
@Deployment(resources = {"org/destiny/activiti/my-process-signal.bpmn20.xml"})
public void testSignalEventReceived() {
RuntimeService runtimeService = activitiRule.getRuntimeService();
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("my-process");
// 查询数据库是否有一个正在等待信号的节点
Execution execution = runtimeService.createExecutionQuery()
.signalEventSubscriptionName("my-signal")
.singleResult();
log.info("execution: {}", execution);
// 触发信号
runtimeService.signalEventReceived("my-signal");
// 重新执行查询
execution = runtimeService.createExecutionQuery()
.signalEventSubscriptionName("my-signal")
.singleResult();
log.info("execution: {}", execution);
}
日志输出
15:14:42.184 [main] [INFO] execution: Execution[ id '5' ] - activity 'signal-received - parent '4'  o.d.a.c.RuntimeServiceTest.testSignalEventReceived:155
15:14:42.216 [main] [INFO] execution: null  o.d.a.c.RuntimeServiceTest.testSignalEventReceived:163

5.2.3.3 流程触发 messageEventReceived

消息触发与信号触发非常相似, 唯一的不同是: 信号与具体的流程实例无关, 消息在执行过程中必须制定流程实例 id

流程定义文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema"
expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
<message id="messageStart" name="my-message"/>
<process id="my-process">
<startEvent id="start"/>
<sequenceFlow id="flow1" sourceRef="start" targetRef="message-received"/>
<!-- 定义捕获边界事件, 当该节点接收到 my-message 消息后会继续向后流转 -->
<intermediateCatchEvent id="message-received">
<messageEventDefinition messageRef="messageStart"/>
</intermediateCatchEvent>
<!--<userTask id="someTask" name="Activiti is awesome!"/>-->
<sequenceFlow id="flow2" sourceRef="message-received" targetRef="end"/>
<endEvent id="end"/>
</process>
</definitions>
测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
@Deployment(resources = {"org/destiny/activiti/my-process-message.bpmn20.xml"})
public void testMessageEventReceived() {
RuntimeService runtimeService = activitiRule.getRuntimeService();
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("my-process");
// 查询数据库是否有一个正在等待信号的节点
Execution execution = runtimeService.createExecutionQuery()
.messageEventSubscriptionName("my-message") // 查询订阅了该信号的执行流
.singleResult();
log.info("execution: {}", execution);
// 触发消息, 不同于信号的触发, message 在触发时需要指定 executionId
runtimeService.messageEventReceived("my-message", execution.getId());
// 重新执行查询
execution = runtimeService.createExecutionQuery()
.messageEventSubscriptionName("my-message")
.singleResult();
log.info("execution: {}", execution);
}
日志输出
15:37:52.024 [main] [INFO] execution: Execution[ id '5' ] - activity 'message-received - parent '4'  o.d.a.c.RuntimeServiceTest.testMessageEventReceived:175
15:37:52.054 [main] [INFO] execution: null  o.d.a.c.RuntimeServiceTest.testMessageEventReceived:183

5.2.3.4 流程基于 message 启动

流程定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema"
expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
<!-- 此时既可以基于 key 启动, 也可以基于 message 启动 -->
<message id="messageStart" name="my-message"/>
<process id="my-process">
<!-- 需要将 messageEventDefinition 放在开始节点 -->
<startEvent id="start">
<messageEventDefinition messageRef="messageStart"/>
</startEvent>
<sequenceFlow id="flow1" sourceRef="start" targetRef="someTask"/>
<userTask id="someTask" name="Activiti is awesome!"/>
<sequenceFlow id="flow2" sourceRef="someTask" targetRef="end"/>
<endEvent id="end"/>
</process>
</definitions>
测试代码
1
2
3
4
5
6
7
@Test
@Deployment(resources = {"org/destiny/activiti/my-process-message-start.bpmn20.xml"})
public void testMessageStart() {
RuntimeService runtimeService = activitiRule.getRuntimeService();
ProcessInstance processInstance = runtimeService.startProcessInstanceByMessage("my-message");
log.info("processInstance: {}", processInstance);
}
日志输出
15:45:12.844 [main] [INFO] processInstance: ProcessInstance[5]  o.d.a.c.RuntimeServiceTest.testMessageStart:195

基于 message 启动流程时, ProcessInstance 的 id 是 5, 意味着在流程订阅表中多插入了一条数据, 在实际启动过程中, 还是通过 message 找到 ProcessDefinition 的 key, 最终根据 key 来启动

5.3 TaskService 任务管理服务

TaskService 提供的功能:

  • 对用户任务管理和流程的控制
  • 设置用户任务的权限信息(拥有者/候选人/办理人)
  • 针对用户任务添加任务附件, 任务评论和事件记录

TaskService 对 Task 管理与流程控制:

  • Task 对象的创建, 删除
  • 查询 Task, 并驱动 Task 节点完成执行
  • Task 相关参数变量设置
    • local 变量
    • 非 local 变量

5.3.1 基本操作

5.3.1.1 获取 task/ 设置变量/ 驱动完成

流程定义文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema"
expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
<process id="my-process">
<startEvent id="start"/>
<sequenceFlow id="flow1" sourceRef="start" targetRef="someTask"/>
<!-- 添加候选人 -->
<userTask id="someTask" name="Activiti is awesome!" activiti:candidateUsers="destiny,destiny1,destiny2">
<!-- 添加描述, message 会根据上下文中传入的 message 变量值去替换 -->
<documentation>
some task ${message}
</documentation>
</userTask>
<sequenceFlow id="flow2" sourceRef="someTask" targetRef="end"/>
<endEvent id="end"/>
</process>
</definitions>
测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Test
@Deployment(resources = {"org/destiny/activiti/my-process-task.bpmn20.xml"})
public void testTaskService() {
Map<String, Object> variables = Maps.newHashMap();
variables.put("message", "my test message");
TaskService taskService = activitiRule.getTaskService();
// 部署流程定义文件
ProcessInstance processInstance = activitiRule.getRuntimeService()
.startProcessInstanceByKey("my-process", variables);
Task task = taskService.createTaskQuery().singleResult();
log.info("task: {}", ToStringBuilder.reflectionToString(task, ToStringStyle.JSON_STYLE));
log.info("task.description: {}", task.getDescription());

// 设置变量
taskService.setVariable(task.getId(), "k1", "v1");
taskService.setVariableLocal(task.getId(), "localK1", "localV1");

// local 只在 task 范围可见
Map<String, Object> taskServiceVariables = taskService.getVariables(task.getId());
Map<String, Object> taskServiceVariablesLocal = taskService.getVariablesLocal(task.getId());
// 根据流程获取
Map<String, Object> processVariables = activitiRule.getRuntimeService().getVariables(task.getExecutionId());

log.info("taskServiceVariables: {}", taskServiceVariables); // {k1=v1, localK1=localV1, message=my test message}
log.info("taskServiceVariablesLocal: {}", taskServiceVariablesLocal); // {localK1=localV1}
log.info("processVariables: {}", processVariables); // {k1=v1, message=my test message}

Map<String, Object> completeVar = Maps.newHashMap();
completeVar.put("cKey1", "cValue1");
taskService.complete(task.getId(), completeVar);

Task task1 = taskService.createTaskQuery().taskId(task.getId()).singleResult();
log.info("task1: {}", task1);
}

日志输出
16:13:26.654 [main] [INFO] Loading XML bean definitions from class path resource [activiti.cfg.xml]  o.s.b.f.x.XmlBeanDefinitionReader.loadBeanDefinitions:316
16:13:28.438 [main] [INFO] Activiti 5 compatibility handler implementation not found or error during instantiation : org.activiti.compatibility.DefaultActiviti5CompatibilityHandler. Activiti 5 backwards compatibility disabled.  o.a.e.c.DefaultActiviti5CompatibilityHandlerFactory.createActiviti5CompatibilityHandler:38
16:13:28.451 [main] [INFO] performing create on engine with resource org/activiti/db/create/activiti.h2.create.engine.sql  o.a.e.i.d.DbSqlSession.executeSchemaResource:1147
16:13:28.527 [main] [INFO] performing create on history with resource org/activiti/db/create/activiti.h2.create.history.sql  o.a.e.i.d.DbSqlSession.executeSchemaResource:1147
16:13:28.535 [main] [INFO] performing create on identity with resource org/activiti/db/create/activiti.h2.create.identity.sql  o.a.e.i.d.DbSqlSession.executeSchemaResource:1147
16:13:28.541 [main] [INFO] ProcessEngine default created  o.a.e.i.ProcessEngineImpl.<init>:87
16:13:28.793 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.ContinueProcessOperation :  o.a.e.i.i.DebugCommandInvoker.executeOperation:33
16:13:28.796 [main] [INFO] 
4 (process instance)
└── 6 : start (StartEvent, parent id 4 (active)
  o.a.e.i.i.DebugCommandInvoker.executeOperation:34
16:13:28.798 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.TakeOutgoingSequenceFlowsOperation :  o.a.e.i.i.DebugCommandInvoker.executeOperation:33
16:13:28.799 [main] [INFO] 
4 (process instance)
└── 6 : start (StartEvent, parent id 4 (active)
  o.a.e.i.i.DebugCommandInvoker.executeOperation:34
16:13:28.800 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.ContinueProcessOperation :  o.a.e.i.i.DebugCommandInvoker.executeOperation:33
16:13:28.800 [main] [INFO] 
4 (process instance)
└── 6 : start -> someTask, parent id 4 (active)
  o.a.e.i.i.DebugCommandInvoker.executeOperation:34
16:13:28.802 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.ContinueProcessOperation :  o.a.e.i.i.DebugCommandInvoker.executeOperation:33
16:13:28.802 [main] [INFO] 
4 (process instance)
└── 6 : someTask (UserTask, parent id 4 (active)
  o.a.e.i.i.DebugCommandInvoker.executeOperation:34
16:13:28.878 [main] [INFO] task: {"owner":null,"assigneeUpdatedCount":0,"originalAssignee":null,"assignee":null,"delegationState":null,"parentTaskId":null,"name":"Activiti is awesome!","localizedName":null,"description":"some task my test message","localizedDescription":null,"priority":50,"createTime":"Mon Dec 03 16:13:28 CST 2018","dueDate":null,"suspensionState":1,"category":null,"isIdentityLinksInitialized":false,"taskIdentityLinkEntities":[],"executionId":"6","execution":null,"processInstanceId":"4","processInstance":null,"processDefinitionId":"my-process:1:3","taskDefinitionKey":"someTask","formKey":null,"isDeleted":false,"isCanceled":false,"eventName":null,"currentActivitiListener":null,"tenantId":"","queryVariables":null,"forcedUpdate":false,"claimTime":null,"variableInstances":null,"usedVariablesCache":{},"transientVariabes":null,"cachedElContext":null,"id":"9","revision":1,"isInserted":false,"isUpdated":false,"isDeleted":false}  o.d.a.c.TaskServiceTest.testTaskService:33
16:13:28.878 [main] [INFO] task.description: some task my test message  o.d.a.c.TaskServiceTest.testTaskService:34
16:13:28.893 [main] [INFO] taskServiceVariables: {k1=v1, localK1=localV1, message=my test message}  o.d.a.c.TaskServiceTest.testTaskService:46
16:13:28.893 [main] [INFO] taskServiceVariablesLocal: {localK1=localV1}  o.d.a.c.TaskServiceTest.testTaskService:47
16:13:28.893 [main] [INFO] processVariables: {k1=v1, message=my test message}  o.d.a.c.TaskServiceTest.testTaskService:48
16:13:28.893 [main] [INFO] 
4 (process instance)
└── 6 : someTask (UserTask, parent id 4 (active)
  o.a.e.i.i.DebugCommandInvoker.executeOperation:34
16:13:28.893 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.TakeOutgoingSequenceFlowsOperation :  o.a.e.i.i.DebugCommandInvoker.executeOperation:33
16:13:28.893 [main] [INFO] 
4 (process instance)
└── 6 : someTask (UserTask, parent id 4 (active)
  o.a.e.i.i.DebugCommandInvoker.executeOperation:34
16:13:28.893 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.ContinueProcessOperation :  o.a.e.i.i.DebugCommandInvoker.executeOperation:33
16:13:28.893 [main] [INFO] 
4 (process instance)
└── 6 : someTask -> end, parent id 4 (active)
  o.a.e.i.i.DebugCommandInvoker.executeOperation:34
16:13:28.893 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.ContinueProcessOperation :  o.a.e.i.i.DebugCommandInvoker.executeOperation:33
16:13:28.893 [main] [INFO] 
4 (process instance)
└── 6 : end (EndEvent, parent id 4 (active)
  o.a.e.i.i.DebugCommandInvoker.executeOperation:34
16:13:28.893 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.TakeOutgoingSequenceFlowsOperation :  o.a.e.i.i.DebugCommandInvoker.executeOperation:33
16:13:28.893 [main] [INFO] 
4 (process instance)
└── 6 : end (EndEvent, parent id 4 (active)
  o.a.e.i.i.DebugCommandInvoker.executeOperation:34
16:13:28.893 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.EndExecutionOperation :  o.a.e.i.i.DebugCommandInvoker.executeOperation:33
16:13:28.893 [main] [INFO] 
4 (process instance)
└── 6 : end (EndEvent, parent id 4 (active)
  o.a.e.i.i.DebugCommandInvoker.executeOperation:34
16:13:28.893 [main] [INFO] task1: null  o.d.a.c.TaskServiceTest.testTaskService:55

5.3.1.2 TaskService 设置 Task 权限信息

  • 候选用户(candidateUser) 和候选组(candidateGroup)
  • 指定拥有人(Owner)和办理人(Assignee)
  • 通过 claim 设置办理人(发现 task 已经有指定办理人且不是 claim 指定的人就会抛出异常)
流程定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema"
expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
<process id="my-process">
<startEvent id="start"/>
<sequenceFlow id="flow1" sourceRef="start" targetRef="someTask"/>
<!-- 添加候选人 -->
<userTask id="someTask" name="Activiti is awesome!" activiti:candidateUsers="destiny,destiny1,destiny2">
<!-- 添加描述, message 会根据上下文中传入的 message 变量值去替换 -->
<documentation>
some task ${message}
</documentation>
</userTask>
<sequenceFlow id="flow2" sourceRef="someTask" targetRef="end"/>
<endEvent id="end"/>
</process>
</definitions>
测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Test
@Deployment(resources = {"org/destiny/activiti/my-process-task.bpmn20.xml"})
public void testTaskServiceUser() {
Map<String, Object> variables = Maps.newHashMap();
variables.put("message", "my test message");
activitiRule.getRuntimeService().startProcessInstanceByKey("my-process", variables);
TaskService taskService = activitiRule.getTaskService();
Task task = taskService.createTaskQuery().singleResult();
log.info("task: {}", ToStringBuilder.reflectionToString(task, ToStringStyle.JSON_STYLE));
log.info("task.description: {}", task.getDescription());
taskService.setOwner(task.getId(), "user1");
// 可能存在覆盖已有代办放的问题, 因此不推荐
// taskService.setAssignee(task.getId(), "destiny");
// 查询在候选人列表, 且未指定办理人的 task 列表
List<Task> taskList = taskService.createTaskQuery()
.taskCandidateOrAssigned("destiny")
.taskUnassigned()
.listPage(0, 100);
// 使用 claim 设置候选人
for (Task task1 : taskList) {
try {
taskService.claim(task1.getId(), "destiny");
} catch (Exception e) {
log.warn(e.getMessage(), e);
}
}
// 查看当前 task 的所有用户关系内容
List<IdentityLink> identityLinkList = taskService.getIdentityLinksForTask(task.getId());
for (IdentityLink identityLink : identityLinkList) {
log.info("identityLink: {}", identityLink);
}
// 完成任务, 首先找到处于代办状态的所有 task
List<Task> destinys = taskService.createTaskQuery().taskAssignee("destiny").listPage(0, 100);
for (Task destiny : destinys) {
variables.clear();
variables.put("cKey1", "cValue1");
taskService.complete(destiny.getId(), variables);
}
destinys = taskService.createTaskQuery().taskAssignee("destiny").listPage(0, 100);
log.info("是否存在: {}", CollectionUtils.isEmpty(destinys));
}
输出日志
16:41:28.534 [main] [INFO] Loading XML bean definitions from class path resource [activiti.cfg.xml]  o.s.b.f.x.XmlBeanDefinitionReader.loadBeanDefinitions:316
16:41:30.373 [main] [INFO] Activiti 5 compatibility handler implementation not found or error during instantiation : org.activiti.compatibility.DefaultActiviti5CompatibilityHandler. Activiti 5 backwards compatibility disabled.  o.a.e.c.DefaultActiviti5CompatibilityHandlerFactory.createActiviti5CompatibilityHandler:38
16:41:30.385 [main] [INFO] performing create on engine with resource org/activiti/db/create/activiti.h2.create.engine.sql  o.a.e.i.d.DbSqlSession.executeSchemaResource:1147
16:41:30.443 [main] [INFO] performing create on history with resource org/activiti/db/create/activiti.h2.create.history.sql  o.a.e.i.d.DbSqlSession.executeSchemaResource:1147
16:41:30.449 [main] [INFO] performing create on identity with resource org/activiti/db/create/activiti.h2.create.identity.sql  o.a.e.i.d.DbSqlSession.executeSchemaResource:1147
16:41:30.453 [main] [INFO] ProcessEngine default created  o.a.e.i.ProcessEngineImpl.<init>:87
16:41:30.631 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.ContinueProcessOperation :  o.a.e.i.i.DebugCommandInvoker.executeOperation:33
16:41:30.636 [main] [INFO] 
4 (process instance)
└── 6 : start (StartEvent, parent id 4 (active)
  o.a.e.i.i.DebugCommandInvoker.executeOperation:34
16:41:30.638 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.TakeOutgoingSequenceFlowsOperation :  o.a.e.i.i.DebugCommandInvoker.executeOperation:33
16:41:30.638 [main] [INFO] 
4 (process instance)
└── 6 : start (StartEvent, parent id 4 (active)
  o.a.e.i.i.DebugCommandInvoker.executeOperation:34
16:41:30.639 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.ContinueProcessOperation :  o.a.e.i.i.DebugCommandInvoker.executeOperation:33
16:41:30.640 [main] [INFO] 
4 (process instance)
└── 6 : start -> someTask, parent id 4 (active)
  o.a.e.i.i.DebugCommandInvoker.executeOperation:34
16:41:30.640 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.ContinueProcessOperation :  o.a.e.i.i.DebugCommandInvoker.executeOperation:33
16:41:30.641 [main] [INFO] 
4 (process instance)
└── 6 : someTask (UserTask, parent id 4 (active)
  o.a.e.i.i.DebugCommandInvoker.executeOperation:34
16:41:30.715 [main] [INFO] task: {"owner":null,"assigneeUpdatedCount":0,"originalAssignee":null,"assignee":null,"delegationState":null,"parentTaskId":null,"name":"Activiti is awesome!","localizedName":null,"description":"some task my test message","localizedDescription":null,"priority":50,"createTime":"Mon Dec 03 16:41:30 CST 2018","dueDate":null,"suspensionState":1,"category":null,"isIdentityLinksInitialized":false,"taskIdentityLinkEntities":[],"executionId":"6","execution":null,"processInstanceId":"4","processInstance":null,"processDefinitionId":"my-process:1:3","taskDefinitionKey":"someTask","formKey":null,"isDeleted":false,"isCanceled":false,"eventName":null,"currentActivitiListener":null,"tenantId":"","queryVariables":null,"forcedUpdate":false,"claimTime":null,"variableInstances":null,"usedVariablesCache":{},"transientVariabes":null,"cachedElContext":null,"id":"9","revision":1,"isInserted":false,"isUpdated":false,"isDeleted":false}  o.d.a.c.TaskServiceTest.testTaskServiceUser:69
16:41:30.715 [main] [INFO] task.description: some task my test message  o.d.a.c.TaskServiceTest.testTaskServiceUser:70
16:41:30.739 [main] [INFO] identityLink: IdentityLinkEntity[id=10, type=candidate, userId=destiny, taskId=9]  o.d.a.c.TaskServiceTest.testTaskServiceUser:94
16:41:30.740 [main] [INFO] identityLink: IdentityLinkEntity[id=12, type=candidate, userId=destiny1, taskId=9]  o.d.a.c.TaskServiceTest.testTaskServiceUser:94
16:41:30.740 [main] [INFO] identityLink: IdentityLinkEntity[id=14, type=candidate, userId=destiny2, taskId=9]  o.d.a.c.TaskServiceTest.testTaskServiceUser:94
16:41:30.740 [main] [INFO] identityLink: IdentityLinkEntity[id=null, type=assignee, userId=destiny, taskId=9]  o.d.a.c.TaskServiceTest.testTaskServiceUser:94
16:41:30.740 [main] [INFO] identityLink: IdentityLinkEntity[id=null, type=owner, userId=user1, taskId=9]  o.d.a.c.TaskServiceTest.testTaskServiceUser:94
16:41:30.749 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.TriggerExecutionOperation :  o.a.e.i.i.DebugCommandInvoker.executeOperation:33
16:41:30.751 [main] [INFO] 
4 (process instance)
└── 6 : someTask (UserTask, parent id 4 (active)
  o.a.e.i.i.DebugCommandInvoker.executeOperation:34
16:41:30.752 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.TakeOutgoingSequenceFlowsOperation :  o.a.e.i.i.DebugCommandInvoker.executeOperation:33
16:41:30.753 [main] [INFO] 
4 (process instance)
└── 6 : someTask (UserTask, parent id 4 (active)
  o.a.e.i.i.DebugCommandInvoker.executeOperation:34
16:41:30.754 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.ContinueProcessOperation :  o.a.e.i.i.DebugCommandInvoker.executeOperation:33
16:41:30.754 [main] [INFO] 
4 (process instance)
└── 6 : someTask -> end, parent id 4 (active)
  o.a.e.i.i.DebugCommandInvoker.executeOperation:34
16:41:30.755 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.ContinueProcessOperation :  o.a.e.i.i.DebugCommandInvoker.executeOperation:33
16:41:30.755 [main] [INFO] 
4 (process instance)
└── 6 : end (EndEvent, parent id 4 (active)
  o.a.e.i.i.DebugCommandInvoker.executeOperation:34
16:41:30.755 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.TakeOutgoingSequenceFlowsOperation :  o.a.e.i.i.DebugCommandInvoker.executeOperation:33
16:41:30.755 [main] [INFO] 
4 (process instance)
└── 6 : end (EndEvent, parent id 4 (active)
  o.a.e.i.i.DebugCommandInvoker.executeOperation:34
16:41:30.757 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.EndExecutionOperation :  o.a.e.i.i.DebugCommandInvoker.executeOperation:33
16:41:30.757 [main] [INFO] 
4 (process instance)
└── 6 : end (EndEvent, parent id 4 (active)
  o.a.e.i.i.DebugCommandInvoker.executeOperation:34
16:41:30.770 [main] [INFO] 是否存在: true  o.d.a.c.TaskServiceTest.testTaskServiceUser:106

5.3.2 TaskService 设置 Task 附加信息

  • 任务附件(Attachment)创建与查询
  • 任务评论(Comment)创建与查询
  • 事件记录(Event)创建于查询

5.3.2.1 任务附件(Attachment)创建与查询

流程定义

同上

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
@Deployment(resources = {"org/destiny/activiti/my-process-task.bpmn20.xml"})
public void testTaskAttachment() {
Map<String, Object> variables = Maps.newHashMap();
variables.put("message", "my test message");
activitiRule.getRuntimeService().startProcessInstanceByKey("my-process", variables);
TaskService taskService = activitiRule.getTaskService();
Task task = taskService.createTaskQuery().singleResult();
// 可以上传数据流或 url
Attachment attachment = taskService.createAttachment("url", task.getId(), task.getProcessInstanceId(), "name", "desc", "/url/test.png");
log.info("attachment: {}", attachment);
List<Attachment> taskAttachments = taskService.getTaskAttachments(task.getId());
for (Attachment taskAttachment : taskAttachments) {
log.info("taskAttachment: {}", ToStringBuilder.reflectionToString(taskAttachment, ToStringStyle.JSON_STYLE));
}
}

日志输出

17:12:47.043 [main] [INFO] attachment: org.activiti.engine.impl.persistence.entity.AttachmentEntityImpl@72b16078  o.d.a.c.TaskServiceTest.testTaskAttachment:119
17:12:47.050 [main] [INFO] taskAttachment: {"name":"name","description":"desc","type":"url","taskId":"9","processInstanceId":"4","url":"/url/test.png","contentId":null,"content":null,"userId":null,"time":"Mon Dec 03 17:12:47 CST 2018","id":"16","revision":1,"isInserted":false,"isUpdated":false,"isDeleted":false}  o.d.a.c.TaskServiceTest.testTaskAttachment:122

5.3.2.2 任务评论(Comment)创建与查询

流程定义

同上

测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
@Deployment(resources = {"org/destiny/activiti/my-process-task.bpmn20.xml"})
public void testTaskComment() {
Map<String, Object> variables = Maps.newHashMap();
variables.put("message", "my test message");
activitiRule.getRuntimeService().startProcessInstanceByKey("my-process", variables);
TaskService taskService = activitiRule.getTaskService();
Task task = taskService.createTaskQuery().singleResult();
// 添加评论
taskService.addComment(task.getId(), task.getProcessInstanceId(), "record note1");
taskService.addComment(task.getId(), task.getProcessInstanceId(), "record note2");
List<Comment> taskComments = taskService.getTaskComments(task.getId());
for (Comment taskComment : taskComments) {
log.info("taskComment: {}", ToStringBuilder.reflectionToString(taskComment, ToStringStyle.JSON_STYLE));
}
// 事件记录
List<Event> taskEvents = taskService.getTaskEvents(task.getId());
for (Event taskEvent : taskEvents) {
log.info("taskEvent: {}", ToStringBuilder.reflectionToString(taskEvent, ToStringStyle.JSON_STYLE));
}
}
日志输出
17:05:17.107 [main] [INFO] taskComment: {"type":"comment","userId":null,"time":"Mon Dec 03 17:05:17 CST 2018","taskId":"9","processInstanceId":"4","action":"AddComment","message":"record note2","fullMessage":"record note2","id":"17","isInserted":false,"isUpdated":false,"isDeleted":false}  o.d.a.c.TaskServiceTest.testTaskComment:140
17:05:17.107 [main] [INFO] taskComment: {"type":"comment","userId":null,"time":"Mon Dec 03 17:05:17 CST 2018","taskId":"9","processInstanceId":"4","action":"AddComment","message":"record note1","fullMessage":"record note1","id":"16","isInserted":false,"isUpdated":false,"isDeleted":false}  o.d.a.c.TaskServiceTest.testTaskComment:140
17:05:17.109 [main] [INFO] taskEvent: {"type":"comment","userId":null,"time":"Mon Dec 03 17:05:17 CST 2018","taskId":"9","processInstanceId":"4","action":"AddComment","message":"record note2","fullMessage":"record note2","id":"17","isInserted":false,"isUpdated":false,"isDeleted":false}  o.d.a.c.TaskServiceTest.testTaskComment:146
17:05:17.110 [main] [INFO] taskEvent: {"type":"comment","userId":null,"time":"Mon Dec 03 17:05:17 CST 2018","taskId":"9","processInstanceId":"4","action":"AddComment","message":"record note1","fullMessage":"record note1","id":"16","isInserted":false,"isUpdated":false,"isDeleted":false}  o.d.a.c.TaskServiceTest.testTaskComment:146

Comment 和 Event 的区别

所有对 task 的操作都会生成新的 Event 记录, comment 只是其中的一种, 比如在上例中新增 owner 或 assignee, 也会产生新的 event 记录

5.4 身份管理服务

Activiti 提供了相对比较简单的用户/用户组管理

主要功能:

  • 管理用户(User)
  • 管理用户组(Group)
  • 用户与用户组的关系(Membership)(多对多关系)

5.4.1 创建用户/用户组/对应关系

流程定义

此处不需要基于流程定义完成

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Slf4j
public class IdentityServiceTest {

@Rule
public ActivitiRule activitiRule = new ActivitiRule();

@Test
public void testIdentity() {
IdentityService identityService = activitiRule.getIdentityService();
User user1 = identityService.newUser("user1");
user1.setEmail("destinywk@163.com");
User user2 = identityService.newUser("user2");
user2.setEmail("destinywk@126.com");
identityService.saveUser(user1);
identityService.saveUser(user2);

Group group1 = identityService.newGroup("group1");
identityService.saveGroup(group1);
Group group2 = identityService.newGroup("group2");
identityService.saveGroup(group2);

// 创建关系
identityService.createMembership("user1", "group1");
identityService.createMembership("user2", "group1");
identityService.createMembership("user1", "group2");

List<User> userList = identityService.createUserQuery()
.memberOfGroup("group1")
.listPage(0, 100);

for (User user : userList) {
log.info("user: {}", ToStringBuilder.reflectionToString(user, ToStringStyle.JSON_STYLE));
}

List<Group> groupList = identityService.createGroupQuery()
.groupMember("user1").listPage(0, 100);
for (Group group : groupList) {
log.info("group: {}", ToStringBuilder.reflectionToString(group, ToStringStyle.JSON_STYLE));
}
}

}

日志输出

17:37:12.982 [main] [INFO] Loading XML bean definitions from class path resource [activiti.cfg.xml]  o.s.b.f.x.XmlBeanDefinitionReader.loadBeanDefinitions:316
17:37:14.894 [main] [INFO] Activiti 5 compatibility handler implementation not found or error during instantiation : org.activiti.compatibility.DefaultActiviti5CompatibilityHandler. Activiti 5 backwards compatibility disabled.  o.a.e.c.DefaultActiviti5CompatibilityHandlerFactory.createActiviti5CompatibilityHandler:38
17:37:14.910 [main] [INFO] performing create on engine with resource org/activiti/db/create/activiti.h2.create.engine.sql  o.a.e.i.d.DbSqlSession.executeSchemaResource:1147
17:37:14.971 [main] [INFO] performing create on history with resource org/activiti/db/create/activiti.h2.create.history.sql  o.a.e.i.d.DbSqlSession.executeSchemaResource:1147
17:37:14.978 [main] [INFO] performing create on identity with resource org/activiti/db/create/activiti.h2.create.identity.sql  o.a.e.i.d.DbSqlSession.executeSchemaResource:1147
17:37:14.982 [main] [INFO] ProcessEngine default created  o.a.e.i.ProcessEngineImpl.<init>:87
17:37:15.041 [main] [INFO] user: {"firstName":null,"lastName":null,"email":"destinywk@163.com","password":null,"pictureByteArrayRef":"ByteArrayRef[id=null, name=null, entity=null]","id":"user1","revision":1,"isInserted":false,"isUpdated":false,"isDeleted":false}  o.d.a.c.IdentityServiceTest.testIdentity:46
17:37:15.042 [main] [INFO] user: {"firstName":null,"lastName":null,"email":"destinywk@126.com","password":null,"pictureByteArrayRef":"ByteArrayRef[id=null, name=null, entity=null]","id":"user2","revision":1,"isInserted":false,"isUpdated":false,"isDeleted":false}  o.d.a.c.IdentityServiceTest.testIdentity:46
17:37:15.048 [main] [INFO] group: {"name":null,"type":null,"id":"group1","revision":1,"isInserted":false,"isUpdated":false,"isDeleted":false}  o.d.a.c.IdentityServiceTest.testIdentity:52
17:37:15.048 [main] [INFO] group: {"name":null,"type":null,"id":"group2","revision":1,"isInserted":false,"isUpdated":false,"isDeleted":false}  o.d.a.c.IdentityServiceTest.testIdentity:52

身份管理服务的调用过程:

image

会将 User 封装成一个 Command, 交由命令执行器去执行, 最后调用 MyBatis 底层接口去操作 DB

5.5 表单管理服务 FormService

功能:

  • 解析流程定义中表单项的配置
  • 提供提交表单的方式驱动用户节点流转
  • 获取自定义外部表单 key

5.5.1

流程定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema"
expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
<process id="my-process">
<startEvent id="start" activiti:formKey="/rest/process/form/start">
<!-- 配置表单项 -->
<extensionElements>
<activiti:formProperty id="message" name="信息" type="string" required="true"/>
</extensionElements>
</startEvent>
<sequenceFlow id="flow1" sourceRef="start" targetRef="someTask"/>
<userTask id="someTask" name="Activiti is awesome!" activiti:formKey="/rest/process/form/userTask">
<extensionElements>
<!-- 配置表单项 -->
<activiti:formProperty id="yesOrNo" name="审批" type="string" required="true"/>
</extensionElements>
</userTask>
<sequenceFlow id="flow2" sourceRef="someTask" targetRef="end"/>
<endEvent id="end"/>
</process>
</definitions>

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Slf4j
public class FormServiceTest {

@Rule
public ActivitiRule activitiRule = new ActivitiRule();

@Test
@Deployment(resources = {"org/destiny/activiti/my-process-form.bpmn20.xml"})
public void testFormService() {
FormService formService = activitiRule.getFormService();
// 获取流程定义文件
ProcessDefinition processDefinition = activitiRule.getRepositoryService().createProcessDefinitionQuery().singleResult();

// 获取 startForm 的 key 和 data
String startFormKey = formService.getStartFormKey(processDefinition.getId());
log.info("startFormKey: {}", startFormKey);
StartFormData startFormData = formService.getStartFormData(processDefinition.getId());
log.info("startFormKey: {}", ToStringBuilder.reflectionToString(startFormData, ToStringStyle.JSON_STYLE));
for (FormProperty startFormProperty : startFormData.getFormProperties()) {
log.info("startFormProperty: {}", ToStringBuilder.reflectionToString(startFormProperty, ToStringStyle.JSON_STYLE));
}

// 启动流程
Map<String, String> properties = Maps.newHashMap();
properties.put("message", "my test message");
formService.submitStartFormData(processDefinition.getId(), properties);

// 查询 task
Task task = activitiRule.getTaskService().createTaskQuery().singleResult();

// 获取 taskForm 的 data
TaskFormData taskFormData = formService.getTaskFormData(task.getId());
log.info("taskFormData: {}", ToStringBuilder.reflectionToString(taskFormData, ToStringStyle.JSON_STYLE));
for (FormProperty taskFormProperty : taskFormData.getFormProperties()) {
log.info("taskFormProperty: {}", ToStringBuilder.reflectionToString(taskFormProperty, ToStringStyle.JSON_STYLE));
}
}
}

日志输出

18:15:27.539 [main] [INFO] startFormKey: /rest/process/form/start  o.d.a.c.FormServiceTest.testFormService:36
18:15:27.547 [main] [INFO] startFormKey: {"processDefinition":"ProcessDefinitionEntity[my-process:1:3]","formKey":"/rest/process/form/start","deploymentId":"1","formProperties":[org.activiti.engine.impl.form.FormPropertyImpl@3bde62ff]}  o.d.a.c.FormServiceTest.testFormService:38
18:15:27.547 [main] [INFO] startFormProperty: {"id":"message","name":"信息","type":"org.activiti.engine.impl.form.StringFormType@2baa8d82","isRequired":true,"isReadable":true,"isWritable":true,"value":null}  o.d.a.c.FormServiceTest.testFormService:40
18:15:27.561 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.ContinueProcessOperation :  o.a.e.i.i.DebugCommandInvoker.executeOperation:33
18:15:27.564 [main] [INFO] 
4 (process instance)
└── 5 : start (StartEvent, parent id 4 (active)
  o.a.e.i.i.DebugCommandInvoker.executeOperation:34
18:15:27.565 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.TakeOutgoingSequenceFlowsOperation :  o.a.e.i.i.DebugCommandInvoker.executeOperation:33
18:15:27.566 [main] [INFO] 
4 (process instance)
└── 5 : start (StartEvent, parent id 4 (active)
  o.a.e.i.i.DebugCommandInvoker.executeOperation:34
18:15:27.567 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.ContinueProcessOperation :  o.a.e.i.i.DebugCommandInvoker.executeOperation:33
18:15:27.567 [main] [INFO] 
4 (process instance)
└── 5 : start -> someTask, parent id 4 (active)
  o.a.e.i.i.DebugCommandInvoker.executeOperation:34
18:15:27.568 [main] [INFO] Execution tree while executing operation class org.activiti.engine.impl.agenda.ContinueProcessOperation :  o.a.e.i.i.DebugCommandInvoker.executeOperation:33
18:15:27.568 [main] [INFO] 
4 (process instance)
└── 5 : someTask (UserTask, parent id 4 (active)
  o.a.e.i.i.DebugCommandInvoker.executeOperation:34
18:15:27.602 [main] [INFO] taskFormData: {"task":"Task[id=10, name=Activiti is awesome!]","formKey":"/rest/process/form/userTask","deploymentId":"1","formProperties":[org.activiti.engine.impl.form.FormPropertyImpl@4e4efc1b]}  o.d.a.c.FormServiceTest.testFormService:53
18:15:27.602 [main] [INFO] taskFormProperty: {"id":"yesOrNo","name":"审批","type":"org.activiti.engine.impl.form.StringFormType@2baa8d82","isRequired":true,"isReadable":true,"isWritable":true,"value":null}  o.d.a.c.FormServiceTest.testFormService:55

5.6 HistoryService 历史数据管理服务

作用:

  • 管理流程实例哦结束后的历史数据
  • 构建历史数据的查询对象
  • 根据流程实例 id 删除流程历史数据
历史数据实体 描述
HistoricProcessInstance 历史流程实例实体类
HistoricVariableInstance 流程或任务变量值的实体
HistoricActivityInstance 单个活动节点执行的信息
HistoricTaskInstance 用户任务实例的信息
HistoricDetail 历史流程活动任务详细信息
  • HistoryService 构建历史查询对象:
    • create[历史数据实体]Query
    • createNative[历史数据实体]Query
    • createProcessInstanceHistoryLogQuery: 只能查出一个流程实例的一个对象, 每次只能查出一条记录, 包含流程实体所有的其他数据, 包括task, Activiti, comment 等信息
  • HistoryService 删除历史操作
    • deleteHistoricProcessInstance, 采用级联操作, 删除与流程实例相关的所有历史信息
    • deleteHistoricTaskInstance, 范围相对较小, 只删除 Task 及 Task 相关的变量

5.7 ManagementService 管理服务

作用:

  • Job 任务管理
  • 数据库相关通用操作
  • 执行流程引擎命令(Command)

5.7.1 Job 任务管理

工作查询对象 描述
JobQuery 查询一般工作
TimerJobQuery 查询定时任务
SuspendedKobQUery 查询中断工作
DeadLetterJobQuery 查询无法执行的工作(一般重试三次)

5.7.2 数据库相关操作

  • 查询表结构元数据
  • 通用表查询
  • 执行自定义的 sql 查询

5.8 DynamicBpmnService 动态流程定义服务

不推荐使用

6 数据库设计和模型映射

数据表分类 描述
ACT_GE_* 通用数据表
ACT_RE_* 流程定义存储表
ACT_ID_* 身份信息表
ACT_RU_* 运行时数据库表
ACT_HI_* 历史数据库表

6.1 MySql 建表语句

除了核心引擎是必选的, 其他两个不是必须的

  • 核心引擎: activiti.mysql.create.engine.sql
  • 历史数据: activiti.mysql.create.history.sql
  • 身份信息: activiti.mysql.create.identity.sql

6.1.1 通用数据库

数据表分类 描述
ACT_GE_PROPERTY 属性表(保存流程引擎的 key-value 键值属性)
ACT_GE_BYTEARRAY 资源表(存储流程定义相关的资源)

7 BPMN2.0 概述

  • 是一套业务流程模型与符号建模标准
  • 精准的执行语义来描述元素的操作
  • 以 XML 为载体, 以符号可视化业务

BPMN2.0元素:

  • 流对象
  • 连接对象
  • 数据
  • 泳道
  • 描述对象

image
image

7.1 流对象

  • 活动(Activities): User Task, Service Task…
  • 事件(Events): Start Event, End Event…
  • 网关(Gateway): Exclusive Gateway…

8 Activiti 集成 Spring Boot

Activiti6.0 依赖的 SpringBoot 版本是 1.2.6

如果直接与 SpringBoot 2.0.0 集成的话, 会出现 ClassNotFound 等问题

因此在集成 SpringBoot 2.0.0 的时候, 需要 Activiti6.0 源码进行部分改动

升级 Activiti 6.0 依赖 SpringBoot 版本为 2.0.0 的改动

  • 升级 SpringBoot 依赖并解决编译错误
  • 更新 activiti-spring-boot-starter-basic 版本并安装
  • 集成使用 Activiti 的 AutoConfiguration 功能

如果直接将 SpringBoot 2.0.0 和 activiti-spring-boot-starter-basic 6.0.0 集成, 会发生如下错误:

java.lang.IllegalStateException: Failed to load ApplicationContext
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'documentationPluginsBootstrapper' defined in URL [jar:file:/Users/destiny/.m2/repository/io/springfox/springfox-spring-web/2.8.0/springfox-spring-web-2.8.0.jar!/springfox/documentation/spring/web/plugins/DocumentationPluginsBootstrapper.class]: Unsatisfied dependency expressed through constructor parameter 1; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'webMvcRequestHandlerProvider' defined in URL [jar:file:/Users/destiny/.m2/repository/io/springfox/springfox-spring-web/2.8.0/springfox-spring-web-2.8.0.jar!/springfox/documentation/spring/web/plugins/WebMvcRequestHandlerProvider.class]: Unsatisfied dependency expressed through constructor parameter 1; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestMappingHandlerMapping' defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Invocation of init method failed; nested exception is java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExceptionProxy
...
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'webMvcRequestHandlerProvider' defined in URL [jar:file:/Users/destiny/.m2/repository/io/springfox/springfox-spring-web/2.8.0/springfox-spring-web-2.8.0.jar!/springfox/documentation/spring/web/plugins/WebMvcRequestHandlerProvider.class]: Unsatisfied dependency expressed through constructor parameter 1; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestMappingHandlerMapping' defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Invocation of init method failed; nested exception is java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExceptionProxy
...
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestMappingHandlerMapping' defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Invocation of init method failed; nested exception is java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExceptionProxy
...
Caused by: java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExceptionProxy
...

真正的原因是对应的类无法找到

8.1 修改 activiti 源码以适应 SpringBoot 2.0.0 版本升级

8.1.1 将 activiti 中 SpringBoot 依赖升级到 2.0.0

修改 Activiti 源码中 modules/activiti-spring-boot/pom.xml pom 文件, 将其中

1
<spring.boot.version>1.2.6.RELEASE</spring.boot.version>

修改为

1
<spring.boot.version>2.0.0.RELEASE</spring.boot.version>

然后重新编译源码, 此时以下几个类会报错

8.1.1.1 org.activiti.spring.boot.actuate.endpoint.ProcessEngineEndpoint

image

主要的问题是 SpringBoot 从 1 升级到 2 的时候, 对 EndPoint 的使用方式发生了改变:
1.x 是继承一个抽象类 AbstractEndpoint
2.x 修改为使用对应的注解

修改后的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package org.activiti.spring.boot.actuate.endpoint;

import org.activiti.engine.ProcessEngine;
import org.activiti.engine.impl.cfg.ProcessEngineConfigurationImpl;
import org.activiti.engine.impl.persistence.deploy.DefaultDeploymentCache;
import org.activiti.engine.impl.persistence.deploy.DeploymentCache;
import org.activiti.engine.impl.persistence.deploy.ProcessDefinitionCacheEntry;
import org.activiti.engine.repository.ProcessDefinition;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;

import java.util.*;

/**
* Registers a Boot Actuator endpoint that provides information on the
* running process instance and renders BPMN diagrams of the deployed processes.
*
* @author Josh Long
*/
//@ConfigurationProperties(prefix = "endpoints.activiti")
@Endpoint(id = "activiti") // 使用注解 Endpoint
public class ProcessEngineEndpoint { // 不再继承 AbstractEndpoint

private final ProcessEngine processEngine;

public ProcessEngineEndpoint(ProcessEngine processEngine) {
this.processEngine = processEngine; // 不再继承 Endpoint, 因此不需要调用 super 构造方法
}

@ReadOperation // 不需要 @Override
public Map<String, Object> activiti() {

Map<String, Object> metrics = new HashMap<String, Object>();

// Process definitions
metrics.put("processDefinitionCount", processEngine.getRepositoryService().createProcessDefinitionQuery().count());

// List of all process definitions
List<ProcessDefinition> processDefinitions = processEngine.getRepositoryService().createProcessDefinitionQuery().orderByProcessDefinitionKey().asc().list();
List<String> processDefinitionKeys = new ArrayList<String>();
for (ProcessDefinition processDefinition : processDefinitions) {
processDefinitionKeys.add(processDefinition.getKey() + " (v" + processDefinition.getVersion() + ")");
}
metrics.put("deployedProcessDefinitions", processDefinitionKeys);

// Process instances
Map<String, Object> processInstanceCountMap = new HashMap<String, Object>();
metrics.put("runningProcessInstanceCount", processInstanceCountMap);
for (ProcessDefinition processDefinition : processDefinitions) {
processInstanceCountMap.put(processDefinition.getKey() + " (v" + processDefinition.getVersion() + ")",
processEngine.getRuntimeService().createProcessInstanceQuery().processDefinitionId(processDefinition.getId()).count());
}
Map<String, Object> completedProcessInstanceCountMap = new HashMap<String, Object>();
metrics.put("completedProcessInstanceCount", completedProcessInstanceCountMap);
for (ProcessDefinition processDefinition : processDefinitions) {
completedProcessInstanceCountMap.put(processDefinition.getKey() + " (v" + processDefinition.getVersion() + ")",
processEngine.getHistoryService().createHistoricProcessInstanceQuery().finished().processDefinitionId(processDefinition.getId()).count());
}

// Open tasks
metrics.put("openTaskCount", processEngine.getTaskService().createTaskQuery().count());
metrics.put("completedTaskCount", processEngine.getHistoryService().createHistoricTaskInstanceQuery().finished().count());


// Tasks completed today
metrics.put("completedTaskCountToday", processEngine.getHistoryService().createHistoricTaskInstanceQuery().finished().taskCompletedAfter(
new Date(System.currentTimeMillis() - secondsForDays(1))).count());

// Process steps
metrics.put("completedActivities", processEngine.getHistoryService().createHistoricActivityInstanceQuery().finished().count());

// Process definition cache
DeploymentCache<ProcessDefinitionCacheEntry> deploymentCache = ((ProcessEngineConfigurationImpl) processEngine.getProcessEngineConfiguration()).getProcessDefinitionCache();
if (deploymentCache instanceof DefaultDeploymentCache) {
metrics.put("cachedProcessDefinitionCount", ((DefaultDeploymentCache) deploymentCache).size());
}
return metrics;
}

private long secondsForDays(int days) {
int hour = 60 * 60 * 1000;
int day = 24 * hour;
return days * day;
}
}

8.1.1.2 org.activiti.spring.boot.actuate.endpoint.ProcessEngineMvcEndpoint

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package org.activiti.spring.boot.actuate.endpoint;

import java.io.InputStream;

import org.activiti.bpmn.BpmnAutoLayout;
import org.activiti.bpmn.model.BpmnModel;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.repository.ProcessDefinition;
import org.activiti.image.ProcessDiagramGenerator;
import org.activiti.image.impl.DefaultProcessDiagramGenerator;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

/**
* Renders a valid running BPMN process definition as a BPMN diagram.
*
* This is duplicative of the functionality in the full REST API implementation.
*
* @author Joram Barrez
* @author Josh Long
*/
public class ProcessEngineMvcEndpoint { // 同样不再继承 EndpointMvcAdapter

private final RepositoryService repositoryService;
private final ProcessEngineEndpoint processEngineEndpoint; // 创建一个 ProcessEngineEndpoint 私有变量

public ProcessEngineMvcEndpoint(ProcessEngineEndpoint processEngineEndpoint, RepositoryService repositoryService) {
// super(processEngineEndpoint);
this.processEngineEndpoint = processEngineEndpoint;
this.repositoryService = repositoryService;
}

/**
* Look up the process definition by key. For example,
* this is <A href="http://localhost:8080/activiti/processes/fulfillmentProcess">process-diagram for</A>
* a process definition named {@code fulfillmentProcess}.
*/
@RequestMapping(value = "/processes/{processDefinitionKey:.*}", method = RequestMethod.GET, produces = MediaType.IMAGE_JPEG_VALUE)
@ResponseBody
public ResponseEntity processDefinitionDiagram(@PathVariable String processDefinitionKey) {
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.processDefinitionKey(processDefinitionKey)
.latestVersion()
.singleResult();
if (processDefinition == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}

ProcessDiagramGenerator processDiagramGenerator = new DefaultProcessDiagramGenerator();
BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinition.getId());

if (bpmnModel.getLocationMap().size() == 0) {
BpmnAutoLayout autoLayout = new BpmnAutoLayout(bpmnModel);
autoLayout.execute();
}

InputStream is = processDiagramGenerator.generateJpgDiagram(bpmnModel);
return ResponseEntity.ok(new InputStreamResource(is));
}

}

8.1.1.3 org.activiti.spring.boot.EndpointAutoConfiguration

image

报错原因: SpringBoot2 不再使用 AbstractEndpoint
开启该表达式 AutoConfiguration 不会生效, 因此需要删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package org.activiti.spring.boot;

import org.activiti.engine.ProcessEngine;
import org.activiti.engine.RepositoryService;
import org.activiti.spring.boot.actuate.endpoint.ProcessEngineEndpoint;
import org.activiti.spring.boot.actuate.endpoint.ProcessEngineMvcEndpoint;
//import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* The idea behind this module is that Spring Security could
* talk to the {@link org.activiti.engine.IdentityService}
* as required.
*
* @author Josh Long
*/
@Configuration
//@ConditionalOnClass (name = "org.springframework.boot.actuate.endpoint.AbstractEndpoint")
public class EndpointAutoConfiguration {

@Bean
public ProcessEngineEndpoint processEngineEndpoint(ProcessEngine engine) {
return new ProcessEngineEndpoint(engine);
}

@Bean
public ProcessEngineMvcEndpoint processEngineMvcEndpoint(
ProcessEngineEndpoint engineEndpoint, RepositoryService repositoryService) {
return new ProcessEngineMvcEndpoint(engineEndpoint, repositoryService);
}
}

8.1.1.4 org.activiti.spring.boot.DataSourceProcessEngineAutoConfiguration

image

报错原因: ConditionalOnMissingClass 注解中的 name 已经不能使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package org.activiti.spring.boot;

import java.io.IOException;

import javax.sql.DataSource;

import org.activiti.spring.SpringAsyncExecutor;
import org.activiti.spring.SpringProcessEngineConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

/**
* @author Joram Barrez
* @author Josh Long
*/
@Configuration
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class DataSourceProcessEngineAutoConfiguration {

@Configuration
// @ConditionalOnMissingClass(name= "javax.persistence.EntityManagerFactory")
@ConditionalOnMissingClass(value= "javax.persistence.EntityManagerFactory") // 将 name 替换为 value
@EnableConfigurationProperties(ActivitiProperties.class)
public static class DataSourceProcessEngineConfiguration extends AbstractProcessEngineAutoConfiguration {

@Bean
@ConditionalOnMissingBean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}

@Bean
@ConditionalOnMissingBean
public SpringProcessEngineConfiguration springProcessEngineConfiguration(
DataSource dataSource,
PlatformTransactionManager transactionManager,
SpringAsyncExecutor springAsyncExecutor) throws IOException {

return this.baseSpringProcessEngineConfiguration(dataSource, transactionManager, springAsyncExecutor);
}
}

}

8.1.1.4 org.activiti.spring.boot.SecurityAutoConfiguration

image

报错原因: 包结构发生改变, 导致原有的类全路径不可用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import org.activiti.engine.IdentityService;
import org.activiti.rest.security.BasicAuthenticationProvider;
import org.activiti.spring.security.IdentityServiceUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configurers.GlobalAuthenticationConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;

/**
* Installs a Spring Security adapter for the Activiti
* {@link org.activiti.engine.IdentityService}.
*
* @author Josh Long
*/
@Configuration
//@AutoConfigureBefore(org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration.class)
@AutoConfigureBefore(org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class) // 修改类路径
public class SecurityAutoConfiguration {

@Configuration
@ConditionalOnClass( UserDetailsService.class)
public static class UserDetailsServiceConfiguration
extends GlobalAuthenticationConfigurerAdapter {

@Override
public void init(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService( userDetailsService());
}

@Bean
public UserDetailsService userDetailsService() {
return new IdentityServiceUserDetailsService(this.identityService);
}

@Autowired
private IdentityService identityService;
}

@Configuration
@ConditionalOnClass(name = {"org.activiti.rest.service.api.RestUrls", "org.springframework.web.servlet.DispatcherServlet"})
@EnableWebSecurity
public static class SecurityConfiguration extends WebSecurityConfigurerAdapter {

@Bean
public AuthenticationProvider authenticationProvider() {
return new BasicAuthenticationProvider();
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authenticationProvider(authenticationProvider())
.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic();
}
}
}

8.2 对 Activiti 版本做更改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>


<parent>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starters</artifactId>
<version>6.0.0</version>
</parent>

<artifactId>activiti-spring-boot-starter-basic</artifactId>
<!-- 在此处新增版本 -->
<version>6.0.0-boot2</version>
<!-- <properties> <spring.framework.version>4.1.4.RELEASE</spring.framework.version>
</properties> -->

...
</project>

尝试执行 mvn clean install

此时 maven 会去尝试下载 6.0.0-boot2 版本, 但显然公网仓库中不会存在

[INFO] Scanning for projects...
[INFO] 
[INFO] ----------< org.activiti:activiti-spring-boot-starter-basic >-----------
[INFO] Building activiti-spring-boot-starter-basic 6.0.0-boot2
[INFO] --------------------------------[ jar ]---------------------------------
[WARNING] The POM for org.activiti:activiti-engine:jar:6.0.0-boot2 is missing, no dependency information available
[WARNING] The POM for org.activiti:activiti-spring:jar:6.0.0-boot2 is missing, no dependency information available
[WARNING] The POM for org.activiti:activiti-rest:jar:6.0.0-boot2 is missing, no dependency information available
[WARNING] The POM for org.activiti:activiti-common-rest:jar:6.0.0-boot2 is missing, no dependency information available
[WARNING] The POM for org.activiti:activiti-image-generator:jar:6.0.0-boot2 is missing, no dependency information available
[WARNING] The POM for org.activiti:activiti-bpmn-model:jar:6.0.0-boot2 is missing, no dependency information available
[WARNING] The POM for org.activiti:activiti-bpmn-layout:jar:6.0.0-boot2 is missing, no dependency information available
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.850 s
[INFO] Finished at: 2018-12-03T23:15:57+08:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal on project activiti-spring-boot-starter-basic: Could not resolve dependencies for project org.activiti:activiti-spring-boot-starter-basic:jar:6.0.0-boot2: The following artifacts could not be resolved: org.activiti:activiti-engine:jar:6.0.0-boot2, org.activiti:activiti-spring:jar:6.0.0-boot2, org.activiti:activiti-rest:jar:6.0.0-boot2, org.activiti:activiti-common-rest:jar:6.0.0-boot2, org.activiti:activiti-image-generator:jar:6.0.0-boot2, org.activiti:activiti-bpmn-model:jar:6.0.0-boot2, org.activiti:activiti-bpmn-layout:jar:6.0.0-boot2: Failure to find org.activiti:activiti-engine:jar:6.0.0-boot2 in http://maven.aliyun.com/nexus/content/groups/public was cached in the local repository, resolution will not be reattempted until the update interval of nexus-aliyun has elapsed or updates are forced -> [Help 1]
[ERROR] 
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR] 
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/DependencyResolutionException

错误原因: activiti-spring-boot-starter-basic 的版本号被修改为 6.0.0-boot2, 但安装的时候, 对应的 activiti-engine 版本只有 6.0.0, 并没有 6.0.0-boot2
可以将所有需要找 6.0.0-boot2 的版本修改为去找 6.0.0

将根 pom 中所有的 ${project.version} 修改为 6.0.0

>