前言:本文基于若依前后端分离版本(Spring Boot 3.3.0 + Vue 3 + Activiti 8.1.0)进行改造,相关教程可以在网上找到。在撰写此博客期间,笔者刚刚开始接触 Java Web,本系列下的文章内容包含大量“个人初期”视角,注意鉴别。
概述
Activiti8 是一个强大的流程管理工具,可以实现流程相关的业务,包括申请、任务发放、审批等操作。通过 Spring Boot 的 Security 进行鉴权,能够实现流程的部署、激活、挂起等功能。其核心目的是自动化实现流程图中的各个节点,对节点任务进行监控以及流程的执行。在流程测试员环节,可以规定由哪些用户或用户组负责,这些用户登录系统后就能看到当前需要处理的任务。
权限要求
Activiti7 及以上版本对权限有严格要求,主要涉及 GROUP_、ROLE_ACTIVITI_USER 等字段。需要将所有登录用户划分到对应的组内,暂时用 post_code(岗位代码)表示。例如,HR 人力资源部门需要添加 GROUP_HR 权限,也可以根据实际的部门(如 dept_code)进行权限划分。同时,所有用户必须拥有后一个权限,否则无法应用 Activiti 相关功能。
若依代码修改(添加权限)
若依本身实现了角色主导的权限控制,规定角色可以访问的内容,对每个用户赋予一个角色。若依原有的权限列表 permissions,规定了对一些菜单的增删改查权限。另一部分是修改代码后新添加的权限字段,基于角色和用户组的权限。
UserDetailsServiceImpl.java
1 2 3 4 5 6 7 8 public UserDetails createLoginUser (SysUser user) { Set<String> postCode = sysPostService.selectPostCodeByUserId(user.getUserId()); postCode = postCode.parallelStream().map( s -> "GROUP_" + s).collect(Collectors.toSet()); postCode.add("ROLE_ACTIVITI_USER" ); List<SimpleGrantedAuthority> collect = postCode.stream().map(s -> new SimpleGrantedAuthority (s)).collect(Collectors.toList()); return new LoginUser (user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user), collect); }
当然,上述代码中的用户组就可以根据自己的需求去改了,记得修改mapper、service之类的。
LoginUser.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private Set<String> permissions;private List<SimpleGrantedAuthority> authorities;public LoginUser (Long userId, Long deptId, SysUser user, Set<String> permissions, List<SimpleGrantedAuthority> authorities) { this .userId = userId; this .deptId = deptId; this .user = user; this .permissions = permissions; this .authorities = authorities; } @Override public Collection<? extends GrantedAuthority > getAuthorities(){ return authorities; }
最终,登录用户的信息应该是这样的:
数据库建表
由于 Activiti8 某个表中的字段与 MySQL 的一个类型会起冲突,需要手动添加表。其他表在初始化的时候会自动创建,跟着网上的其他帖子做就可以。以下是创建 act_hi_identitylink 表的 SQL 语句(据说更高版本的activiti修复了这个BUG):
1 2 3 4 5 6 7 8 9 CREATE TABLE `act_hi_identitylink` ( `ID_` varchar (64 ) COLLATE utf8_bin NOT NULL , `GROUP_ID_` varchar (255 ) COLLATE utf8_bin DEFAULT NULL , `TYPE_` varchar (255 ) COLLATE utf8_bin DEFAULT NULL , `USER_ID_` varchar (255 ) COLLATE utf8_bin DEFAULT NULL , `TASK_ID_` varchar (64 ) COLLATE utf8_bin DEFAULT NULL , `PROC_INST_ID_` varchar (64 ) COLLATE utf8_bin DEFAULT NULL , `DETAILS_` longblob, PRIMARY KEY (`ID_`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8 COLLATE = utf8_bin
小插曲
到这一步按理说就可以正常使用activiti了,跟着网上的教程也应该是如此。但是在实际应用中遇到了另一个问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 *************************** APPLICATION FAILED TO START *************************** Description: Field permitAllUrl in com.iams.framework.config.SecurityConfig required a single bean, but 2 were found: - requestMappingHandlerMapping: defined by method 'requestMappingHandlerMapping' in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class] - controllerEndpointHandlerMapping: defined by method 'controllerEndpointHandlerMapping' in class path resource [org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.class] This may be due to missing parameter name information Action: Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed Ensure that your compiler is configured to use the '-parameters' flag. You may need to update both your build tool settings as well as your IDE. (See https://github.com/spring-projects/spring-framework/wiki/Upgrading-to-Spring-Framework-6.x#parameter-name-retention)
鄙人初学者,啥也不懂,感觉大概意思是“我”需要一个bean,但是找到了两个同类型的,“我”不知道用哪个。一顿操作猛如虎,全靠GPT帮我写,找不到具体用哪个?直接指明就行!
PermitAllUrlProperties.java
1 2 3 4 5 6 private final RequestMappingHandlerMapping mapping; @Autowired public PermitAllUrlProperties (@Qualifier("requestMappingHandlerMapping") RequestMappingHandlerMapping mapping) { this .mapping = mapping; }
部署流程(通过 BPMN)
先不考虑如何进行前端传文件进行部署的方式,后端静态部署一个流程看看:
查询是否已经存在 当前流程(BPMN 文件中的 process id=“borrowArchive” ):
1 2 3 long count = repositoryService.createProcessDefinitionQuery() .processDefinitionKey(“borrowArchive”) .count();
如果不存在,就开始部署流程,传入名称与文件路径:
1 2 3 4 Deployment deployment = repositoryService.createDeployment() .addClasspathResource(bpmnFilePath) .name("档案借阅流程" ) .deploy();
emmmmm,虽然部署了,但我们怎么用他?我们怎么获得一些关于定义好的流程的信息?我们先得把他从部署的一大堆流程中拿出来再仔细揣摩它,activiti提供了一个查找全部已部署流程的接口:processDefinitionQuery。pageDomain是一个分页对象,具体是什么,不知道,先用着,以后慢慢了解。
1 2 3 4 5 6 7 8 9 10 import org.activiti.engine.RepositoryService;@Autowired private RepositoryService repositoryService;ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery().orderByProcessDefinitionId().orderByProcessDefinitionVersion().desc();List<ProcessDefinition> processDefinitions = processDefinitionQuery.listPage((pageDomain.getPageNum() - 1 ) * pageDomain.getPageSize(), pageDomain.getPageSize());
在List后面打一个断点,debug一下,仔细(大概)看一下部署后的流程有哪些信息。
嫌麻烦?其实也还可以直接查数据库,毕竟数据库的字段都是要被拿来增删查改的!可以查看ACT_RE_PROCDEF
表,大概是下面这个样子:
当然,如果还嫌麻烦,直接查看ProcessDefinition类的定义也行,熟悉代码的小伙伴一眼就明白。
知道有些啥东西就行(了吧),反正笔者也是初学者,知道在哪看怎么看就行,用到它的信息再回来仔细对比。
实例化(创建)流程
是时候发起一个流程了!也就是实例化,方法如下:其中的DefinitionKey一定要对应我们部署的流程,我们要发起哪个流程就靠它跟上文我们部署的流程进行对应了。可以看到这里还有一个withName,似乎并不是之前的流程名,而是具体的事例。对的,这里的Name是具体流程实例层面的,之前的Name是对整个流程的名字(讲道理,没跑代码,有些忘了)。
1 2 3 4 5 6 ProcessInstance processInstance = processRuntime.start(ProcessPayloadBuilder .start() .withProcessDefinitionKey(“borrowArchive”) .withName(“借阅申请——张三”) .withBusinessKey(id) .build());
不过发起实例之后我们可以去数据库看看,这个实例有哪些字段。(我的另一个项目,两个流程,各发起了一个实例,两个流程都是两个任务的流程 ,而且我另一个项目中启动实例的时候并没有添加名字,所以此处的Name字段是空的)
另外还有一个比较有意思的地方,两个实例应该就两条数据,为什么这里是四个?而且明晃晃的PARENT_ID
,也就是说如果遇到多任务的,会根据任务顺序串成层次表?
确实是,当然还有别的类型,先不管啦!
任务分发
每个流程的关节点一般需要进行审批等工作,bpmn 文件中会使用 activiti:candidateUsers=“${deptLeader}” 规定当前任务的候选人/候选组。实例化流程时会使用 withVariable(“deptLeader”, “ry”)(跟着上面的withName续写就行)指定处理该任务的候选人(似乎是会传一个参数进去保存,用户名?ID?实测下来是用户名)。
候选人(比如:ry)登录系统后,通过 taskRuntime.tasks(),调用当前用户信息查询对应任务,进行展示发送。代码上的流程其实跟查询部署的流程很像:
ActTaskServiceImpl.java(代码细节暂不深究,能跑就行!好像还跟security有关?)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import org.activiti.api.task.runtime.TaskRuntime; @Autowired private TaskRuntime taskRuntime; @Override public Page<ActTaskDTO> selectProcessDefinitionList (PageDomain pageDomain) { Page<ActTaskDTO> list = new Page <ActTaskDTO>(); org.activiti.api.runtime.shared.query.Page<Task> pageTasks = taskRuntime.tasks(Pageable.of((pageDomain.getPageNum() - 1 ) * pageDomain.getPageSize(), pageDomain.getPageSize())); List<Task> tasks = pageTasks.getContent(); int totalItems = pageTasks.getTotalItems(); list.setTotal(totalItems); if (totalItems != 0 ) { Set<String> processInstanceIdIds = tasks.parallelStream().map(t -> t.getProcessInstanceId()).collect(Collectors.toSet()); List<ProcessInstance> processInstanceList = runtimeService.createProcessInstanceQuery().processInstanceIds(processInstanceIdIds).list(); List<ActTaskDTO> actTaskDTOS = tasks.stream() .map(t -> new ActTaskDTO (t, processInstanceList.parallelStream().filter(pi -> t.getProcessInstanceId().equals(pi.getId())).findAny().get())) .collect(Collectors.toList()); list.addAll(actTaskDTOS); } return list; }
其实也可以稍微看一下数据库中的表,初窥门径:ACT_RU_TASK
、ACT_RU_IDENTITYLINK
,分别标识当前任务、当前任务分配给的用户与组。
实例键与业务键
Activiti 实例化流程时会生成 ID 用于标记每一个流程实例,但业务键由用户提供,用于绑定唯一的业务实体。实例键由 Activiti 生成,用于标记每一个流程实例。如果业务键是唯一的,那么业务键和实例键都可以唯一指定一个流程实例。也可以不设置业务键,将 getId() 的结果绑定到业务实体上,但不推荐这样做。
任务执行
说审批任务,其实大多数情况下审批环节就两个字段:审批结果、备注。流程任务之间判断一些值的情况,比如BPMN文件中:
1 2 3 <sequenceFlow id="flow3" name="通过" sourceRef="decisionGateway" targetRef="approveEnd"> <conditionExpression xsi:type="tFormalExpression">${approvalResult == '通过'}</conditionExpression> </sequenceFlow>
其中的approvalResult
就是一个需要我们传入的值,如何将审批结果与传入流程的变量关联起来?我这里说一个不太合适的方式:把前端传来的键值对,全部注册成流程的变量,相当于我们把string a = "通过";
传入一个判断语句,看看a是不是“通过”。
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 @Override public int formDataSave (String taskID, List<ActWorkflowFormDataDTO> awfs) throws ParseException { Task task = taskRuntime.task(taskID); Map<String, Object> variables = new HashMap <>(); for (ActWorkflowFormDataDTO awf : awfs) { if ("i" .equals(awf.getControlIsParam())) { variables.put(awf.getControlId(), awf.getControlValue()); } } if (task.getAssignee() == null ) { taskRuntime.claim(TaskPayloadBuilder.claim() .withTaskId(task.getId()) .build()); } runtimeService.setVariables(task.getProcessInstanceId(), variables); taskRuntime.complete(TaskPayloadBuilder.complete() .withTaskId(taskID) .withVariables(variables) .build()); return 1 ; }
上面这段代码基本就是把前端送来的键值对(现版本是固定的approvalResult:value1,approvalComment:value2),把他们添加到流程中,流程判断语句会判断value1是不是“通过”。
状态监听
待审核、已批准、未通过三种常见的状态需要实时传达给前端用户。现阶段的流程推进没有对数据库做任何更改,需要添加一个监听器,去修改数据库中的具体状态。流程会给状态 state 注入值,当任务走到这里时开始执行函数。比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class ArchiveDestroyStateListener implements ExecutionListener { private Expression state; @Override public void notify (DelegateExecution delegateExecution) { String instanceId = delegateExecution.getProcessInstanceId(); String businessKey = delegateExecution.getProcessInstanceBusinessKey(); ArchiveDestroyApprovalMapper archiveDestroyApprovalMapper = SpringUtils.getBean(ArchiveDestroyApprovalMapper.class); ArchiveDestroyApproval archiveDestroyApproval = archiveDestroyApprovalMapper.selectArchiveDestroyApprovalByInstanceIdAndBusinessKey(instanceId, businessKey); archiveDestroyApproval.setStatus(state.getValue(delegateExecution).toString()); archiveDestroyApprovalMapper.updateArchiveDestroyApproval(archiveDestroyApproval); } }
通过当前的任务查询对应的业务记录(MySQL数据),修改业务记录的信息,前端就能进行展示了。