当前 Activiti 版本
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter</artifactId>
<version>7.1.0.M4</version>
</dependency>
<dependency>
<groupId>org.activiti.dependencies</groupId>
<artifactId>activiti-dependencies</artifactId>
<version>7.1.0.M4</version>
<type>pom</type>
</dependency>
这个版本自动创建的数据库 ACT_RE_DEPLOYMENT 缺少两个字段自己添加一下
PROJECT_RELEASE_VERSION_
VERSION_
deployment: 添加资源文件,获取部署信息,部署时间等
@Autowired
private RepositoryService repositoryService;
/**
* 流程部署测试 1
* 影响的表
* act_re_procdef 流程定义表
* act_re_deployment 流程部署表
* act_ge_bytearray 通用的流程定义和流程资源
*
* ge 代表 general 通用的
* re 代表 repository
*/
@Test
public void initDeploymentBPMN(){
String fileName= "BPMN/part1_deployment.bpmn";
String imageName = "BPMN/part1_deployment.png";
Deployment deployment = repositoryService.createDeployment()
.addClasspathResource(fileName)
.addClasspathResource(imageName)
.name("部署流程测试_v1")
.deploy();
System.out.println(deployment.getName());
}
/**
* 流程部署测试 zip
*/
@Test
public void initDeploymentZIP(){
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("BPMN/part1_deployment.zip");
ZipInputStream zip = new ZipInputStream(resourceAsStream);
Deployment deployment = repositoryService.createDeployment()
.addZipInputStream(zip)
.name("流程部署测试_zip")
.deploy();
System.out.println(deployment.getName());
}
/**
* 查询部署流程列表
*/
@Test
public void getDeployments(){
List<Deployment> list = repositoryService.createDeploymentQuery().list();
list.forEach(deployment -> {
System.out.println("id: "+ deployment.getId());
System.out.println("name: " + deployment.getName());
System.out.println("deploymentTime: " + deployment.getDeploymentTime());
System.out.println("key: " + deployment.getKey());
});
}
processDefinition 获取版本号,key,资源名称,部署 id 等
@Autowired
private RepositoryService repositoryService;
// 查询流程定义
@Test
public void getDefinition(){
List<ProcessDefinition> list = repositoryService.createProcessDefinitionQuery().list();
for (ProcessDefinition pd : list) {
System.out.println("name :" + pd.getName());
System.out.println("key :" + pd.getKey());
System.out.println("name :" + pd.getResourceName());
System.out.println("deploymentId :" + pd.getDeploymentId());
System.out.println("version :" + pd.getVersion());
}
}
/**
* 删除流程定义
*/
@Test
public void delDefinition(){
String pid = "f434aaf6-28eb-11eb-8d4d-408d5c97c1d5";
// 参数一: 流程定义id
// 参数二: true 表示将删除该路程下所有的流程任务及历史
// false 不会删除任务及历史
repositoryService.deleteDeployment(pid, false);
}
流程实例 ProcessInstance
ProcessDefinition 与 ProcessInstance 是一对多的关系
理解为:行动计划于具体行动的关系,流程实例是流程定义的具体实现
用户任务的属性面板
创建流程
Task 任务
@Autowired
private TaskService taskService;
/**
* 查询所有任务
*/
@Test
public void getTasks(){
List<Task> list = taskService.createTaskQuery().list();
for (Task task : list) {
System.out.println("任务名称:" + task.getName());
System.out.println("任务执行人:" + task.getAssignee());
System.out.println("流程实例id:" + task.getProcessInstanceId());
}
}
/**
* 查询我的待办任务
*/
@Test
public void getTaskByAssignee(){
List<Task> zhangsanTask = taskService.createTaskQuery()
.taskAssignee("张三")
.list();
for (Task task : zhangsanTask) {
System.out.println("任务id:" + task.getId());
System.out.println("任务名称:" + task.getName());
System.out.println("任务执行人:" + task.getAssignee());
System.out.println("流程实例id:" + task.getProcessInstanceId());
}
}
/**
* 执行任务
* 影响的表 act_run_task act_hi_taskinst
*/
@Test
public void completeTask(){
taskService.complete("f5a6ac5d-2a85-11eb-be00-408d5c97c1d5");
System.out.println("执行任务");
}
//拾取任务
@Test
public void claimTask(){
Task task = taskService.createTaskQuery().taskId("1f2a8edf-cefa-11ea-84aa-dcfb4875e032").singleResult();
taskService.claim("1f2a8edf-cefa-11ea-84aa-dcfb4875e032","bajie");
}
//归还与交办任务
@Test
public void setTaskAssignee(){
Task task = taskService.createTaskQuery().taskId("1f2a8edf-cefa-11ea-84aa-dcfb4875e032").singleResult();
taskService.setAssignee("1f2a8edf-cefa-11ea-84aa-dcfb4875e032","null");//归还候选任务
taskService.setAssignee("1f2a8edf-cefa-11ea-84aa-dcfb4875e032","wukong");//交办
}
查询历史记录
@Autowired
private HistoryService historyService;
/**
* 根据用户名查询历史记录
*/
@Test
public void historyTaskInstanceByUser(){
List<HistoricTaskInstance> history = historyService.createHistoricTaskInstanceQuery()
.orderByHistoricTaskInstanceEndTime()
.asc()
.taskAssignee("张三")
.list();
for (HistoricTaskInstance hi : history){
System.out.println("执行人:" + hi.getAssignee());
System.out.println("name:" + hi.getName());
System.out.println("流程实例id:" + hi.getProcessInstanceId());
}
}
/**
* 根据流程实例查询任务
*/
@Test
public void historyTaskInstanceByProcessInstanceId(){
List<HistoricTaskInstance> list = historyService.createHistoricTaskInstanceQuery()
.orderByHistoricTaskInstanceEndTime().asc()
.processInstanceId("f5a091d9-2a85-11eb-be00-408d5c97c1d5")
.list();
for (HistoricTaskInstance hi : list){
System.out.println("执行人:" + hi.getAssignee());
System.out.println("name:" + hi.getName());
System.out.println("流程实例id:" + hi.getProcessInstanceId());
}
}
UEL 统一表达式语言(Unified Expression Language)
- 表达式以 "{day > 5}
- 支持逻辑运算 ${userName == 'zhangsan' and pwd == '999'}
- 支持变量与实例类赋值
- 对应的数据表
- act_ru_variable 运行时参数表
-
act_hi_varinst 历史参数表(参数实例的缩写)
流程图
启动流程实例
@Autowired
private RepositoryService repositoryService;
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
/**
* 部署流程实例
*/
@Test
public void deplyProcessInstance(){
Deployment deploy = repositoryService
.createDeployment()
.disableSchemaValidation()
.addClasspathResource("BPMN/Part6_UEL.bpmn")
.deploy();
System.out.println("部署id:" + deploy.getId());
System.out.println("部署名称:" + deploy.getName());
System.out.println("部署版本:" + deploy.getVersion());
}
/**
* 启动流程实例带参数
*/
@Test
public void initProcessInstanceWithArgs(){
// 流程变量
Map<String,Object> variables = new HashMap<String,Object>();
variables.put("assignee","张一山");
ProcessInstance processInstance = runtimeService
.startProcessInstanceByKey("myProcess_UEL", variables);
System.out.println("流程实例id:" + processInstance.getProcessDefinitionId());
}
act_run_task 表
费用报销单,小于,等于 100 经理审批,大于 100 主管审批
部署,启动略过......
/**
* UEL 测试
* 费用报销单,小于,等于 100 经理审批,大于 100 主管审批
*/
@Test
public void completeTaskWithArgs(){
Map<String,Object> variables = new HashMap<String,Object>();
variables.put("money", 101);
taskService.complete("7ec08949-2c13-11eb-b4b1-408d5c97c1d5",variables);
System.out.println("大于 100 应该是主管审批");
}
UEL 执行人候选人测试
实体类
/**
* 注意要实现 serializable 接口
* 变量名要小写
*/
public class User implements Serializable {
// 执行人
private String assign;
// 候选人
private String candidate;
// get set 略过 ......
}
部署流程略过......
启动流程实例
/**
* 启动流程实例,对象参数
*/
@Test
public void initProcessInstanceWithClassArgs(){
User user = new User();
// 设置执行人
user.setAssign("zhangsan");
// 设置候选人
user.setCandidate("lisi,wangwu");
Map<String,Object> variables = new HashMap<String,Object>();
variables.put("user",user);
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("myProcess_11", variables);
System.out.println("流程实例 id" + processInstance.getProcessDefinitionId());
}
执行人是张三
张三执行完后,下一步是候选人执行但是,有两个候选人,需要先拾取任务,此时执行是空的
张三拾取组任务
/**
* 拾取任务
*/
@Test
public void claimTask(){
taskService.claim("95152f81-2c19-11eb-b715-408d5c97c1d5","zhangsan");
System.out.println("拾取任务成功");
}
- 设置流程变量
/**
* 直接指定全局流程变量
*/
@Test
public void setVariables(){
// 通过 id 设置流程变量
// 参数1:流程实例id
// 参数2:流程变量名
// 参数3:流程变量所对应的值
// 设置单个变量
runtimeService.setVariable("2501","name","zhangsan");
Map<String,Object> variables = new HashMap<String,Object>();
variables.put("name","zhangsan");
variables.put("age","15");
// 设置多个变量
runtimeService.setVariables("2501",variables);
// 通过任务节点 id 设置流程变量 同上
// taskService.setVariables();
// taskService.setVariable();
}
/**
* 设置局部变量,只在当前环节有用
*/
@Test
public void setLocalVariable(){
/*runtimeService.setVariableLocal();
runtimeService.setVariablesLocal();
taskService.setVariableLocal();
taskService.setVariablesLocal();*/
}
网关
排他网关
-
ExclusiveGateway:当时用排他网关,流程执行到网关时,按照顺序与计算条件进行处理,当计算条件为 true 时,继续执行下一个环节,没有满足的条件则抛出异常,如过多个条件都满足,排他网关会根据 bpmn 顺序靠前的条件继续执行,也就是说排他网关只会执行一个条件
排他网关需要指定执行条件,如果没有执行会执行其中的一个,具体执行某一个,先连接那根线会先执行哪一个会根据 BPMN xml 的顺序去执行
当 zhangsan 填写请假单之后,请假大于 3 天应该又 wangwu 审核,小于等于 3 天 由 lisi 审核
@Autowired
private RepositoryService repositoryService;
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
/**
* 部署流程
*/
@Test
public void deployProcess(){
Deployment deploy = repositoryService.createDeployment()
.disableSchemaValidation()
.addClasspathResource("BPMN/part8_exclusive.bpmn")
.deploy();
System.out.println("流程部署 id = " + deploy.getId());
}
/**
* 启动流程实例
*/
@Test
public void startProcessInstance(){
ProcessInstance pi = runtimeService.startProcessInstanceByKey("myProcess_exclusive");
System.out.println("流程实例 id = " + pi.getId());
System.out.println("流程实例名称:" + pi.getName());
}
-
可以看到张三有一个任务
- 请假 10 天 大于三天
/**
* zhangsan 执行请假任务 请假10 天 大于 3 天 应该是 wangwu 审核
*/
@Test
public void execTask(){
Map<String,Object> map = new HashMap<String,Object>();
map.put("day",10);
taskService.complete("d6eac7a1-2ed3-11eb-8d1b-9c5c8e79c1e8", map);
System.out.println("zhansan 执行请假任务");
}
-
wangwu 得到审批任务
-
排他网常用于下面这种流程,就是审批不同意的驳回,如果同意继续往下执行,可以加一些边界时间进行通知,不同意打回
并行网关
-
ParallelGeteway:把任务拆分成多路,并把多路任务合并成一路,比较常用,多用于重要环节,需要多人审批的场景
当 zhangsan 填写完请假单后,lisi 和 wangwu 都会收到一条任务
部署启动流程实例
@Autowired
RepositoryService repositoryService;
@Autowired
private TaskService taskService;
@Autowired
private RuntimeService runtimeService;
/**
* 部署流程
*/
@Test
public void deployProcess(){
Deployment deploy = repositoryService.createDeployment()
.disableSchemaValidation()
.addClasspathResource("BPMN/part7_parallelism.bpmn")
.deploy();
System.out.println("部署 id = " + deploy.getId());
}
/**
* 启动流程实例
*/
@Test
public void startProcessInstance(){
ProcessInstance pi = runtimeService.startProcessInstanceByKey("myProcess_holiday");
System.out.println("流程实例 id = " + pi.getId());
System.out.println("流程名称 = " + pi.getName());
}
-
部署启动之后张三会收到一个任务
- zhangsan 执行任务,zhangsan 执行完任务,lisi 和 wangwu 都应该都已一个审核任务
/**
* zhangsan 执行任务
*/
@Test
public void execTask(){
taskService.complete("adc03b8a-2ca3-11eb-b95d-005056c00008");
System.out.println("zhangsan 填写请假单");
}
-
果然 zhangsan 执行完成之后,lisi 和 wangwu 都收到了任务
包容网关
-
InclusiveGateway:包容网关可以理解为能够添加条件的并行网关,并行网关是不能添加条件的,连出去几根线,就需要进行几个审核流程,而包容网关,可以在每条线路上设置条件,并且可以执行多条线路。包容网关与并行网关比是可以设置条件的,与排他网关比,排他网关只会有一个结束环节被启动,而包容网关可以有多个环节被启动
- 包容网关是并行网关的延伸版,所以也是成对出现的,上面的流程 zhangsan 填写请假单,请假 1 天,此时 wangwu 和 zhaoliu 都应当受到一条请假任务
- 流程部署 zhangsan 会收到一个任务
@Autowired
private RepositoryService repositoryService;
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
/**
* 流程部署
*/
@Test
public void deployProcess(){
Deployment deploy = repositoryService.createDeployment()
.disableSchemaValidation()
.addClasspathResource("BPMN/part9_inclusive.bpmn")
.deploy();
System.out.println("流程部署 id = " + deploy.getId());
}
/**
* 启动流程
*/
@Test
public void startProcess(){
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("myProcess_inclusive");
System.out.println("流程实例 id = " + processInstance.getId());
}
- zhangsan 执行任务,按照预想 zhangsan 请假 1 天,此时 wangwu 和 zhaoliu 都应当受到一条请假任务
/**
* zhangsan 执行任务 请假一天
*/
@Test
public void execTask(){
// 流程变量
Map<String,Object> map = new HashMap<String,Object>();
// 请假一天
map.put("day", 1);
taskService.complete("3c1c7f0a-2ee9-11eb-94e6-9c5c8e79c1e8",map);
System.out.println("张三填写请假单");
}
-
wangwu 和 zhaoliu 收到任务
- 王五赵六审批完成,流程结束
/**
* 王五赵六审批任务
*/
@Test
public void auditTask(){
taskService.complete("8c78e4ba-2eea-11eb-bd03-9c5c8e79c1e8");
taskService.complete("8c790bcc-2eea-11eb-bd03-9c5c8e79c1e8");
System.out.println("wangwu zhaoliu 审批");
}
事件网关
- EventGateway:事件网关执行连接到事件,并且连接的事件必须大于等于两个
activiti 7 新特性
ProcessRuntime
- 为了与 ProcessRuntime API 交互,当前登录用户必须具有 “ACTIVITI_USER” 角色,就是如果你要使用 ProcessRuntime API 进行交互当前登录角色必须是 “ACTIVITI_USER”,“ACTIVITI_USER” 是什么角色呢?,要执行流转在任务执行的环节把执行人写死就行了,这个执行人在不我的数据库里并没有关系,这是以前的做法,现在的做法不是了,你的用户必须在 springSecurity 中有才能去进行操作
github 找到 activiti 项目下载到本地,拷贝一下两个文件到项目
- SecurityUtil 是一个登陆辅助类,在 activiti 7 中所有的方法都需要使用该类中的 logInAs 方法进行登陆,改类会去调用 springSecurity 自带的方法 loadUserByUserName 去查询当前的用户是否存在于 springSecurity 框架中,如果存在返回 UserDetails 类,通过该类可以获取到用户的信息等......,反之会抛出异常
@Component
public class SecurityUtil {
private Logger logger = LoggerFactory.getLogger(SecurityUtil.class);
@Autowired
private UserDetailsService userDetailsService;
public void logInAs(String username) {
UserDetails user = userDetailsService.loadUserByUsername(username);
if (user == null) {
throw new IllegalStateException("User " + username + " doesn't exist, please 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);
}
}
- DemoApplicaitonConfiguration 该类会运行中在内存中创建许多用户,实际项目中回去从数据库中读取用户信息,这里暂时这么使用
//@Configuration
public class DemoApplicationConfiguration {
private Logger logger = LoggerFactory.getLogger(DemoApplicationConfiguration.class);
// @Bean
public UserDetailsService myUserDetailsService() {
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
// 创建三个用户 zhangsan,lisi,wangwu ,拥有 ACTIVITI_USER 角色
String[][] usersGroupsAndRoles = {
{"zhangsan", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"},
{"lisi", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"},
{"wangwu", "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));
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;
}
// @Bean 密码编码
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
ProcessRuntime API 测试,要使用 ProcessRuntime 就需要使用 springSecurity 安全框架去做,如果项目没有使用 springSecurity 安全框架,不使用新特性也是可以的
@Autowired
private ProcessRuntime processRuntime;
@Autowired
private SecurityUtil securityUtil;
@Autowired
private RepositoryService repositoryService;
// 获取流程实例
@Test
public void getProcessInstance(){
// 登录 这个 securityUtil 是我们复制下来的,不是 Apache 下的
securityUtil.logInAs("zhangsan");
// 查询
Page<ProcessInstance> pdp = processRuntime
.processInstances(Pageable.of(0, 100));
System.out.println("流程实例总数量:"+pdp.getTotalItems());
// 查询流程实例
List<ProcessInstance> pis = pdp.getContent();
pis.forEach(p -> {
System.out.println("流程id = "+p.getId());
System.out.println("流程名称 = "+p.getName());
System.out.println("开始日期 = "+p.getStartDate());
System.out.println("状态 = "+p.getStartDate());
System.out.println("流程定义id = "+p.getProcessDefinitionId());
System.out.println("流程定义key = "+p.getProcessDefinitionKey());
});
}
/**
* 部署流程还是用以前的方式,新的 api 中没有封装
*/
@Test
public void deployProcessInstance(){
Deployment deploy = repositoryService.createDeployment()
.disableSchemaValidation()
.addClasspathResource("BPMN/part10_processRuntime.bpmn")
.deploy();
System.out.println("流程部署 id = " + deploy.getId());
}
// 启动流程实例
@Test
public void startProcessInstance(){
// 登录
securityUtil.logInAs("zhangsan");
processRuntime.start(ProcessPayloadBuilder
.start()
.withProcessDefinitionKey("myProcess_processRuntime") // 根据流程 key 启动
// .withVariable("aaa","bb" )
// .withVariables("map")
.withBusinessKey("可以自定义业务 key")
.build()
);
}
// 删除流程实例
@Test
public void delProcessInstance(){
// 登录
securityUtil.logInAs("zhangsan");
processRuntime.delete(ProcessPayloadBuilder
.delete()
.withProcessInstanceId("edb14933-2f06-11eb-b202-9c5c8e79c1e8")
.build()
);
}
// 挂起流程
@Test
public void suspendProcessInstance(){
// 登录
securityUtil.logInAs("zhangsan");
processRuntime.suspend(ProcessPayloadBuilder
.suspend()
.withProcessInstanceId("流程实例 id")
.build()
);
}
// 激活流程实例
@Test
public void resumeProcessInstance(){
// 登录
securityUtil.logInAs("zhangsan");
processRuntime.resume(ProcessPayloadBuilder
.resume()
.withProcessInstanceId("流程实例 id")
.build()
);
}
// 流程实例参数
@Test
public void getVariables(){
// 登录
securityUtil.logInAs("zhangsan");
List<VariableInstance> variables = processRuntime.variables(ProcessPayloadBuilder
.variables()
.withProcessInstanceId("d6e3277d-2ed3-11eb-8d1b-9c5c8e79c1e8")
.build()
);
variables.forEach(var -> {
System.out.println("变量名称="+var.getName());
System.out.println("变量值="+var.getValue());
System.out.println("任务id="+var.getTaskId());
System.out.println("流程id="+var.getProcessInstanceId());
});
}
TaskRuntime
- TaskRuntime 是 activiti 7 新出现的 api ,它是对 task 任务进行的封装,有task 的查询,创建,拾取,完成等等,taskRuntime 的封装还是做的挺好了,以前查询代办任务是一个方法,查询候选人任务也是一个方法,而 taskRuntime 封装成一个方法了,这个是比较符合实际的,并且 taskRuntime 还做了一些异常判断
@Autowired
private SecurityUtil securityUtil;
@Autowired
private TaskRuntime taskRuntime;
/**
* 获取当前登录用户任务
*/
@Test
public void getTasks(){
securityUtil.logInAs("zhangsan");
// 查询 0 - 100 条
Page<Task> tasks = taskRuntime.tasks(Pageable.of(0, 10));
List<Task> list = tasks.getContent();
// taskRuntime 其实是将 执行人和候选人都查出来了
list.forEach(t -> {
System.out.println("任务id="+t.getId());
System.out.println("任务名称="+t.getName());
System.out.println("任务状态="+t.getStatus());
System.out.println("创建时间="+t.getCreatedDate());
// taskRuntime 其实是将 执行人和候选人都查出来了
// 我们可以在这里做一个判断
// 如果 assignee 执行人能查出来,但是如果等于 null ,说明某一个任务的候选人有他,需要当前用户去拾取任务
if(t.getAssignee() == null){
// 说明该任务的候选人有他,需要拾取
System.out.println("assignee: 这是一条待拾取的任务");
}else{
System.out.println("执行人就是当前用户 assignee: "+t.getAssignee());
}
});
}
/**
* 完成任务
*/
@Test
public void completeTask(){
// 登录
securityUtil.logInAs("zhangsan");
// 执行任务
Task task = taskRuntime.task("任务 id");
// 如果执行人为 null 说明当前执行任务是候选人,需要先拾取任务
if(task.getAssignee() == null){
// 拾取任务
taskRuntime.claim(TaskPayloadBuilder
.claim()
.withTaskId(task.getId())
.build()
);
}
// 执行任务
taskRuntime.complete(TaskPayloadBuilder
.complete()
.withTaskId(task.getId())
.build()
);
System.out.println("任务执行完成");
}
SpringSecurity 的主要功能
认证
鉴权/授权
用户的三种来源
- application.properties 配置用户
这种是写死在配置文件中的 - 代码中配置内存用户
上面 SecurityUtis 类就是内存用户 - 从数据库中加载用户
activiti 7 中默认已经集成了 SpirngSecurity 框架,我们拉测试一下
先注释掉之前的我们测试用的内容用户,新建一个测试类 controller
@RestController
public class HelloController {
@RequestMapping(value = "/hello", method = RequestMethod.GET)
public String hello() {
return "Activiti7 欢迎你";
}
}
启动 springboot 工程,访问 url http://localhost:8080/hello 我们创建的控制器,此时会跳转到 SpringSecuriy 内置的登录页
-
默认的用户是 user,密码是在项目启动的时候回打印到控制台
-
我们现在使用内存用户登录,放开上面注释的内存用户类,此时不会再控制打印出用户密码了,可以使用内存用户登录了,登录也是没有任何问题的
- 我们最终想要的不是在内存中构建用户的,而是要在数据库中构建用户,所以我们需要自己去编码实现
编写测试代码
- 创建一个 MyUserDetailsSevice 类,实现 SpringSecurity 的 UserDetailsService
@Component
@Configuration
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 登陆密码 需要进行加密
String password = passwordEncoder().encode("666");
return new User(username, // 用户名,现在没有查数据库所以前端可以随便写
password, // 密码必须为 666
// activiti 7 需要用户必须拥有 ROLE_ACTIVITI_USER 角色
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ACTIVITI_USER"));
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
- 此时我们启动项目用户名随便输,密码为 666 就可以访问了
从数据库中读取用户
- 新建实体类 UserInfoBean 实现 UserDetails 接口,为什么要实现 UserDetails 接口呢?因为登录校验的时候返回的就是 UserDetails 类,SpringSecurity 会根据返回的用户去进行密码的比对
/**
* 用户类
*/
public class UserInfoBean implements UserDetails {
private Long id; // 编号
private String name; // 名字
private String address; // 地址
private String username; // 用户名
private String password; // 密码
private String roles; // 角色
public String getAddress() {
return address;
}
// 这里返回用户的角色列表,为角色赋值
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays.
stream(roles.split(",")). // 使用 , 号分割角色字符串,引用数据库里是这么存的
map(r -> new SimpleGrantedAuthority(r))
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
// 下面暂时都先返回 true
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
- 编写一个 mapper 类,这里使用的是 mabatis
@Mapper
public interface UserInfoBeanMapper {
/**
* 根据用户名查询用户信息
* @param username
* @return
*/
@Select("select * from user where username = #{username}")
public UserInfoBean selectByUsername(@Param("username")String username);
}
- 改造上面写的 MyUserDetailsService 类
@Component
@Configuration
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserInfoBeanMapper userInfoBeanMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 登陆密码 需要进行加密
/*String password = passwordEncoder().encode("666");
return new User(username, // 用户名,现在没有查数据库所以前端可以随便写
password, // 密码必须为 666
// activiti 7 需要用户必须拥有 ROLE_ACTIVITI_USER 角色
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ACTIVITI_USER"));*/
/**
* 需要做的操作
* 读取数据库判断用户
* 如果用户是 null 抛出异常
* 返回用户信息
*/
UserInfoBean userInfoBean = userInfoBeanMapper.selectByUsername(username);
if(userInfoBean == null){
throw new UsernameNotFoundException("用户不存在!");
}
// 返回用户信息,密码比对不只是在这里进行比对的
// 我们将查询到的用户信息返回给框架,然后安全框架内部根据用户输入的密码和查询的密码进行比对
return userInfoBean;
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
-
下面是我创建的用户表,此时启动项目,就可以使用这里的用户名面来进行登陆了
SpringSecurity 配置文件处理
- 新建配置类 ActivitiSecurityConfig 继承 WebSecurityConfigurerAdapter 类
/**
* SpringSecurity 配置类
*/
@Configuration
public class ActivitiSercurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Autowired
private LoginFailureHandler loginFailureHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin() // 这里我们就要使用 http 进行登录了,不能使用登录页登录了
.loginPage("/login") // 登录方法
.loginProcessingUrl("/login") // 配置没有访问权限的 url 的处理链接,需要 loginPage 一样
.successHandler(loginSuccessHandler) // 登录成功处理类
.failureHandler(loginFailureHandler) // 失败处理
.and()
.authorizeRequests()
.antMatchers("/login").permitAll() // 配置没有访问权限的 url 不需要登录访问
.anyRequest().authenticated() // 所有的 url 都需要登录才能访问
.and()
.logout().permitAll()
.and()
.csrf().disable()
.headers().frameOptions().disable(); // frame 框架校验关闭,否则前端使用的 frame 框架也会报错
}
}
- 登陆成功处理类
/**
* 登录成功处理类
*/
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
// 这个是处理 ajax
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
}
// 登录成功输出一句话
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write("登录成功,用户名:"+authentication.getName());
}
}
- 登陆失败处理类
/**
* 登录失败的处理
*/
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
// 设置失败状态码
httpServletResponse.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write("登录失败:原因是"+e.getMessage());
}
}
- 没有开放的 url,并且是没有登陆的时候访问的处理 url
/**
* 未登录用户处理
*/
@RestController
public class ActivitiSecurityController {
@RequestMapping("/login")
@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
public String requireAuthentication(HttpServletRequest request, HttpServletResponse response){
return new String("需要登录才能访问!!!");
}
}
测试
-
登陆成功
-
登陆失败
- 没有登陆,访问没权开放的 url http://localhost:8080/hello 会自动跳转到 http://localhost:8080/login
BPMN-JS
官网 bpmn.io
-
进入官网下载实例
-
使用 git 下载或者直接下载 zip 都可以
Activiti7整合BPMN-JS说明
安装Node.js
BPMN-JS地址
在resources文件夹下再创建一个resources文件夹
- 实际路径为resources/resources/
把从github下载的bpmn-js-examples-master.zip压缩包解压
拷贝properties-panel到resources/resources/并改文件夹名字为bpmnjs
- 实际路径为resources/resources/bpmnjs
使用命 terminal 令行工具打开resources/resources/bpmnjs/并执行命令
安装所需依赖
npm install
启动
npm run dev
启动会在控制台输出访问连接
拷贝 bpmnjs 初始化文件夹下的内容到 resources/resources/bpmnjs 目录下
打开resources/resources/bpmnjs/app/index.js,注释以下内容(bpmn.js 默认是一个叫 camunda 的工作流,不是 activiti)
import propertiesProviderModule from 'bpmn-js-properties-panel/lib/provider/camunda';
import camundaModdleDescriptor from 'camunda-bpmn-moddle/resources/camunda.json';
注释掉初始化部分
var bpmnModeler = new BpmnModeler({
container: canvas,
propertiesPanel: {
parent: '#js-properties-panel'
},
additionalModules: [
propertiesPanelModule,
propertiesProviderModule
],
moddleExtensions: {
camunda: camundaModdleDescriptor
}
});
添加以下内容,主要是汉化的部分和 activiti 部分
import propertiesProviderModule from '../resources/properties-panel/provider/activiti';
import activitiModdleDescriptor from '../resources/activiti.json';
import customTranslate from '../resources/customTranslate/customTranslate';
import customControlsModule from '../resources/customControls';
添加翻译组件
// 添加翻译组件
var customTranslateModule = {
translate: ['value', customTranslate]
};
var bpmnModeler = new BpmnModeler({
container: canvas,
propertiesPanel: {
parent: '#js-properties-panel'
},
additionalModules: [
propertiesPanelModule,
propertiesProviderModule,
customControlsModule,
customTranslateModule
],
moddleExtensions: {
activiti:activitiModdleDescriptor
}
});
-
这时重新刷新之前打开的 bpmn 页面,就会看到已经是汉化的了,并且生成的 bpmn 就是 activiti 需要的文件了,可以正常部署了
-
这里有一个小问题,就是我们在部署的时候,在页面上有一个 “可执行文件” 必须勾上才可以部署,但是默认其实不是勾选的,还是比较麻烦的,我们现在让他默认勾选上
-
其实我们默认打开的 bpmn 页面就是下面这个文件
-
用编辑器打开,将默认的 bpmn 文件将 isExeclutable 改为 true 就可以了
-
npm run dev 启动就可以看见已经默认勾上了
- 我们可以直接访问项目目录下的 bpmn.js 画图页面进行画图下载 bmpn xml 文件进行测试上面的接口 http://localhost:8080/bpmnjs/dist/index.html ,但是可能会有乱码
乱码解决,在下面俩个文件中加入 <meat charset="UTF-8"/> 即可
编码实现
创建全局枚举配置类
/**
* 全局枚举配置类
*/
public class GlobalConfig {
// 是否是测试环节
public static final boolean Test = true;
public enum ResponseCode{
SUCCESS(0,"成功"),
ERROR(1,"失败");
private final int code;
private final String desc;
ResponseCode(int code,String desc){
this.code = code;
this.desc = desc;
}
public int getCode() {
return code;
}
public String getDesc() {
return desc;
}
}
}
创建 ajax 返回类,用户给前端返回同一类型
/**
* ajax 返回类
*/
public class AjaxResponse {
private int status; // 返回状态码
private String msg; // 成功或错误提示
private Object obj; // 返回内容
private AjaxResponse(int status, String msg, Object obj) {
this.status = status;
this.msg = msg;
this.obj = obj;
}
public static AjaxResponse AjaxData(int status,String msg,Object obj){
return new AjaxResponse(status,msg,obj);
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
}
改造之前的登录成功和失败处理类
- LoginFailureHandler
/**
* 登录成功处理类
*/
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
// 这个是处理 ajax
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
}
// 返回成功处理
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(
objectMapper.writeValueAsString(
AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
// 返回用户名
authentication.getName()))
);
}
}
- LoginFailureHandler
/**
* 登录失败的处理
*/
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;
// 登录失败处理
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(
objectMapper.writeValueAsString(
AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.ERROR.getCode(),
GlobalConfig.ResponseCode.ERROR.getDesc(),
"登录失败:原因是"+e.getMessage()
)
));
}
}
-
测试,这是返回的就是我们定义的 AjaxResponse 类了
登录成功
登录失败
流程定义接口
/**
* 流程定义 控制类
*/
@RestController
@RequestMapping("/processDefinition")
public class ProcessDefinitionController {
@Autowired
private RepositoryService repositoryService;
/**
* 添加流程定义,通过上传 bpmn
* @param multipartFile 流程定文件
* @param deployName 部署名称
* @return
*/
@PostMapping("/uploadStreamAndDeployment")
public AjaxResponse uploadStreamAndDeployment(
@RequestParam("bpmnFile")MultipartFile multipartFile,
@RequestParam("deployName")String deployName
){
try {
// 获取上传的文件名
String fileName = multipartFile.getOriginalFilename();
// 获取文件名扩展名
String extension = FilenameUtils.getExtension(fileName);
// 获取文件字节流对象
InputStream fileInputStream = multipartFile.getInputStream();
Deployment deployment = null;
// 如果后缀名 zip 压缩包
if(extension.equals("zip")){
// 流程部署
ZipInputStream zip = new ZipInputStream(fileInputStream);
deployment = repositoryService.createDeployment()
.addZipInputStream(zip)
.deploy();
}else {
// bpmn 文件部署
deployment = repositoryService.createDeployment()
.addInputStream(fileName,fileInputStream)
.name(deployName)
.deploy();
}
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
deployment.getId()+"|"+fileName
);
}catch (Exception e){
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.ERROR.getCode(),
"添加流程定义失败",
e.getMessage()
);
}
}
/**
* 添加流程定义通过在线提交 BPMN 的 xml
* @param xmlBPMN
* @param deployName
* @return
*/
@PostMapping("/addDeploymentByString")
public AjaxResponse addDeploymentByString(@RequestParam("xmlBPMN")String xmlBPMN,
@RequestParam("deployName")String deployName){
try{
Deployment deployment = repositoryService.createDeployment()
// 这里由于是 xml 的字符串没有资源名称,这里我们先写死一个
.addString("createWithBPMNJS.bpmn", xmlBPMN)
.name(deployName)
.deploy();
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
deployment.getId()
);
}catch (Exception e){
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.ERROR.getCode(),
"添加流程定义失败",
e.getMessage()
);
}
}
// 获取流程定义列表
@GetMapping("/getDefinitions")
public AjaxResponse getDefinitions(){
try{
// 保存流程定义列表
List<Map<String,Object>> listMap = new ArrayList<Map<String,Object>>();
// 查询流程列表
List<ProcessDefinition> list = repositoryService.createProcessDefinitionQuery()
.list();
list.forEach(pd -> {
Map<String,Object> map = new HashMap<String,Object>();
// 流程定义名称
map.put("name",pd.getName());
// 流程定义 key
map.put("key",pd.getKey());
// 资源名称
map.put("resourceName",pd.getResourceName());
// 部署 id
map.put("deploymentId",pd.getDeploymentId());
// 版本
map.put("version",pd.getVersion());
listMap.add(map);
});
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
listMap
);
}catch (Exception e){
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.ERROR.getCode(),
"获取流程定义列表失败",
e.toString()
);
}
}
// 获取流程定义的 xml
// 这个方法的作用是,在流程定的列表中当你想查看上传的 BPMN 是什么样子的时候,在 Activiti 7 以前,
// 你只能在上传 BPMN 的同时在上传一张图片,这里去查看图片,那么在 activiti 7 中就不需要这么麻烦了,
// 我们只需要将数据库中的二进制文件读出来并返回给前端 xml 即可
/**
*
* @param response
* @param deploymentId 流程部署 id
* @param resourceName 资源名称
*/
@GetMapping("/getDefinitionXML")
public void getDefinitionXML(HttpServletResponse response,
@RequestParam("deploymentId")String deploymentId,
@RequestParam("resourceName")String resourceName){
// 声明返回的是一个 xml 类型
response.setContentType("text/xml");
try(
// 读取流程部署文件流
InputStream inputStream = repositoryService.getResourceAsStream(deploymentId, resourceName);
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
BufferedOutputStream outputStream = new BufferedOutputStream(response.getOutputStream());
) {
byte[] bytes = new byte[1024];
int len = 0;
while ((len = bufferedInputStream.read(bytes)) != -1){
outputStream.write(bytes, 0, len);
}
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 获取流程部署列表
* @return
*/
@GetMapping("/getDeployments")
public AjaxResponse getDeployments(){
try {
List<Map<String,Object>> listMap = new ArrayList<Map<String,Object>>();
// 流程部署列表
List<Deployment> list = repositoryService.createDeploymentQuery()
.list();
list.forEach(p -> {
Map<String,Object> map = new HashMap<String,Object>();
// 部署id
map.put("id", p.getId());
// 部署名称
map.put("name",p.getName());
// 部署时间
map.put("deploymentTime", p.getDeploymentTime());
listMap.add(map);
});
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
listMap
);
}catch (Exception e){
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.ERROR.getCode(),
"获取流程部署列表失败",
e.toString()
);
}
}
/**
* 删除流程定义
* @param depId
* @return
*/
@DeleteMapping("/delDefinition")
public AjaxResponse delDefinition(@RequestParam("depId")String depId){
try {
repositoryService.deleteDeployment(depId, true);
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
null
);
}catch (Exception e){
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.ERROR.getCode(),
"删除流程定义失败",
e.toString()
);
}
}
}
流程实例接口
/**
* 流程实例 控制类
*/
@RestController
@RequestMapping("/processInstance")
public class ProcessInstanceController {
// 自己定义的 SecurityUtil 使用 activity 新特性需要登录
@Autowired
private SecurityUtil securityUtil;
@Autowired
private ProcessRuntime processRuntime;
@Autowired
private RepositoryService repositoryService;
/**
* 获取流程实例列表
* @AuthenticationPrincipal 注解可以获取用于登录信息
* @param userInfoBean
* @return
*/
@GetMapping("/getInstances")
public AjaxResponse getInstance(@AuthenticationPrincipal UserInfoBean userInfoBean){
try{
// 这是 GlobalConfig 类定义的是否是测试标记,标记是测试环境使用 内存用户登录,方便测试使用
if(GlobalConfig.Test){
// 测试环境使用内存用户登录
securityUtil.logInAs("zhangsan");
}
// 获取流程实例
Page<ProcessInstance> processInstancePage = processRuntime.processInstances(Pageable.of(0, 100));
// 将流程实例转换为 list
List<ProcessInstance> lists = processInstancePage.getContent();
// 对 list 进行排序 按照启动日期排序
lists.sort((x,y) -> y.getStartDate().toString().compareTo(x.getStartDate().toString()));
// 创建 map 保存流程实例列表
List<Map<String,Object>> listMap = new ArrayList<Map<String, Object>>();
for(ProcessInstance pi : lists){
// 因为 ProcessInstance 中没有历史高亮需要的 deploymentID 和 资源 resourceName ,所以需要查询
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.processDefinitionId(pi.getProcessDefinitionId())
.singleResult();
listMap.add(new CreateMap.Build()
.setAttribute("id",pi.getId())
.setAttribute("name",pi.getName())
.setAttribute("status",pi.getStatus())
.setAttribute("processDefinitionId",pi.getProcessDefinitionId())
.setAttribute("processDefinitionKey",pi.getProcessDefinitionKey())
.setAttribute("startDate",pi.getStartDate())
.setAttribute("processDefinitionVersion", pi.getProcessDefinitionVersion())
.setAttribute("deploymentId", processDefinition.getDeploymentId())
.setAttribute("resourceName",processDefinition.getResourceName())
.build());
}
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
listMap
);
}catch (Exception e){
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.ERROR.getCode(),
"获取流程实例列表失败",
e.toString()
);
}
}
/**
* 启动流程实例
* @param processDefinitionKey 流程定义 key
* @param instanceName 流程实例名称
* @return
*/
@GetMapping("/startProcess")
public AjaxResponse startProcess(@RequestParam("processDefinitionKey")String processDefinitionKey,
@RequestParam("instanceName")String instanceName){
try{
// 测试环境使用内容用户登录
if(GlobalConfig.Test){
securityUtil.logInAs("zhangsan");
}else{
// 这样也可以获取到登录人
// securityUtil.logInAs(SecurityContextHolder.getContext().getAuthentication().getName());
}
ProcessInstance processInstance = processRuntime.start(
ProcessPayloadBuilder
.start()
.withProcessDefinitionKey(processDefinitionKey)
.withName(instanceName)
.withBusinessKey("自定义业务key")
// .withVariable("参数name", "参数值") 启动一般不加参数,业务生成是才加参数
.build()
);
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
null
);
}catch (Exception e){
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.ERROR.getCode(),
"启动流程实例失败",
e.toString()
);
}
}
// 挂起流程实例
// 一般使用场景是:流程实例运行过程中,用户觉得流程需要暂停,但是又不想删除它,需要将历史保留起来,
// 这个时候需要使用挂起了
/**
* 挂起流程实例
* @param instanceId 实例 id
* @return
*/
@GetMapping("/suspendInstance")
public AjaxResponse suspendInstance(@RequestParam("instanceId")String instanceId){
try{
// 测试环境使用内容用户登录
if(GlobalConfig.Test){
securityUtil.logInAs("zhangsan");
}
ProcessInstance processInstance = processRuntime.suspend(
ProcessPayloadBuilder
.suspend()
.withProcessInstanceId(instanceId)
.build()
);
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
processInstance.getName() // 返回实例名称
);
}catch (Exception e){
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.ERROR.getCode(),
"挂起暂停流程实例失败",
e.toString()
);
}
}
/**
* 恢复流程实例
* @param instanceId 实例 id
* @return
*/
@GetMapping("/resumeInstance")
public AjaxResponse resumeInstance(@RequestParam("instanceId")String instanceId){
try{
// 测试环境使用内容用户登录
if(GlobalConfig.Test){
securityUtil.logInAs("zhangsan");
}
ProcessInstance processInstance = processRuntime.resume(
ProcessPayloadBuilder
.resume()
.withProcessInstanceId(instanceId)
.build()
);
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
processInstance.getName() // 返回实例名称
);
}catch (Exception e){
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.ERROR.getCode(),
"恢复流程实例失败",
e.toString()
);
}
}
/**
* 删除流程实例
* @param instanceId 实例 id
* @return
*/
@GetMapping("/deleteInstance")
public AjaxResponse deleteInstance(@RequestParam("instanceId")String instanceId){
try{
// 测试环境使用内容用户登录
if(GlobalConfig.Test){
securityUtil.logInAs("zhangsan");
}
ProcessInstance processInstance = processRuntime.delete(
ProcessPayloadBuilder
.delete()
.withProcessInstanceId(instanceId)
.build()
);
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
processInstance.getName() // 返回实例名称
);
}catch (Exception e){
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.ERROR.getCode(),
"删除流程实例失败",
e.toString()
);
}
}
/**
* 查询流程实例参数
* @param instanceId 实例 id
* @return
*/
@GetMapping("/variables")
public AjaxResponse variables(@RequestParam("instanceId")String instanceId){
try{
// 测试环境使用内容用户登录
if(GlobalConfig.Test){
securityUtil.logInAs("zhangsan");
}
List<VariableInstance> variables = processRuntime.variables(
ProcessPayloadBuilder
.variables()
.withProcessInstanceId(instanceId)
.build()
);
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
variables // 返回流程参数列表
);
}catch (Exception e){
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.ERROR.getCode(),
"查询流程实例参数失败",
e.toString()
);
}
}
}
任务接口
/**
* 任务 控制类
*/
@RestController
@RequestMapping("/task")
public class TaskController {
@Autowired
private SecurityUtil securityUtil;
@Autowired
private TaskRuntime taskRuntime;
@Autowired
private ProcessRuntime processRuntime;
/**
* 获取我的待办任务
* @return
*/
@GetMapping("/getTasks")
public AjaxResponse getTasks(){
try{
// 这是 GlobalConfig 类定义的是否是测试标记,标记是测试环境使用 内存用户登录,方便测试使用
if(GlobalConfig.Test){
// 测试环境使用内存用户登录
securityUtil.logInAs("zhangsan");
}
// 获取任务列表
Page<Task> tasks = taskRuntime.tasks(Pageable.of(0, 100));
List<Map<String,Object>> listMap = new ArrayList<Map<String,Object>>();
for (Task task : tasks.getContent()){
Map<String,Object> map = new HashMap<String, Object>();
map.put("id", task.getId());
map.put("name",task.getName());
map.put("status",task.getStatus());
map.put("createDate",task.getCreatedDate());
/**
* 执行人这里有一个重要的细节
* 这个方法查询的是当前登录人所有的任务,task.getAssignee() 查询的就是实际登录人的名称
* 实际上这个方法不光是执行是当前登录人的所有任务,他还包括候选人是当前登录人的所有任务,
* 只不过如果候选人是当前登录用户 task.getAssignee() 返回的是 null,这里如果 task.getAssignee()
* 返回的确实是当前登录人,那么 task.getAssignee() 就是当前登录人,否则我们和给返回一个带拾取任务
* 这样前端用户就很清楚的知道是分配给他的任务,还是说是一条带拾取的任务
*/
// 我们判断一下
if(task.getAssignee() == null){
map.put("assignee","带拾取任务"); // 这里先写死了
}else{
map.put("assignee",task.getAssignee()); // 当前登录人
}
// 由于这里获取不到流程实例名称,我们需要查询一下
ProcessInstance processInstance = processRuntime.processInstance(task.getProcessInstanceId());
// 保存流程实例名称
map.put("instanceName",processInstance.getName());
listMap.add(map);
}
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
listMap
);
}catch (Exception e){
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.ERROR.getCode(),
"获取我的任务列表失败",
e.toString()
);
}
}
/**
* 完成任务
* @return
*/
@GetMapping("/completeTask")
public AjaxResponse completeTask(@RequestParam("taskId")String taskId){
try{
// 这是 GlobalConfig 类定义的是否是测试标记,标记是测试环境使用 内存用户登录,方便测试使用
if(GlobalConfig.Test){
// 测试环境使用内存用户登录
securityUtil.logInAs("zhangsan");
}
// 查询任务
Task task = taskRuntime.task(taskId);
// task.getAssignee() == null 说明这是一条带拾取的任务,我们先拾取在完成
if(task.getAssignee() == null){
taskRuntime.claim(TaskPayloadBuilder
.claim()
.withTaskId(taskId)
.build()
);
}
// 完成任务 注意:挂起的任务需要先激活在完成
taskRuntime.complete(
TaskPayloadBuilder
.complete()
.withTaskId(taskId)
//.withVariable("xxx","xx") 完成任务也是可以设置实例参数的
.build()
);
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
null
);
}catch (Exception e){
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.ERROR.getCode(),
"完成任务 "+taskId+" 失败",
e.toString()
);
}
}
}
历史任务接口
/**
* 查询历史任务
*/
@RestController
@RequestMapping("/activitiHistory")
public class ActivitiHistoryController {
@Autowired
private SecurityUtil securityUtil;
@Autowired
private HistoryService historyService;
/**
* 查询用户历史任务
*
* @return
* @AuthenticationPrincipal 注解可以获取用于登录信息
*/
@GetMapping("getInstanceByUserName")
public AjaxResponse getInstanceByUserName(@AuthenticationPrincipal UserInfoBean userInfoBean) {
try {
// 获取用于历史任务列表
List<HistoricTaskInstance> historicTaskInstances = historyService.createHistoricTaskInstanceQuery()
.orderByHistoricTaskInstanceEndTime().desc()
.taskAssignee(userInfoBean.getUsername())
.list();
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
historicTaskInstances
);
} catch (Exception e) {
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.ERROR.getCode(),
"获取用户历史任务列表失败",
e.toString()
);
}
}
/**
* 根据流程实例id查询任务
*
* @param processInstanceId // 流程实例id
* @return
*/
@GetMapping("getInstanceByProcessInstanceId")
public AjaxResponse getInstanceByProcessInstanceId(@RequestParam("processInstanceId") String processInstanceId) {
try {
// 获取用于历史任务列表
List<HistoricTaskInstance> historicTaskInstances = historyService.createHistoricTaskInstanceQuery()
.orderByHistoricTaskInstanceEndTime().desc()
.processInstanceId(processInstanceId)
.list();
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
historicTaskInstances
);
} catch (Exception e) {
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.ERROR.getCode(),
"获取历史任务列表失败",
e.toString()
);
}
}
}
Activiti 中的表单
动态表单
动态表单是程序员最喜欢实现的表单方式,因为他的页面会根据数据的内容来描述出来
普通表单
普通表单就是由前端一个一个的去做静态的表单页面
- 在 activiti7 以前 6 或者 5 它是有 form 这个属性的,我们可以通过 form 属性来存和读响应的表单字段,但是在 activiti7 按照官网的说法为了轻量化去掉了者个内容,那么表单字段要如何渲染,表单字段不光是说我定义了一个 string 的字段,那么我就在前端显示一个输入框这么简单,他还有两个非常重要的使命,第一,这个字段是有默认值的,这个默认值可能是一个固定值,也有可能是之前的某一个任务环节天的值,在当前任务环节要显示,这个功能要这么做,这也是我们动态表单的一个重要功能,还有一个是什么呢,我们经常在页面填写的时候,填写的内容要做为 UEL 表达式的参数,比如说我要请三天假和请十天假是不同的人审批,我在页面上填写的内容是要作为参数去做流程判断的,或者说下一个环节是有谁审批,是由这一个环节的操作人去选取的,那么选取的值也要做为 UEL 表达式的参数,那么这一部分工作要怎么做,也是动态表单中的一个重要功能
第一个我们要做的功能是什么呢,在 BPMN js 中绘制表单字段,然后使用接口去获取表单字段,我们先来看能拿到什么内容
渲染表单接口(也是在 taskController 中)
* 渲染动态表单
* @return
*/
@GetMapping("/formDataShow")
public AjaxResponse formDataShow(@RequestParam("taskId")String taskId){
try{
// 这是 GlobalConfig 类定义的是否是测试标记,标记是测试环境使用 内存用户登录,方便测试使用
if(GlobalConfig.Test){
// 测试环境使用内存用户登录
securityUtil.logInAs("zhangsan");
}
// 查询任务
Task task = taskRuntime.task(taskId);
/**
* 关键代码,这里在强调一下,在 activiti6 和 5 的时候实际上是有 form 这个类的
* 但是在 activiti7 中去掉的为了轻量化,但是我们在 7 中还是有方法可以通过流程
* 定义的 id 和任务的 id 拿到一个叫 userTask 的类,在 userTask 类中可以拿到表单属性
*/
UserTask userTask = (UserTask) repositoryService.getBpmnModel(task.getProcessDefinitionId())
/**
* 获取流程元素,这里是要传什么呢?这里实际上是要传任务的key的,在 task 里并没有任务的 key 的
* 在 activiti 6 中是有任务的 key 的,不过我们可以用另外一个方案,我们可以用表单的 key ,这里
* 我们可以表表单的 key 和任务的 key 启成一模一样的名字,这样你拿表单的 key 就相当于拿任务的 key
* 了
*/
.getFlowElement(task.getFormKey());
List<FormProperty> formProperties = userTask.getFormProperties();
for (FormProperty formProperty : formProperties) {
}
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
null
);
}catch (Exception e){
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.ERROR.getCode(),
"完成任务 "+taskId+" 失败",
e.toString()
);
}
}
创建表单流程
下载到本地然后通过我们之前写的接口添加流程定义,通过 bpmn
通过接口启动流程
调用渲染表单接口打断点观察
通过断点可以看出在 activiti7 M4 版本中只能拿到表单的编号,和类型
activiti 中的表单
- 动态表单:使用拼字符串的方式描述表单信息
- 普通表单:需要设置 businessKey 以及任务与表单一对一关系
Activiti 表单字段的约定内容
- 表单控件命名约束:FormProperty_0ueitp2-_-类型-_-名称-_-默认值-_-是否是参数
说明:
- FormProperty_0ueitp2:bpmn js 自动生成的一个id
- -_- 是我们自定义的一个表单字段分割符
- 类型:例如 string,int ,date 等...前端可以根据类型渲染不同的控件
- 名称:控件左边的 label 例如:姓名:,年龄:
- 默认值:
- 无:那么前端什么都不要写,就是空的
- 字符常量:例如:请输入姓名,请输入年龄,到时候输入框里就会显示这些常量
- FormProperty_开头定义过的控件ID:以 FormProperty_ 开头表示需要显示之前这个控件的值,显示到控件中
- 是否是参数:f 为不是参数,s 是字符,t 是时间(不需要 int ,因为 int 等价于 string)
例子
FormProperty_0lwer-_-姓名-_-请输入姓名-_-f
FormProperty_fdfer-_-年龄-_-请输入年龄-_-s
- 注意:表单 key 必须和任务编号一模一样,因为参数需要任务 key ,但是无法获取,只能获取表单 key “task.getFormKey()” 当做任务 key
实例
创建动态表单 bpmn
表单的内容是我们自定义的格式
- 表单一内容:FormProperty_3s5ucaq-_-string-_-姓名-_-请输入姓名-_-f
- 表单二内容:FormProperty_279b9u1-_-int-_-年龄-_-无-_-s
下载 bpmn 文件,通过我们编写的接口进行上传,部署流程,启动流程,这里略过......
渲染动态表单
/**
* 渲染动态表单
* @return
*/
@GetMapping("/formDataShow")
public AjaxResponse formDataShow(@RequestParam("taskId")String taskId){
try{
// 这是 GlobalConfig 类定义的是否是测试标记,标记是测试环境使用 内存用户登录,方便测试使用
if(GlobalConfig.Test){
// 测试环境使用内存用户登录
securityUtil.logInAs("zhangsan");
}
// 查询任务
Task task = taskRuntime.task(taskId);
/**
* 关键代码,这里在强调一下,在 activiti6 和 5 的时候实际上是有 form 这个类的
* 但是在 activiti7 中去掉的为了轻量化,但是我们在 7 中还是有方法可以通过流程
* 定义的 id 和任务的 id 拿到一个叫 userTask 的类,在 userTask 类中可以拿到表单属性
*/
UserTask userTask = (UserTask) repositoryService.getBpmnModel(task.getProcessDefinitionId())
/**
* 获取流程元素,这里是要传什么呢?这里实际上是要传任务的key的,在 task 里并没有任务的 key 的
* 在 activiti 6 中是有任务的 key 的,不过我们可以用另外一个方案,我们可以用表单的 key ,这里
* 我们可以表表单的 key 和任务的 key 启成一模一样的名字,这样你拿表单的 key 就相当于拿任务的 key
* 了
*/
.getFlowElement(task.getFormKey());
// 说明该环节是不需要表单的
if(userTask == null){
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
"无表单"
);
}
List<FormProperty> formProperties = userTask.getFormProperties();
// 保存分割后的格式返回给前端
List<Map<String,Object>> listMap = new ArrayList<Map<String, Object>>();
for (FormProperty formProperty : formProperties) {
// 分割表单数据
String[] split = formProperty.getId().split("-_-");
Map<String,Object> form = new HashMap<String,Object>();
form.put("id", split[0]);
form.put("controlType", split[1]);
form.put("controlLabel", split[2]);
form.put("controlDefValue", split[3]);
form.put("controlParam", split[4]);
listMap.add(form);
}
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
listMap
);
}catch (Exception e){
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.ERROR.getCode(),
"完成任务 "+taskId+" 失败",
e.toString()
);
}
}
通过查询渲染表单接口最终返回给前端的数据
activiti 动态表单的提交
解析数据提交
activiti 动态表单提交参数的约定内容
- 动态表单提交内容约定
- formData:控件id-_-控件值-_-是否是参数!_!控件id-_-控件值-_-是否是参数
- 列子:
- FormProperty_0gkhjf-_-不是参数-_-f!_!FormProperty_1gkmgj-_-我是参数-_-s
- !_! 时用来分割多个控件
- 是否是参数:f 为不是参数,s 是字符,t 是时间(不需要 int ,因为 int 等价于 string)
保存动态表单测试,使用上面例子的数据
/**
* 保存动态表单
* @param taskId 任务 id
* @param formData 控件组字符串
* @return
*/
@PostMapping("/formDataSave")
public AjaxResponse formDataSave(@RequestParam("taskId")String taskId,
@RequestParam("formData")String formData){
try{
// 这是 GlobalConfig 类定义的是否是测试标记,标记是测试环境使用 内存用户登录,方便测试使用
if(GlobalConfig.Test){
// 测试环境使用内存用户登录
securityUtil.logInAs("zhangsan");
}
// 查询任务
Task task = taskRuntime.task(taskId);
// 前端传来的字符串拆分为控件组
String[] formDataList = formData.split("!_!");
// 保存解析后的数据
List<Map<String,Object>> controlItems = new ArrayList<Map<String, Object>>();
for (String control : formDataList) {
// 分割表单数据
String[] split = control.split("-_-");
Map<String,Object> form = new HashMap<String,Object>();
// 流程定义 key
form.put("PROC_DEF_ID_", task.getProcessDefinitionId());
// 流程实例 key
form.put("PROC_INST_ID_", task.getProcessInstanceId());
// 表单 key
form.put("FORM_KEY_", task.getFormKey());
// 控件 key
form.put("Control_ID_", split[0]);
// 控件值
form.put("Control_VALUE_", split[1]);
// 是否是流程参数
form.put("Control_PARAM", split[2]);
controlItems.add(form);
}
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
controlItems
);
}catch (Exception e){
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.ERROR.getCode(),
"渲染动态表单失败",
e.toString()
);
}
}
最终解析
提交表单内容写入输入库
数据库表结构
编写插入数据库的 mapper
@Mapper
@Component
public interface ActivitiMapper {
@Insert("<script>" +
"insert into formdata(PROC_DEF_ID_,PROC_INST_ID_,FORM_KEY_,Control_ID_,Control_VALUE_)" +
"values" +
"<foreach collection=\"maps\" item=\"formdata\" index=\"index\" separator=\",\">" +
"(" +
"#{formdata.PROC_DEF_ID_,jdbcType=VARCHAR}," +
"#{formdata.PROC_INST_ID_,jdbcType=VARCHAR}," +
"#{formdata.FORM_KEY_,jdbcType=VARCHAR}," +
"#{formdata.Control_ID_,jdbcType=VARCHAR}," +
"#{formdata.Control_VALUE_,jdbcType=VARCHAR}" +
")" +
"</foreach>" +
"</script>")
public int insertFormData(@Param("maps") List<Map<String,Object>> maps);
}
在保存动态表单方法中调用
/**
* 保存动态表单
* @param taskId 任务 id
* @param formData 控件组字符串
* @return
*/
@PostMapping("/formDataSave")
public AjaxResponse formDataSave(@RequestParam("taskId")String taskId,
@RequestParam("formData")String formData){
try{
// 这是 GlobalConfig 类定义的是否是测试标记,标记是测试环境使用 内存用户登录,方便测试使用
if(GlobalConfig.Test){
// 测试环境使用内存用户登录
securityUtil.logInAs("zhangsan");
}
// 查询任务
Task task = taskRuntime.task(taskId);
// 前端传来的字符串拆分为控件组
String[] formDataList = formData.split("!_!");
// 保存解析后的数据
List<Map<String,Object>> controlItems = new ArrayList<Map<String, Object>>();
for (String control : formDataList) {
// 分割表单数据
String[] split = control.split("-_-");
Map<String,Object> form = new HashMap<String,Object>();
// 流程定义 key
form.put("PROC_DEF_ID_", task.getProcessDefinitionId());
// 流程实例 key
form.put("PROC_INST_ID_", task.getProcessInstanceId());
// 表单 key
form.put("FORM_KEY_", task.getFormKey());
// 控件 key
form.put("Control_ID_", split[0]);
// 控件值
form.put("Control_VALUE_", split[1]);
// 是否是流程参数
form.put("Control_PARAM", split[2]);
controlItems.add(form);
}
// 将表单写入数据库
activitiMapper.insertFormData(controlItems);
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
controlItems
);
}catch (Exception e){
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.ERROR.getCode(),
"渲染动态表单失败",
e.toString()
);
}
}
可以看到动态表单插入成功
参数作为 UEL 表达式参数
画 张三动态表单 bpmn 图
模拟表单一内容
- FormProperty_2kou82p-_-string-_-姓名-_-请输入姓名-_-f
模拟表单二内容
-
FormProperty_11muvfh-_-long-_-年龄-_-请输入年龄-_-s