1. 前言
实话实说,网上关于Activiti的教程千篇一律,有参考价值的不多。很多都是老早以前写的,基本都是直接照搬官方提供的示例,要么就是用单元测试跑一下,要么排除Spring Security,很少有看到一个完整的项目。太难了,笔者在实操的时候,遇到很多坑,在此做一个记录。
其实,选择用Activiti7没别的原因,就是因为穷。但凡是有钱,谁还用开源版的啊,当然是用商业版啦。国外的工作流引擎没有考虑中国的实际情况,很多像回退、委派、撤销等等功能都没有,所以最省事的还是中国特色的BPM。
Activiti7的文档比较少,但是教程多。Flowable的文档比较齐全,但是网上教程少。
2. Maven依赖
<?配置 application.properties
server.port=8080server.servlet.context-path=/activiti7spring.datasource.driver-class-name=com.mysql.jdbc.Driverspring.datasource.url=jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&nullCatalogMeansCurrent=truespring.datasource.username=rootspring.datasource.password=123456spring.jpa.database=mysqlspring.jpa.open-in-view=truespring.jpa.properties.hibernate.enable_lazy_load_no_trans=truespring.jpa.show-sql=truespring.redis.host=192.168.28.31spring.redis.port=6379spring.redis.password=123456spring.redis.database=1spring.activiti.database-schema-update=truespring.activiti.db-history-used=truespring.activiti.history-level=fullspring.activiti.check-process-definitions=falsespring.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 */@Setterpublic 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 */@Servicepublic 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 */@Componentpublic 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)@EnableWebSecuritypublic 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 ProcessInstan......原文转载:http://www.shaoqun.com/a/876494.html
跨境电商:https://www.ikjzd.com/
赛兔:https://www.ikjzd.com/w/2375
史泰博:https://www.ikjzd.com/w/2112
亚马逊应用商店:https://www.ikjzd.com/w/531
1.前言实话实说,网上关于Activiti的教程千篇一律,有参考价值的不多。很多都是老早以前写的,基本都是直接照搬官方提供的示例,要么就是用单元测试跑一下,要么排除SpringSecurity,很少有看到一个完整的项目。太难了,笔者在实操的时候,遇到很多坑,在此做一个记录。其实,选择用Activiti7没别的原因,就是因为穷。但凡是有钱,谁还用开源版的啊,当然是用商业版啦。国外的工作流引擎没有考虑
达方物流:https://www.ikjzd.com/w/2562
去香港购物哪里好?:http://www.30bags.com/a/404841.html
去香港购物刷卡好还是现金好?香港刷卡是怎么扣钱的?:http://www.30bags.com/a/426397.html
去香港购物选购手机要注意什么?:http://www.30bags.com/a/404807.html
去香港国际机场DFS需要注意些什么?:http://www.30bags.com/a/404090.html
口述被三人干了一晚上 两个男人做我的详细过程:http://lady.shaoqun.com/a/248079.html
七十女人喜欢被㖭 进去后女人就不反抗了:http://www.30bags.com/m/a/249920.html
又粗又长我被老外玩晕了 老外把我女朋友啪肿了:http://www.30bags.com/m/a/249739.html
性生活强忍不射精好不好 小心逆行射精带来4个危害 :http://lady.shaoqun.com/a/421136.html
退款不退货!亚马逊卖家损失上万元且不断增加:https://www.ikjzd.com/articles/146621
精致好用的独居生物!你们是穿着最朴素的衣服生活的小仙女。:http://lady.shaoqun.com/a/421137.html
救赎——我出轨的时候,我老公就在隔壁:http://lady.shaoqun.com/a/421138.html