Activiti
Activiti is a workflow and Business Process Management platform
版本 7.0.0GA
上一次接触到工作流引擎还是刚毕业的时候,那个时候还是JBPM
安装 可视化设计器
工欲善其事必先利其器,网上找找activiti入门的资料往往能搜到长的像这样的xml文件(而且一般又臭又长。。。):
<?xml version="1.0" encoding="UTF-8"?>
<definitions id="taskAssigneeExample"
xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:activiti="http://activiti.org/bpmn"
targetNamespace="org.activiti.examples">
<process id="LoanRequestProcess" name="Process creating and handling loan request">
...
</process>
</definitions>
这个xml文件对应了一个 BpmnModel
,实际上,我们无需处理手动处理这种xml文件,通过 https://www.activiti.org/userguide/#activitiDesigner 来安装设计器(eclipse),安装完毕之后就可以可视化设计了,如图:
这里的 id 很迷惑,启动工作流实例 有一个方法 叫做
startProcessInstanceByKey
,其实这里的 id 就是这个方法里面的 key,那么startProcessInstanceById
这个方法里面的 id又是什么呢?我没去看过表,但应该是工作流实例持久化后的数据库记录 id。
制作完想要的流程图之后就会自动生成xml文件,在和Spring Boot
的集成中,必须要将制作好的xml文件放到 resources/process 文件夹中,并且文件名必须以 bpmn20.xml结尾
基础信息
术语
下面是我接触到的几个术语:
ProcessDefinition
流程的定义,按我的理解,就是你画的那张流程图
ProcessInstance
某个人按照你的流程图开始执行一次流程,就是一个流程处理实例
Task
就是流程处理过中用户需要处理的任务(比如审核)
流程表
启动应用的过程中,activiti会自动创建一些表用于流程的处理,请见 https://www.activiti.org/userguide/#creatingDatabaseTable ,我认为这些表知道大概的意思就好,直接通过 sql 来操作这些表实在是下下下策。
另外在测试过程中,我发现历史表并没有被创建,查看了文档后,在application.properties
中增加了下面一条后便能自动创建了
spring.activiti.db-history-used=true
用户和用户组
在 UserTask
中,我们可以直接指定某一个人或者某一些用户或者某一些用户组来处理这个任务
assignee:直接指定具体人员
candidateUser:候选人,可以通过,
分割来指定多个用户,这些用户可以认领
candidateGroup:候选组,可以通过,
分割来指定多个组,所有组内的用户都可以认领任务
candidateUser和candidateGroup可以同时指定,测试了一下因该是并集的关系
如果直接指定了特定的用户,那么用户可以直接完成该任务,如果指定了用户组或者候选人,那么这个用户组下面的所有用户或者候选人都可以看见这个任务,但并不意味着用户组下面的每个用户都得去处理这个任务,这个中间还有一个认领的过程,用户组下的某个用户只有在认领完成之后,才能执行完成任务的操作
用户、用户组和Spring Security的集成
activiti
已经自带了和Spring Security
的集成,通过官方提供的样例代码,可以了解的更多:
@Bean
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" },
{ "admin", "password", "ROLE_ACTIVITI_ADMIN" }, };
for (String[] user : usersGroupsAndRoles) {
List<String> authoritiesStrings = Arrays.asList(Arrays.copyOfRange(user, 2, user.length));
System.out.println(authoritiesStrings);
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;
}
可以看到,GROUP_
开头的角色代表了一个用户组,而在activiti源码中,是这样获取用户组的:
@Component
public class ActivitiUserGroupManagerImpl implements UserGroupManager {
private final UserDetailsService userDetailsService;
public ActivitiUserGroupManagerImpl(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
public List<String> getUserGroups(String username) {
return userDetailsService.loadUserByUsername(username).getAuthorities().stream()
.filter((GrantedAuthority a) -> a.getAuthority().startsWith("GROUP_"))
.map((GrantedAuthority a) -> a.getAuthority().substring(6))
.collect(Collectors.toList());
}
}
也就是说,我们甚至什么都不需要变动,只需要增加几个GROUP_
开头的角色,就可以无缝集成了?
简单API
它的API看的我云里雾里,看了好多文档,就完成任务一个操作都有好几个API,这里只说下我用到的几个api,主要针对于普通用户操作,下面几个api都会进行内部的权限校验:
流程操作
开启流程
processRuntime.start(ProcessPayloadBuilder.start().withBusinessKey(entityId).withProcessDefinitionKey(definitionKey).build())
任务操作
分页查询代办任务
taskRuntime.tasks(Pageable.of(0,10))
认领任务
taskRuntime.claim(TaskPayloadBuilder.claim().withTaskId(taskId).build())
取消认领
taskRuntime.release(TaskPayloadBuilder.release().withTaskId(taskId).build());
完成任务
Task task = taskRuntime.task("");
engine.getTaskService().addComment(task.getId(), task.getProcessInstanceId(), "xxx");
taskRuntime.complete(TaskPayloadBuilder.complete().withTaskId(taskId).withVariable(name, value).build());
一些其他的api,比如完成一个任务也可以用 engine.getTaskService().complete(taskId,variables)
,我认为这些api更倾向于管理员,实际上,taskRuntime.complete内部也是调用了这个api
Flow 流向
flow在我的理解下就是流程图里带箭头的线,通过指定condition表达式就可以控制它的方向
condition表达式可以指定流程变量,而流程变量可以在开始流程的时候设置,也可以在完成任务的时候设置,动态添加也可以,例如:
taskRuntime.complete(TaskPayloadBuilder.complete().withVariable("task1_output", "APPROVTE").withTaskId(taskId).build());
流程通过|拒绝状态变更
在activiti中,流程只有结束|未结束的区别,但对实际的业务来说,应该是通过|未通过的状态,例如:
这里的通过和未通过实际上都代表着流程的结束,那么如何区分这两个情况呢,事件监听是一种方法,在结束节点上,加上一个流程结束的监听器:
监听器执行了一个表达式,${bpmnService.finish(execution,'REJECT')}
,bpmnService
是一个spring bean,执行了finish
方法,execution
是ExecutionEntity
(具体不清楚),可以用来获取processInstanceId
,REJECT
代表任务结束时的状态,这样我们就可以统一处理这种情况:
@Component
public class BpmnService {
private final ApplicationEventPublisher publisher;
private final ProcessEngine engine;
public BpmnService(ApplicationEventPublisher publisher, ProcessEngine engine) {
super();
this.publisher = publisher;
this.engine = engine;
}
public void finish(ExecutionEntity entity, String status) {
ProcessInstance instance = engine.getRuntimeService().createProcessInstanceQuery()
.processInstanceId(entity.getProcessInstanceId()).singleResult();
String definitionKey = instance.getProcessDefinitionKey();
String businessKey = instance.getBusinessKey();
publisher.publishEvent(new BpmnEvent(this, status, definitionKey, businessKey));
}
}
这里我们发布了一个事件,因为不同的业务单据,需要不同的处理,所以这里只是封装了一个事件,让其他业务服务来处理这个事件,例如:
@Service
public class TestService {
@EventListener(classes = BpmnEvent.class)
public void listenBpmnEvent(BpmnEvent event) {
if ("myProcess".equals(event.getProcessDefinitionKey())) {
// update business entity status
}
}
}
打印历史记录
通过HistoryService
可以查看单据的审核过程:
HistoryService hs = engine.getHistoryService();
List<HistoricTaskInstance> list = hs.createHistoricTaskInstanceQuery().processInstanceId(id)
.orderByTaskCreateTime().asc().orderByTaskAssignee().asc().list();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (HistoricTaskInstance ins : list) {
if (ins.getAssignee() == null)
continue;
String date = ins.getEndTime() == null ? "" : sdf.format(ins.getEndTime());
HistoricVariableInstance hvi = hs.createHistoricVariableInstanceQuery().taskId(ins.getId())
.variableName(ins.getTaskDefinitionKey() + "_signal").singleResult();
String status = hvi == null ? "未知" : getStatus(hvi.getValue());
List<Comment> comments = engine.getTaskService().getTaskComments(ins.getId());
String commentStr = comments.stream().map(Comment::getFullMessage)
.collect(Collectors.joining(System.lineSeparator()));
System.out.println(date + " " + ins.getAssignee() + " " + commentStr);
}
private String getStatus(Object v) {
String vs = Objects.toString(v);
if ("approve".equalsIgnoreCase(vs))
return "通过";
if ("reject".equalsIgnoreCase(vs))
return "拒绝";
if ("revoke".equalsIgnoreCase(vs))
return "驳回";
return "未知";
}
最终效果如下:
demo
https://github.com/mhlx/activiti_demo 这个demo参考了 https://dzone.com/articles/getting-started-activiti-and 和官方的demo