with spring boot
两种部署方式
一种是与spring boot 集成 称为Activiti Core
一种是使用docker 容器? 装一堆东西称为Activiti Cloud
Getting Started
依赖 pom
pom 修改
<!-- https://mvnrepository.com/artifact/org.activiti/activiti-spring-boot-starter -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter</artifactId>
<version>x.x.x</version>
</dependency>
<!-- 还有数据库依赖 -->
<!-- 中央仓库只有 7.x.M6, 以后的版本发布在私服 -->
<repositories>
<repository>
<id>activiti-releases</id>
<url>https://artifacts.alfresco.com/nexus/content/repositories/activiti-releases</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>自动建表ACT_RE_DEPLOYMENT会少了两个字段 VERSION_ PROJECT_RELEASE_VERSION_
手动添加之 正常结构注意顺序
CREATE TABLE `act_re_deployment` (
`ID_` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL DEFAULT '',
`NAME_` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
`CATEGORY_` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
`KEY_` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
`TENANT_ID_` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT '',
`DEPLOY_TIME_` timestamp(3) NULL DEFAULT NULL,
`ENGINE_VERSION_` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
`VERSION_` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
`PROJECT_RELEASE_VERSION_` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
PRIMARY KEY (`ID_`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;配置 yml
spring:
############ activiti 工作流
activiti:
#表示启动时检查数据库表, 不存在则创建
database-schema-update: true
#表示哪种情况下使用历史表, 这里配置为full表示全部记录历史, 方便绘制流程图
history-level: full
#true表示使用历史表, 如果不配置, 则工程启动后可以检查数据库, 只建立了17张表
db-history-used: true-- 清空工作流 表数据
delete from act_ru_identitylink;
delete from act_ru_variable;
delete from act_ru_task;
delete from act_ru_execution;
delete from act_ge_bytearray;
delete from act_re_deployment;
delete from act_re_procdef
delete from act_hi_varinst
--元数据 视版本而定
--[
INSERT INTO `act_ge_property` VALUES ('cfg.execution-related-entities-count''false'1);
INSERT INTO `act_ge_property` VALUES ('next.dbid''1'1);
INSERT INTO `act_ge_property` VALUES ('schema.history''create(7.0.0.0) upgrade(7.0.0.0->7.1.0.0)'2);
INSERT INTO `act_ge_property` VALUES ('schema.version''7.1.0.0'2);
]--官方的简单示例
参考官方的一个示例项目 activiti-api-basic-full-example-bean
建模文件 categorize-human-content.bpmn20.xml
放到classpath:/processes/categorize-human-content.bpmn20.xml 启动后会自动部署
启动 run
@Override
public void run(String... args) {
//
securityUtil.logInAs("system");
Page<ProcessDefinition> processDefinitionPage = processRuntime.processDefinitions(Pageable.of(010));
logger.info("> Available Process definitions: " + processDefinitionPage.getTotalItems());
for (ProcessDefinition pd: processDefinitionPage.getContent()) {
logger.info("\t > Process definition: " + pd);
}
}
用户鉴权
securityUtil.logInAs("system");
用的是spring 用户 SecurityUtil.java
public void logInAs(String username) {
UserDetails user = userDetailsService.loadUserByUsername(username);
if (user == null) {
throw new IllegalStateException("User " + username + " doesn't existplease provide a valid user");
}
logger.info("> Logged in as: " + username);
SecurityContextHolder.setContext(new SecurityContextImpl(new Authentication() {
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getAuthorities();
}
@Override
public Object getCredentials() {
return user.getPassword();
}
@Override
public Object getDetails() {
return user;
}
@Override
public Object getPrincipal() {
return user;
}
@Override
public boolean isAuthenticated() {
return true;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
}
@Override
public String getName() {
return user.getUsername();
}
}));
org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(username);
}
踩坑指南
用户必须具有ROLE_ACTIVITI_USER 角色代码 才可以参与流程访问
文档中有那么一句话 Something important to notice hereis that in order to interact with the TaskRuntime API as a useryou need to have the role: ACTIVITI_USER (Granted Authority: ROLE_ACTIVITI_USER) .
初始化内存用户的时候, 也有这个角色 ROLE_ACTIVITI_USER 代码 DemoApplicationConfiguration.java
public UserDetailsService myUserDetailsService() {
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
String[][] usersGroupsAndRoles = {
{"salaboy""password""ROLE_ACTIVITI_USER""GROUP_activitiTeam"},
{"ryandawsonuk""password""ROLE_ACTIVITI_USER""GROUP_activitiTeam"},
{"erdemedeiros""password""ROLE_ACTIVITI_USER""GROUP_activitiTeam"},
{"other""password""ROLE_ACTIVITI_USER""GROUP_otherTeam"},
{"system""password""ROLE_ACTIVITI_USER"},
{"admin""password""ROLE_ACTIVITI_ADMIN"},
};
for (String[] user: usersGroupsAndRoles) {
List<String> authoritiesStrings = Arrays.asList(Arrays.copyOfRange(user2user.length));
logger.info("> Registering new user: " + user[0] + " with the following Authorities[" + authoritiesStrings + "]");
inMemoryUserDetailsManager.createUser(new User(user[0]passwordEncoder().encode(user[1]),
authoritiesStrings.stream().map(s -> new SimpleGrantedAuthority(s)).collect(Collectors.toList())));
}
return inMemoryUserDetailsManager;
}查询已部署的流程
Page<ProcessDefinition> processDefinitionPage = processRuntime.processDefinitions(Pageable.of(010));
logger.info("> Available Process definitions: " + processDefinitionPage.getTotalItems());
for (ProcessDefinition pd: processDefinitionPage.getContent()) {
logger.info("\t > Process definition: " + pd);
}out
Available Process definitions: 1 Process definition: ProcessDefinition{id=‘categorizeHumanProcess:1:2e3392e1-c64b-11ea-b436-005056c00008’name=‘categorizeHumanProcess’key=‘categorizeHumanProcess’description=‘null’formKey=‘null’version=1}
启动流程 processText
@Scheduled(initialDelay = 1000fixedDelay = 5000)
public void processText() {
securityUtil.logInAs("system");
//随机 内容 dto
Content content = pickRandomString();
SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yy HH:mm:ss");
logger.info("> Starting process to process content: " + content + " at " + formatter.format(new Date()));
//ProcessPayloadBuilder 负载流程 builder
//使用 processRuntime 对象启动它; 返回流程实例
ProcessInstance processInstance = processRuntime.start(ProcessPayloadBuilder
.start()
//key 对应上面查到ProcessDefinition 的key; (其实是 act_re_procdef 表的key字段?); 最终是 bpmn20.xml 定义的id
.withProcessDefinitionKey("categorizeHumanProcess")
.withName("Processing Content: " + content)
.withVariable("content"content)
.build());
// 开始流程后会在 act_ru_task 插一条记录
logger.info(">>> Created Process Instance: " + processInstance);
}
// Content 对象是个DTO 有以下属性
public class Content {
private String body;
private boolean approved;
private List<String> tags;
....
}
用户处理任务 checkAndWorkOnTasksWhenAvailable
@Scheduled(initialDelay = 1000fixedDelay = 5000)
public void checkAndWorkOnTasksWhenAvailable() {
securityUtil.logInAs("salaboy");//鉴权用户
//获取 查询任务列表// 在xml 定义<bpmn:expression xsi:type="bpmn:tFormalExpression">activitiTeam</bpmn:expression>
//流程activitiTeam用户组只有这个组的用户才能查询到改任务// so act_ru_identitylink
Page<Task> tasks = taskRuntime.tasks(Pageable.of(010));
if (tasks.getTotalItems() > 0) {
for (Task t: tasks.getContent()) {
logger.info("> Claiming task: " + t.getId());
//注意必须先声明任务, 声明之后, 没有人可以看到任务才可以完成任务 (事务?)
taskRuntime.claim(TaskPayloadBuilder.claim().withTaskId(t.getId()).build());
//拿到流程的 withVariable 变量
List<VariableInstance> variables = taskRuntime.variables(TaskPayloadBuilder.variables().withTaskId(t.getId()).build());
VariableInstance variableInstance = variables.get(0);
if (variableInstance.getName().equals("content")) {
Content contentToProcess = variableInstance.getValue();
logger.info("> Content received inside the task to approve: " + contentToProcess);
if (contentToProcess.getBody().contains("activiti")) {
logger.info("> User Approving content");
//用户同意
contentToProcess.setApproved(true);
} else {
logger.info("> User Discarding content");
//用户拒绝
contentToProcess.setApproved(false);
}
//完成任务
taskRuntime.complete(TaskPayloadBuilder.complete()
.withTaskId(t.getId()).withVariable("content"contentToProcess).build());
}
}
} else {
logger.info("> There are no task for me to work on.");
}
}网关 顺序流表达式处理
图例(各种箭头) Expression 配置了 ${content.approved == false}
所以当用户同意时设置contentToProcess.setApproved(true);值
完成任务时 taskRuntime.complete(TaskPayloadBuilder.complete().withTaskId(t.getId()).withVariable("content"contentToProcess).build());会导致网关执行不同的顺序流向.
连接器
根据不同的 顺序流向. 执行这两个连接器的代码..
@Bean
public Connector tagTextConnector() {
return integrationContext -> {
Content contentToTag = (Content) integrationContext.getInBoundVariables().get("content");
contentToTag.getTags().add(" :) ");
integrationContext.addOutBoundVariable("content",
contentToTag);
logger.info("Final Content: " + contentToTag);
return integrationContext;
};
}
@Bean
public Connector discardTextConnector() {
return integrationContext -> {
Content contentToDiscard = (Content) integrationContext.getInBoundVariables().get("content");
contentToDiscard.getTags().add(" :( ");
integrationContext.addOutBoundVariable("content",
contentToDiscard);
logger.info("Final Content: " + contentToDiscard);
return integrationContext;
};
}对应XML定义
-
首先定义了一个流程id name
<bpmn:process id="categorizeHumanProcess" name="categorizeHumanProcess" isExecutable="true" camunda:versionTag="2"> -
定义一个开始事件
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_09xowo4</bpmn:outgoing>
</bpmn:startEvent>- 顺序流 接受开始事件指向一个用户任务
Task_1ylvdew
<bpmn:sequenceFlow id="SequenceFlow_09xowo4" sourceRef="StartEvent_1" targetRef="Task_1ylvdew" />Task_1ylvdew用户任务定义
<bpmn:userTask id="Task_1ylvdew" name="Process Content">
<bpmn:incoming>SequenceFlow_09xowo4</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1jzbgkj</bpmn:outgoing>
<bpmn:potentialOwner>
<bpmn:resourceAssignmentExpression>
<bpmn:formalExpression>activitiTeam</bpmn:formalExpression>
</bpmn:resourceAssignmentExpression>
</bpmn:potentialOwner>
</bpmn:userTask>- 又一个顺序流 从任务
Task_1ylvdew到网关ExclusiveGateway_0c36qc6
<bpmn:sequenceFlow id="SequenceFlow_1jzbgkj" sourceRef="Task_1ylvdew" targetRef="ExclusiveGateway_0c36qc6" />- 网关 单纯只是分叉(执行顺序流表达式?), 从顺序流
SequenceFlow_1jzbgkj到SequenceFlow_0tsc63v和SequenceFlow_049fuit两个顺序流
<bpmn:exclusiveGateway id="ExclusiveGateway_0c36qc6" name="Content Accepted?">
<bpmn:incoming>SequenceFlow_1jzbgkj</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0tsc63v</bpmn:outgoing>
<bpmn:outgoing>SequenceFlow_049fuit</bpmn:outgoing>
</bpmn:exclusiveGateway>- 这个两个顺序流有个表达式, 根据
content.approved变量决定 流向
<bpmn:sequenceFlow id="SequenceFlow_0tsc63v" name="yes" sourceRef="ExclusiveGateway_0c36qc6" targetRef="Task_0snvh02">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">${content.approved == true}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="SequenceFlow_049fuit" name="no" sourceRef="ExclusiveGateway_0c36qc6" targetRef="Task_1asxw87">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">${content.approved == false}</bpmn:conditionExpression>
</bpmn:sequenceFlow>- 两个分支的任务(连接器?)实现名称是
tagTextConnector和discardTextConnector
<bpmn:serviceTask id="Task_0snvh02" name="Tag categorized Content" implementation="tagTextConnector">
<bpmn:incoming>SequenceFlow_0tsc63v</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1nn2llw</bpmn:outgoing>
</bpmn:serviceTask>
<bpmn:serviceTask id="Task_1asxw87" name="Discard and Notify user" implementation="discardTextConnector">
<bpmn:incoming>SequenceFlow_049fuit</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0upfncf</bpmn:outgoing>
</bpmn:serviceTask>- 最后流向结束事件
<bpmn:sequenceFlow id="SequenceFlow_0upfncf" sourceRef="Task_1asxw87" targetRef="EndEvent_13bsqqd" />
<bpmn:sequenceFlow id="SequenceFlow_1nn2llw" sourceRef="Task_0snvh02" targetRef="EndEvent_1ogwwp9" />
这中间有一个
Content的数据对象
userTask 指定办理人
必须在 XML 中定义!!!
- 在xml 中的任务(圆角矩形)
Assignee属性 指定办理人
<bpmn:userTask id="Task_1ylvdew" name="Process Content" camunda:assignee="${username}">注意这个前缀应该是 activiti:assignee=视建模软件会改变我K!!
然而部署总是 前缀错误最终..
<bpmn:userTask id="Task_1ylvdew" name="Process Content" >
<bpmn:incoming>SequenceFlow_09xowo4</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1jzbgkj</bpmn:outgoing>
<bpmn:potentialOwner>
<bpmn:resourceAssignmentExpression>
<!-- user(指定用户) group(指定用户组) -->
<bpmn:formalExpression>user(${username})group(management)</bpmn:formalExpression>
</bpmn:resourceAssignmentExpression>
</bpmn:potentialOwner>
</bpmn:userTask>- 或者在xml 中的任务(圆角矩形)
Assignee属性使用占位符 ${username} 然后通过API动态给variable赋值
StartProcessPayload precess = ProcessPayloadBuilder.start()
.withProcessDefinitionKey("categorizeHumanProcess")
.withVariable("username""admin")踩坑指南
startProcessInstance 无用户任务?
runtimeService::startProcessInstanceByKey 开始流程后, 无用户任务问题, act_ru_task 表亦无记录;
原因是 UserTask的Asynchronous属性为;
勾选用户任务Asynchronous, 流程启动后, 开始节点完成后, 验证下一个节点的地址操作交给定时任务异步来完成, 定时任务执行时间和次数可在act_ru_job表中查看, 用户任务还没在任务表中生成, 默认等待5分钟后才会在act_ru_task表生成用户任务
但是有时我想一个节点完成了就提交事务, 不要依赖下一个节点, 也就是不管下一个节点发生什么都不关已完成的节点的事; 要实现这种, 可使用Asynchronous来实现完成任务在一个事务中执行, 验证下一个节点的地址操作交给定时任务异步来完成;
手动获取定时任务并执行:
Map variableMap = new HashMap();
ProcessInstance pi = runtimeService.startProcessInstanceByKey("TestBook", variableMap);
Job job = managementService.createJobQuery().executionId(pi.getId()).singleResult();
managementService.executeJob(job.getId());