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. Assignment Problem的若干思考

      最近受到南京一个同学的push,又开始了博客园写作之旅.欢迎大家联系我做代码实现工作,QQ:1198552514.权当赚点生活费~ 我的研究也经常用的Assignment problem,而且很多 ...

  2. Golang基础编程(一)-基本结构、数据类型、流程语句

    一.Go语言简介 简洁,高效,并发 二.语言特性 ·简洁,只有25个关键字 ·函数多返回值 ·匿名函数和闭包 ·类型和接口 ·自动垃圾回收 ·编译迅速 ·并发编程 25个关键字: 三.Go程序基本结构 ...

  3. .Net Redis实战——事务和数据持久化

    Redis事务 Redis事务可以让一个客户端在不被其他客户端打断的情况下执行多个命令,和关系数据库那种可以在执行的过程中进行回滚(rollback)的事务不同,在Redis里面,被MULTI命令和E ...

  4. Manjaro Linux安装singularity-container

    技术背景 容器化技术在各种生产领域已经得到了广泛的应用,这得益于容器的轻量化(相比于虚拟机而言),安全性(隔离弱于虚拟机,但是权限控制得当的情况下也可以认为是安全隔离的)以及系统级虚拟化带来的高可用性 ...

  5. 3DPytorch-API NVIDIA Kaolin

    3DPytorch-API NVIDIA Kaolin NVIDIA Kaolin library provides a PyTorch API for working with a variety ...

  6. 深度树匹配模型(TDM)

    深度树匹配模型(TDM) 算法介绍 Tree-based Deep Match(TDM)是由阿里妈妈精准定向广告算法团队自主研发,基于深度学习上的大规模(千万级+)推荐系统算法框架.在大规模推荐系统的 ...

  7. 部署通用基础设施, 满足顶级 SLA 要求

    部署通用基础设施, 满足顶级 SLA 要求 Telefónica 使用基于英特尔 至强 可扩展处理器和英特尔 傲腾 数据中心级固态盘 的 VMware 虚拟存储区域网络 (vSAN)* 架构,完成对高 ...

  8. Python-selenium,切换句柄及封装

    一.获取当前句柄及所有句柄 handle=driver.current_window_handle #获取当前窗口句柄print(handle)handles=driver.window_handle ...

  9. python通过字典实现购物车案例-用户端

    import os dict01 = { 'iphone' : { '5999' : { '总部位于美国' : '价格相对较贵', }, }, 'wahaha' : { '15' : { '总部位于中 ...

  10. 面试热点|理解TCP/IP传输层拥塞控制算法

    0x00.前言 通过本文你将了解到以下内容: 拥塞控制概念以及其背景 流量控制和拥塞控制的区别与联系 拥塞控制主要过程详解 伙伴们认真学习一下,让offer来得更猛烈些吧! 0x01.TCP/IP协议 ...