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. centOS 7-Hadoop3.3.0完全分布式部署

    本文内容不乏对各大佬的案例借鉴,侵删. 本次实验用到的有虚拟机,Xshell,Hadoop压缩包和jdk压缩包   hadoop111A:192.168.241.111 hadoop222B:192. ...

  2. Contos8 安装 MariaDb 时报错:Could not open mysql.plugin table: table mysql.plugin

    导语: 因个人服务器误删了一些文件导致MariaDB崩溃,一直在报错,所以想着重装一下,没想到在重装后启动时再次报错(与之前报错不同),这次的报错原因大致是因为某些插件表找不到. 因此又开启了漫长的寻 ...

  3. MySQL 主从复制&读写分离 简介

    1. 读写分离&读写分离 简介 主从同步延迟 分配机制 解决单点故障 总结 2. 主从复制&读写分离 搭建 搭建主从复制(双主) 搭建读写分离 1. 读写分离&读写分离 简介 ...

  4. 【Java实现】剑指offer53.1——在排序数组中查找数字(LeetCode34:在排序数组中查找元素的起始位置)

    序数组中查找元素的起始位置):思路分享 <剑指offer>题目和LeetCode主站本质是一样的,想要找到target数目,也需要找到左右边界 题目解析: 在一个排序数组中,找到targe ...

  5. mybatis设置自动提交事务

    我们想要mybatis帮助我们自动提交事务其实很简单,只需要在SqlSessionFactory对象的openSession方法中设置参数为true就可以了,mybatis工具类如下: public ...

  6. 实验3、Flask数据库操作-如何使用Flask与数据库

    1. 实验内容 数据库的使用对于可交互的Web应用程序是极其重要的,本节我们主要学习如何与各种主要数据库进行连接和使用,以及ORM的使用 2. 实验要点 掌握Flask对于各种主要数据库的连接方法 掌 ...

  7. centos 7 能ping通但是telnet 22 不通解决方法

    试过关闭Linux防火墙,打开sshd服务,修改ens33为enth0.发现不好使. 经过不断的努力我发现,windows系统里面的VMware Network Adapter VMnet8设置的ip ...

  8. MySQL 页完全指南——浅入深出页的原理

    之前写了一些关于 MySQL 的 InnoDB 存储引擎的文章,里面好几次都提到了页(Pages)这个概念,但是都只是简要的提了一下.例如之前在聊 InnoDB内存结构 时提到过,但当时的重点是内存架 ...

  9. Jenkins 流水线远程部署 .NET Core/Framework 到 IIS

    目录 Windows 安装 Git WebDeploy Windows 从节点 .NET Core 处理 IIS 处理项目 Jenkinsfile .NET Framework 安装环境 .NET F ...

  10. Java 设置Word文本框中的文字旋转方向

    Word文档中可添加文本框,并设置文本框为横向文本排列或是纵向文本排列,或者设置文本框中的文字旋转方向等.通过Java程序代码,也可以实现以上文本框的操作.下面以Java代码示例展示具体的实现步骤.另 ...