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完成任务后,整个流程就结束了

Activiti7 与 Spring Boot 及 Spring Security 整合 踩坑记录的更多相关文章

  1. Spring boot配置MongoDB以及Morphia踩坑记录

    pom 因为项目中采用Morphia(MongoDB的ODM框架,对象-文档映射(object-document mapper)),因此需要在pom文件中引入相应依赖: <dependency& ...

  2. Spring Boot 开发系列一 开发踩坑

    这是学习spring boot 的第二周,公司号称这玩意是啥都不会的新手就可以填空开发,于是决定上手一把,怎么说我也是搞了快七八年的.NET和.NETcore,没想到无情打脸,快被这个能填空开的IDE ...

  3. mondrian4 kylin saiku 整合踩坑记录

    1 先说了版本: Mondrian 4 .kylin2.2 .saiku 3.15 2 saiku 3.15 使用的xml是基于 mondrian4 的schema的xml.判断是不是mondrian ...

  4. spring boot与jdbcTemplate的整合案例2

    简单入门了spring boot后,接下来写写跟数据库打交道的案例.博文采用spring的jdbcTemplate工具类与数据库打交道. 下面是搭建的springbootJDBC的项目的总体架构图: ...

  5. Spring Boot(Spring的自动整合框架)

    Spring Boot 是一套基于Spring框架的微服务框架,由于Spring是一个轻量级的企业开发框架,主要功能就是用于整合和管理其他框架,想法是将平时主流使用到的框架的整合配置预先写好,然后通过 ...

  6. spring boot+freemarker+spring security标签权限判断

    spring boot+freemarker+spring security标签权限判断 SpringBoot+SpringSecurity+Freemarker项目中在页面上使用security标签 ...

  7. Spring Boot 中使用 MyBatis 整合 Druid 多数据源

    2017 年 10 月 20 日   Spring Boot 中使用 MyBatis 整合 Druid 多数据源 本文将讲述 spring boot + mybatis + druid 多数据源配置方 ...

  8. Spring Boot 学习笔记(六) 整合 RESTful 参数传递

    Spring Boot 学习笔记 源码地址 Spring Boot 学习笔记(一) hello world Spring Boot 学习笔记(二) 整合 log4j2 Spring Boot 学习笔记 ...

  9. Spring Boot数据访问之整合Mybatis

    在Mybatis整合Spring - 池塘里洗澡的鸭子 - 博客园 (cnblogs.com)中谈到了Spring和Mybatis整合需要整合的点在哪些方面,需要将Mybatis中数据库连接池等相关对 ...

随机推荐

  1. 如何在 Docker 环境下自动给 .NET 程序生成 Dump

    前言 之前"一线码农"大佬有写文章介绍了如何在 windows 下自动 dump,正好手里有个在 docker 环境下 dump 的需求,所以在参考大佬文章的基础上,有了本篇. ​ ...

  2. Selenium click不生效 报错selenium.common.exceptions.InvalidArgumentException

    记录在使用selenium过程中踩的坑------ 在使用selenium时,用click点击网站弹出的文件上传框的"上传文件"按钮不生效,报错selenium.common.ex ...

  3. Lidar激光雷达市场

    Lidar激光雷达市场 近年来,激光雷达技术在飞速发展,从一开始的激光测距技术,逐步发展了激光测速.激光扫描成像.激光多普勒成像等技术,如今在无人驾驶.AGV.机器人等领域已相继出现激光雷达的身影. ...

  4. 视频动作定位的分层自关注网络:ICCV2019论文解析

    视频动作定位的分层自关注网络:ICCV2019论文解析 Hierarchical Self-Attention Network for Action Localization in Videos 论文 ...

  5. 智能物联网(AIoT,2020年)(下)

    智能物联网(AIoT,2020年)(下) 12工业物联网是AIoT在工业领域第一战场 工业物联网分为感知.决策.执行,OS与软件是大脑+神经 13工业场景下一步如何使用AIoT 不止工业物联网:用人工 ...

  6. Kubeedge Edged概述

    Kubeedge Edged概述 Overview EdgeD是管理节点生命周期的边缘节点模块.它可以帮助用户在边缘节点上部署容器化的工作负载或应用程序.这些工作负载可以执行任何操作,从简单的远程遥测 ...

  7. TinyML-TVM是如何驯服Tiny的(下)

    TinyML-TVM是如何驯服Tiny的(下) Lazy Execution实际上,随着通信开销开始占主导地位,一旦用户请求,就执行算子的开销变得非常昂贵.可以通过延迟评估直到用户需要调用的结果来提高 ...

  8. 1-3. SpringBoot基础,Java配置(全注解配置)取代xml配置

    最近突发奇想,整合一下以前一些学习笔记,分享自己这几年爬过的坑,逐步更新文章,谢谢大家的关注和支持. 这节讲一下SpringBoot的学习必须的一些基础,Java配置.其实在Spring2.0时代就已 ...

  9. Vue.js源码解析-Vue初始化流程

    目录 前言 1. 初始化流程概述图.代码流程图 1.1 初始化流程概述 1.2 初始化代码执行流程图 2. 初始化相关代码分析 2.1 initGlobalAPI(Vue) 初始化Vue的全局静态AP ...

  10. Task00:绪论 - 环境搭建

    本章重点: 在电脑上安装MySQL数据库系统 安装客户端并连接到本机上的MySQL数据库 使用提供的脚本创建本教程所使用的示例数据库 1. MySQL 8.0 的安装 考虑到大家所使用的操作系统的不同 ...