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完成任務後,整個流程就結束了