Activiti7 与 Spring Boot 及 Spring Security 整合 踩坑记录

2021年7月15日 2点热度 0条评论 来源: 废物大师兄

1.  前言

实话实说,网上关于Activiti的教程千篇一律,有参考价值的不多。很多都是老早以前写的,基本都是直接照搬官方提供的示例,要么就是用单元测试跑一下,要么排除Spring Security,很少有看到一个完整的项目。太难了,笔者在实操的时候,遇到很多坑,在此做一个记录。

其实,选择用Activiti7没别的原因,就是因为穷。但凡是有钱,谁还用开源版的啊,当然是用商业版啦。国外的工作流引擎没有考虑中国的实际情况,很多像回退、委派、撤销等等功能都没有,所以最省事的还是中国特色的BPM。

Activiti7的文档比较少,但是教程多。Flowable的文档比较齐全,但是网上教程少。

2.  Maven依赖

<?xml version="1.0" encoding="UTF-8"?> <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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.2</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.cjs.example</groupId> <artifactId>demo-activiti7</artifactId> <version>0.0.1-SNAPSHOT</version> <name>demo-activiti7</name> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.activiti</groupId> <artifactId>activiti-spring-boot-starter</artifactId> <version>7.1.0.M6</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.12.0</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.4</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.10.0</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.76</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>

配置 application.properties

server.port=8080 server.servlet.context-path=/activiti7 spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&nullCatalogMeansCurrent=true spring.datasource.username=root spring.datasource.password=123456 spring.jpa.database=mysql spring.jpa.open-in-view=true spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true spring.jpa.show-sql=true spring.redis.host=192.168.28.31 spring.redis.port=6379 spring.redis.password=123456 spring.redis.database=1 spring.activiti.database-schema-update=true spring.activiti.db-history-used=true spring.activiti.history-level=full spring.activiti.check-process-definitions=false spring.activiti.deployment-mode=never-fail

代码是最好的老师,查看代码所有配置项都一目了然

这里最好关闭自动部署,不然每次项目启动的时候就会自动部署一次

3.  集成 Spring Security

详见我另一篇 《基于 Spring Security 的前后端分离的权限控制系统》 

3.1.  实体类

权限

package com.cjs.example.entity; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import java.io.Serializable; import java.util.Set; /** * 菜单表 * @Author ChengJianSheng * @Date 2021/6/12 */ @Setter @Getter @Entity @Table(name = "sys_menu") public class SysMenuEntity implements Serializable { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id") private Integer id; /** * 资源编码 */ @Column(name = "code") private String code; /** * 资源名称 */ @Column(name = "name") private String name; /** * 菜单/按钮URL */ @Column(name = "url") private String url; /** * 资源类型(1:菜单,2:按钮) */ @Column(name = "type") private Integer type; /** * 父级菜单ID */ @Column(name = "pid") private Integer pid; /** * 排序号 */ @Column(name = "sort") private Integer sort; @ManyToMany(mappedBy = "menus") private Set<SysRoleEntity> roles; }

角色

package com.cjs.example.entity; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import java.io.Serializable; import java.util.Set; /** * 角色表 * @Author ChengJianSheng * @Date 2021/6/12 */ @Setter @Getter @Entity @Table(name = "sys_role") public class SysRoleEntity implements Serializable { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id") private Integer id; /** * 角色名称 */ @Column(name = "name") private String name; @ManyToMany(mappedBy = "roles") private Set<SysUserEntity> users; @ManyToMany @JoinTable(name = "sys_role_menu", joinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")}, inverseJoinColumns = {@JoinColumn(name = "menu_id", referencedColumnName = "id")}) private Set<SysMenuEntity> menus; @ManyToMany @JoinTable(name = "sys_dept_role", joinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")}, inverseJoinColumns = {@JoinColumn(name = "dept_id", referencedColumnName = "id")}) private Set<SysDeptEntity> depts; } 

部门 

package com.cjs.example.entity; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import java.io.Serializable; import java.util.Set; /** * 部门表 * @Author ChengJianSheng * @Date 2021/6/12 */ @Setter @Getter @Entity @Table(name = "sys_dept") public class SysDeptEntity implements Serializable { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id") private Integer id; /** * 部门名称 */ @Column(name = "name") private String name; /** * 父级部门ID */ @Column(name = "pid") private Integer pid; /** * 组对应的角色 */ @ManyToMany(mappedBy = "depts") private Set<SysRoleEntity> roles; } 

用户

package com.cjs.example.entity; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import java.io.Serializable; import java.time.LocalDate; import java.util.Set; /** * 用户表 * @Author ChengJianSheng * @Date 2021/6/12 */ @Setter @Getter @Entity @Table(name = "sys_user") public class SysUserEntity implements Serializable { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id") private Integer id; @Column(name = "username") private String username; @Column(name = "password") private String password; @Column(name = "mobile") private String mobile; @Column(name = "enabled") private Integer enabled; @Column(name = "create_time") private LocalDate createTime; @Column(name = "update_time") private LocalDate updateTime; @OneToOne @JoinColumn(name = "dept_id") private SysDeptEntity dept; @ManyToMany @JoinTable(name = "sys_user_role", joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")}) private Set<SysRoleEntity> roles; } 

3.2.  自定义 UserDetailsService

package com.cjs.example.domain; import lombok.Setter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.Set; /** * @Author ChengJianSheng * @Date 2021/6/12 * @see User * @see User */ @Setter public class MyUserDetails implements UserDetails { private String username; private String password; private boolean enabled; private Set<SimpleGrantedAuthority> authorities; public MyUserDetails(String username, String password, boolean enabled, Set<SimpleGrantedAuthority> authorities) { this.username = username; this.password = password; this.enabled = enabled; this.authorities = authorities; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return enabled; } } 

MyUserDetailsService

package com.cjs.example.service; import com.cjs.example.domain.MyUserDetails; import com.cjs.example.entity.SysMenuEntity; import com.cjs.example.entity.SysRoleEntity; import com.cjs.example.entity.SysUserEntity; import com.cjs.example.repository.SysUserRepository; import org.apache.commons.lang3.StringUtils; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; /** * @Author ChengJianSheng * @Date 2021/6/12 */ @Service public class MyUserDetailsService implements UserDetailsService { @Resource private SysUserRepository sysUserRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUserEntity sysUserEntity = sysUserRepository.findByUsername(username); Set<SysRoleEntity> userRoles = sysUserEntity.getRoles(); Set<SysRoleEntity> deptRoles = sysUserEntity.getDept().getRoles(); Set<SysRoleEntity> roleSet = new HashSet<>(); roleSet.addAll(userRoles); roleSet.addAll(deptRoles); Set<SimpleGrantedAuthority> authorities = roleSet.stream().flatMap(role->role.getMenus().stream()) .filter(menu-> StringUtils.isNotBlank(menu.getCode())) .map(SysMenuEntity::getCode) // .map(e -> "ROLE_" + e.getCode()) .map(SimpleGrantedAuthority::new) .collect(Collectors.toSet()); return new MyUserDetails(sysUserEntity.getUsername(), sysUserEntity.getPassword(), 1==sysUserEntity.getEnabled(), authorities); } }

如果加了“ROLE_”前缀,那么比较的时候应该用 SimpleGrantedAuthority 进行比较

这里姑且不加这个前缀了,因为后面集成 Activiti 的时候用户组有一个前缀 GROUP_

package com.cjs.example.service; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import java.util.Set; import java.util.stream.Collectors; @Component("myAccessDecisionService") public class MyAccessDecisionService { public boolean hasPermission(String permission) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Object principal = authentication.getPrincipal(); if (principal instanceof UserDetails) { UserDetails userDetails = (UserDetails) principal; Set<String> set = userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet()); return set.contains(permission); // // AuthorityUtils.createAuthorityList(permission); // SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission); // return userDetails.getAuthorities().contains(simpleGrantedAuthority); } return false; } } 

3.3.  自定义Token过滤器

package com.cjs.example.filter; import com.alibaba.fastjson.JSON; import com.cjs.example.domain.MyUserDetails; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.concurrent.TimeUnit; /** * @Author ChengJianSheng * @Date 2021/6/17 */ @Component public class TokenFilter extends OncePerRequestFilter { @Autowired private StringRedisTemplate stringRedisTemplate; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String token = request.getHeader("token"); String key = "TOKEN:" + token; if (StringUtils.isNotBlank(token)) { String value = stringRedisTemplate.opsForValue().get(key); if (StringUtils.isNotBlank(value)) { MyUserDetails user = JSON.parseObject(value, MyUserDetails.class); if (null != user && null == SecurityContextHolder.getContext().getAuthentication()) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); // 刷新token // 如果生存时间小于10分钟,则再续1小时 long time = stringRedisTemplate.getExpire(key); if (time < 600) { stringRedisTemplate.expire(key, (time + 3600), TimeUnit.SECONDS); } } } } chain.doFilter(request, response); } } 

3.3.  WebSecurityConfig

package com.cjs.example.config; import com.cjs.example.filter.TokenFilter; import com.cjs.example.handler.*; import com.cjs.example.service.MyUserDetailsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 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.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; /** * @Author ChengJianSheng * @Date 2021/6/12 */ @EnableGlobalMethodSecurity(prePostEnabled = true) @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyUserDetailsService myUserDetailsService; @Autowired private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler; @Autowired private MyAuthenticationFailureHandler myAuthenticationFailureHandler; @Autowired private MyLogoutSuccessHandler myLogoutSuccessHandler; @Autowired private TokenFilter tokenFilter; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .successHandler(myAuthenticationSuccessHandler) .failureHandler(myAuthenticationFailureHandler) .and() .logout().logoutSuccessHandler(myLogoutSuccessHandler) .and() .authorizeRequests() .antMatchers("/activiti7/login").permitAll() .anyRequest().authenticated() .and() .exceptionHandling() .accessDeniedHandler(new MyAccessDeniedHandler()) .authenticationEntryPoint(new MyAuthenticationEntryPoint()) .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .maximumSessions(1) .maxSessionsPreventsLogin(false) .expiredSessionStrategy(new MyExpiredSessionStrategy()); http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class); http.csrf().disable(); } public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }

至此一切都很顺利,毕竟之前也写过很多遍。

package com.cjs.example.controller; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @Author ChengJianSheng * @Date 2021/6/12 */ @RestController @RequestMapping("/hello") public class HelloController { @PreAuthorize("@myAccessDecisionService.hasPermission('hello:sayHello')") @GetMapping("/sayHello") public String sayHello() { return "hello"; } @PreAuthorize("@myAccessDecisionService.hasPermission('hello:sayHi')") @GetMapping("/sayHi") public String sayHi() { return "hi"; } } 

4. 集成 Activiti7

启动项目以后,activiti相关表已经创建好了

接下来,以简单的请假为例来演示

<process id="leave" name="leave" isExecutable="true"> <startEvent id="startevent1" name="Start"></startEvent> <userTask id="usertask1" name="填写请假单" activiti:assignee="${sponsor}"></userTask> <sequenceFlow id="flow1" sourceRef="startevent1" targetRef="usertask1"></sequenceFlow> <endEvent id="endevent1" name="End"></endEvent> <sequenceFlow id="flow2" sourceRef="usertask1" targetRef="endevent1"></sequenceFlow> <userTask id="usertask2" name="经理审批" activiti:candidateGroups="${manager}"></userTask> <sequenceFlow id="flow3" sourceRef="usertask1" targetRef="usertask2"></sequenceFlow> <endEvent id="endevent2" name="End"></endEvent> <sequenceFlow id="flow4" sourceRef="usertask2" targetRef="endevent2"></sequenceFlow> </process>

4.1.  部署流程定义

package com.cjs.example.controller; import com.cjs.example.domain.RespResult; import com.cjs.example.util.ResultUtils; import lombok.extern.slf4j.Slf4j; import org.activiti.engine.RepositoryService; import org.activiti.engine.repository.Deployment; import org.activiti.engine.repository.ProcessDefinition; import org.apache.commons.io.IOUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.util.zip.ZipInputStream; /** * @Author ChengJianSheng * @Date 2021/7/12 */ @Slf4j @RestController @RequestMapping("/deploy") public class DeploymentController { @Autowired private RepositoryService repositoryService; /** * 部署 * @param file ZIP压缩包文件 * @param processName 流程名称 * @return */ @PostMapping("/upload") public RespResult<String> upload(@RequestParam("zipFile") MultipartFile file, @RequestParam("processName") String processName) { String originalFilename = file.getOriginalFilename(); if (!originalFilename.endsWith("zip")) { return ResultUtils.error("文件格式错误"); } ProcessDefinition processDefinition = null; try { ZipInputStream zipInputStream = new ZipInputStream(file.getInputStream()); Deployment deployment = repositoryService.createDeployment().addZipInputStream(zipInputStream).name(processName).deploy(); processDefinition = repositoryService.createProcessDefinitionQuery().deploymentId(deployment.getId()).singleResult(); } catch (IOException e) { log.error("流程部署失败!原因: {}", e.getMessage(), e); } return ResultUtils.success(processDefinition.getId()); } /** * 查看流程图 * @param deploymentId 部署ID * @param resourceName 图片名称 * @param response * @return */ @GetMapping("/getDiagram") public void getDiagram(@RequestParam("deploymentId") String deploymentId, @RequestParam("resourceName") String resourceName, HttpServletResponse response) { InputStream inputStream = repositoryService.getResourceAsStream(deploymentId, resourceName); // response.setContentType(MediaType.IMAGE_PNG_VALUE); try { IOUtils.copy(inputStream, response.getOutputStream()); } catch (IOException e) { e.printStackTrace(); } finally { IOUtils.closeQuietly(inputStream); } } }

首先登录一下

 

然后,将流程图文件打成zip压缩包

查看流程图

4.2.  启动流程实例

最开始,我是这样写的

ProcessInstance processInstance = processRuntime.start(ProcessPayloadBuilder .start() .withProcessDefinitionId(processDefinitionId) .withVariable("sponsor", authentication.getName()) .build());

当我这样写了以后,第一个问题出现了,没有权限访问

查看代码之后,我发现调用ProcessRuntime的方法需要当前登录用户有“ACTIVITI_USER” 权限

于是,我在数据库sys_menu表里加了一条数据

 

重新登录后,zhangsan可以调用ProcessRuntime里面的方法了

很快,第二个问题出现了, 当我用 ProcessRuntime#start() 启动流程实例的时候报错了

org.activiti.engine.ActivitiException: Query return 2 results instead of max 1 at org.activiti.engine.impl.DeploymentQueryImpl.executeSingleResult(DeploymentQueryImpl.java:213) ~[activiti-engine-7.1.0.M6.jar:na] at org.activiti.engine.impl.DeploymentQueryImpl.executeSingleResult(DeploymentQueryImpl.java:30) ~[activiti-engine-7.1.0.M6.jar:na]

查看代码,终于找到问题所在了

 

这明显就是 Activiti 的Bug,查询所有部署的流程没有加任何查询条件,吐了

于是,百度了一下,网上有人建议换一个版本,于是我将activiti-spring-boot-starter的版本从“7.1.0.M6”换成了“7.1.0.M5”,呵呵,又一个错,缺少字段

原来M6和M5的表结构不一样。我又将版本将至“7.1.0.M4”,这次直接起不来了

没办法,版本改回7.1.0.M6,不用ProcessRuntime,改用原来的RuntimeService

package com.cjs.example.controller; import com.cjs.example.domain.RespResult; import com.cjs.example.util.ResultUtils; import org.activiti.api.process.model.ProcessInstance; import org.activiti.api.process.model.builders.ProcessPayloadBuilder; import org.activiti.api.process.runtime.ProcessRuntime; import org.activiti.engine.RuntimeService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; /** * @Author ChengJianSheng * @Date 2021/7/12 */ @RestController @RequestMapping("/processInstance") public class ProcessInstanceController { @Autowired private ProcessRuntime processRuntime; @Autowired private RuntimeService runtimeService; @GetMapping("/start") public RespResult start(@RequestParam("processDefinitionId") String processDefinitionId) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); org.activiti.engine.runtime.ProcessInstance processInstance = null; try { // ProcessInstance processInstance = processRuntime.start(ProcessPayloadBuilder // .start() // .withProcessDefinitionId(processDefinitionId) // .withVariable("sponsor", authentication.getName()) // .build()); Map<String, Object> variables = new HashMap<>(); variables.put("sponsor", authentication.getName()); processInstance = runtimeService.startProcessInstanceById(processDefinitionId, variables); } catch (Exception ex) { ex.printStackTrace(); } return ResultUtils.success(processInstance); } }

这里注意 org.activiti.engine.runtime.ProcessInstance 和 org.activiti.api.process.model.ProcessInstance 别搞混了 

查看流程定义

package com.cjs.example.controller; import com.cjs.example.domain.RespResult; import com.cjs.example.util.ResultUtils; import org.activiti.api.process.model.ProcessDefinition; import org.activiti.api.process.runtime.ProcessAdminRuntime; import org.activiti.api.process.runtime.ProcessRuntime; import org.activiti.api.runtime.shared.query.Page; import org.activiti.api.runtime.shared.query.Pageable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @Author ChengJianSheng * @Date 2021/7/12 */ @RestController @RequestMapping("/processDefinition") public class ProcessDefinitionController { @Autowired private ProcessAdminRuntime processAdminRuntime; // private ProcessRuntime processRuntime; @GetMapping("/list") public RespResult<Page<ProcessDefinition>> getProcessDefinition(){ Page<ProcessDefinition> processDefinitionPage = processAdminRuntime.processDefinitions(Pageable.of(0, 10)); return ResultUtils.success(processDefinitionPage); } } 

4.3.  查询待办任务并完成

按照我们的流程定义,zhangsan提交了请假申请,所以第一个任务是zhangsan的,先让zhangsan登录

Page<Task> page = taskRuntime.tasks(Pageable.of(0, 10)); if (null != page && page.getTotalItems() > 0) { for (Task task : page.getContent()) { taskRuntime.complete(TaskPayloadBuilder.complete().withTaskId(task.getId()).build()); } }

由于第一个任务是一个个人任务,所以不需要先认领任务,直接去完成即可

第二个任务是一个组任务,而且我还用了流程变量,因此要么在启动流程实例的时候就给这个流程变量赋值,要么在上一个任务完成时给变量赋值。

这里,我用的是候选组(Candidate Groups),而不是候选者(Candidate Users)。二者差不多,都是组任务,区别在于如果用候选者的话需要列出所有候选用户并用逗号分隔,如果用候选组的话就只需要写组名即可,多个组之间用逗号分隔。

本例中,我也不用流程变量,例如直接写 activiti:candidateGroups="caiwu" 

taskRuntime.complete(TaskPayloadBuilder.complete().withTaskId(task.getId()).withVariable("manager", "caiwu").build());

有没有发现,这里查询任务的时候没有指定要查谁的任务,完成任务的时候也没有指定是谁完成的,这都是Spring Security的功劳

到这里可以看出,取的是当前登录用户,即 SecurityContextHolder.getContext().getAuthentication().getName()

SecurityContextHolder.getContext().getAuthentication().getName()

同理,完成任务

 

接下来的是一个组任务,任务必须由“canwu”这个组的人去完成,为了让 lisi 能看到这个任务,需要在sys_menu表中加一条记录

当lisi登录进来以后,调用 taskRuntime.tasks(Pageable.of(0, 10)) 查询自己的任务时

通过跟代码,我们知道,查询任务其实是这样的,等价于下面这段代码

Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String authenticatedUserId = authentication.getName(); List<String> userGroups = authentication.getAuthorities() .stream() .map(GrantedAuthority::getAuthority) .filter(a -> a.startsWith("GROUP_")) .map(a -> a.substring("GROUP_".length())) .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); List<Task> taskList = taskService.createTaskQuery() .taskCandidateOrAssigned(authenticatedUserId, userGroups) .processInstanceId("xxx") .listPage(0,10);

查询当前登录用户的个人任务和组任务 

接下来,让 zhaoliu 登录进来 

package com.cjs.example.controller; import org.activiti.api.runtime.shared.query.Page; import org.activiti.api.runtime.shared.query.Pageable; import org.activiti.api.task.model.Task; import org.activiti.api.task.model.builders.TaskPayloadBuilder; import org.activiti.api.task.runtime.TaskRuntime; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @Author ChengJianSheng * @Date 2021/7/12 */ @RestController @RequestMapping("/task") public class TaskController { @Autowired private TaskRuntime taskRuntime; @GetMapping("/pageList") public void pageList() { // 查询待办任务(个人任务 + 组任务) Page<Task> page = taskRuntime.tasks(Pageable.of(0, 10)); if (null != page && page.getTotalItems() > 0) { for (Task task : page.getContent()) { // 认领任务 taskRuntime.claim(TaskPayloadBuilder.claim().withTaskId(task.getId()).build()); // 完成任务 taskRuntime.complete(TaskPayloadBuilder.complete().withTaskId(task.getId()).build()); } } } }

zhaoliu完成任务后,整个流程就结束了

 

 

    原文作者:废物大师兄
    原文地址: https://www.cnblogs.com/cjsblog/p/15009949.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。