Spring Boot(十三):Spring Boot 集成 Activiti - 快速实现工作流

4,701 阅读8分钟

大家好,我是杰哥

上一篇文章,我们介绍了一个常用工作流框架:Activiti,并通过实际例子,了解了它的工作原理与使用步骤。实际上,Spring Boot 也集成了这个框架,从而能够让我们轻松实现项目中的工作流

需要说明的是,Spring Boot 集成的 Activiti 依赖 activiti-spring-boot-starter中 ,同时也集成了 Security 框架,所以说它可谓是自带权限控制功能,这一点从设计上出发,的确是比较人性化的了,毕竟实际需求中的工作流,往往涉及到特定用户的操作,比如有些工作流只能由某些人发起或者查看,而有些流程,只能由某些人进行审批等

当然你也可以考虑将 Security 换成其他权限控制的框架,或者在初学时,直接将 Security 去掉即可(具体方式网上都有的,可以参考)

据我了解,很多 Spring Boot 框架的 WEB 项目中,往往就是直接采用这两个方式一起进行权限管理的工作流控制的,所以,为了完整起见,我们今天就直接来学习 Activiti 与 Spring Security 结合来实现工作流控制的方法与步骤

本次演示信息如下(均为当前最新版本):

  • spring-boot-starter-parent 依赖版本:2.7.0

  • activiti-spring-boot-starter 依赖版本:7.1.0.M6

  • spring-boot-starter-security 依赖版本:2.7.0

一 security 相关

对于 Spring Boot 集成 Security 的步骤,我们在文章 Spring Boot(十一):Spring  Security 实现权限控制 中已经很详细地介绍过,可以跳过去大概阅读一番,以便更好地入门此次的知识

1 引入 spring-boot-starter-security 依赖

虽然 activiti-spring-boot-starter 也包含了 Security 的依赖,但是在 7.1.0.M6 版本里,却不能够直接使用最新版本的权限配置方式(自定义 SecurityFilterChain Bean 来实现权限配置),所以我这里便额外引入了 spring-boot-starter-security 的依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2 表结构

遵从 Security 所采取的权限访问控制方案:RABC -基于角色的权限访问控制(Role-Based Access Control),我们建立如下 5 张表

图片

建表语句如下:

/*
 Navicat Premium Data Transfer

 Source Server         : localhost
 Source Server Type    : MySQL
 Source Server Version : 50733
 Source Host           : localhost:3306
 Source Schema         : activiti_spring

 Target Server Type    : MySQL
 Target Server Version : 50733
 File Encoding         : 65001

 Date: 18/07/2022 15:55:30
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for permission
-- ----------------------------
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `url` varchar(255) NOT NULL,
  `name` varchar(255) NOT NULL,
  `description` varchar(255) DEFAULT NULL,
  `pid` bigint(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of permission
-- ----------------------------
BEGIN;
INSERT INTO `permission` VALUES (1, '/user/common', 'common', NULL, 0);
INSERT INTO `permission` VALUES (2, '/user/admin', 'admin', NULL, 0);
INSERT INTO `permission` VALUES (3, '/process/*', 'activiti', NULL, 0);
COMMIT;

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of role
-- ----------------------------
BEGIN;
INSERT INTO `role` VALUES (1, 'USER');
INSERT INTO `role` VALUES (2, 'ADMIN');
INSERT INTO `role` VALUES (3, 'ACTIVITI_USER');
COMMIT;

-- ----------------------------
-- Table structure for role_permission
-- ----------------------------
DROP TABLE IF EXISTS `role_permission`;
CREATE TABLE `role_permission` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(11) NOT NULL,
  `permission_id` bigint(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of role_permission
-- ----------------------------
BEGIN;
INSERT INTO `role_permission` VALUES (1, 1, 1);
INSERT INTO `role_permission` VALUES (2, 2, 1);
INSERT INTO `role_permission` VALUES (3, 2, 2);
INSERT INTO `role_permission` VALUES (4, 3, 3);
COMMIT;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) NOT NULL,
  `password` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user
-- ----------------------------
BEGIN;
INSERT INTO `user` VALUES (1, 'user', '$2a$10$4zd/aj2BNJhuM5PIs5BupO8tiN2yikzP7JMzNaq1fXhcXUefWCOF2');
INSERT INTO `user` VALUES (2, 'admin', '$2a$10$4zd/aj2BNJhuM5PIs5BupO8tiN2yikzP7JMzNaq1fXhcXUefWCOF2');
INSERT INTO `user` VALUES (3, 'Jack', '$2a$10$4zd/aj2BNJhuM5PIs5BupO8tiN2yikzP7JMzNaq1fXhcXUefWCOF2');
INSERT INTO `user` VALUES (4, 'Marry', '$2a$10$4zd/aj2BNJhuM5PIs5BupO8tiN2yikzP7JMzNaq1fXhcXUefWCOF2');
COMMIT;

-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(11) NOT NULL,
  `role_id` bigint(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user_role
-- ----------------------------
BEGIN;
INSERT INTO `user_role` VALUES (1, 1, 1);
INSERT INTO `user_role` VALUES (3, 2, 2);
INSERT INTO `user_role` VALUES (4, 2, 3);
INSERT INTO `user_role` VALUES (5, 3, 3);
INSERT INTO `user_role` VALUES (6, 4, 3);
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;

其实就是直接拿来文章 Spring Boot(十一):Spring  Security 实现权限控制 中的表结构,并根据我们本次对于 Activiti 项目的演示需要,在之前的基础上,分别增加了两个用户:Jack 和 Marry,以及他们对应的新角色:ACTIVITI_USER 以及新 url :/process/* 的访问权限。当然,根据 RABC 的方式,也配置了其对应的用户角色、角色权限的关系

3 配置类

采用 Spring Boot Security 的最新方式实现,即:

  • 如果想要配置过滤器链,可以通过自定义  SecurityFilterChain Bean 来实现

  • 如果想要配置  WebSecurity,可以通过  WebSecurityCustomizer Bean 来实现

@Configuration
@Slf4j
public class SecurityConfig {

    @Resource
    private PermissionMapper permissionMapper;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry
                authorizeRequests = http.csrf().disable().authorizeRequests();
//        方式二:配置来源于数据库
//         1.查询到所有的权限
        List<Permission> allPermission = permissionMapper.findAllPermission();
//         2.分别添加权限规则
        allPermission.forEach((p -> {
            authorizeRequests.antMatchers(p.getUrl()).hasAnyAuthority(p.getName()) ;
        }));

        authorizeRequests.antMatchers("/**").fullyAuthenticated()
                .anyRequest().authenticated().and().formLogin();
        return http.build();
    }

    @Bean
    WebSecurityCustomizer webSecurityCustomizer() {
        return web -> {
            web.ignoring().antMatchers("/css/**");
            web.ignoring().antMatchers("/js/**");
            web.ignoring().antMatchers("/img/**");
            web.ignoring().antMatchers("/plugins/**");
            web.ignoring().antMatchers("/login.html");
        };
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

当前用户所拥有的的权限等信息,则通过实现 UserDetailsService 接口,并重写其方法 loadUserByUsername() 来获取:

@Slf4j
@Service
public class UserServiceImpl implements UserService  {

    @Resource
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1.根据用户名称查询到user用户 
        User userDetails = userMapper.findByUsername(username);
        if (userDetails == null) {
            return null;
        }
        // 2.查询该用户对应的权限 
        List<Permission> permissionList = userMapper.findPermissionByUsername(username);
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        permissionList.forEach((a) -> grantedAuthorities.add(new SimpleGrantedAuthority(a.getName())));
        log.info(">>permissionList:{}<<", permissionList);
        // 设置权限 
        userDetails.setAuthorities(grantedAuthorities);
        return userDetails;
    }
}

到此,就完成了 Security 的相关配置,接下来,让我们正式进入 Spring Boot 集成 Activiti 的实战环节

二 进入 Spring Boot 集成 Activiti 的实战

需要预先说明的是,

Activiti 提供了几个 Service 类,用来管理工作流,常用的有以下四项:

  • 1)RepositoryService:提供流程定义和部署等功能。比如说,实现流程的的部署、删除,暂停和激活以及流程的查询等功能

  • 2)RuntimeService:提供了处理流程实例不同步骤的结构和行为。包括启动流程实例、暂停和激活流程实例等功能

  • 3)TaskService:提供有关任务相关功能的服务。包括任务的查询、删除以及完成等功能

  • 4)HistoryService:提供 Activiti 引擎收集的历史记录信息服务。主要用于历史信息的查询功能

    还有以下两项:

1)ManagementService:job 任务查询和数据库操作

2)DynamicBpmnService:无需重新部署就能修改流程定义内容

而 Spring Boot 集成 Activiti 实现工作流功能,也主要是采用这些 Service 所提供的 相应的 API 来实现的

1 配置文件

application.yml 文件的配置

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/activiti_spring?useUnicode=true&characterEncoding=utf8&nullCatalogMeansCurrent=true
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  activiti:
#    flase:       默认值。activiti在启动时,会对比数据库表中保存的版本,如果没有表或者版本不匹配,将抛出异常。(生产环境常用)
#    true:        activiti会对数据库中所有表进行更新操作。如果表不存在,则自动创建。(开发时常用)
#    create_drop: 在activiti启动时创建表,在关闭时删除表(必须手动关闭引擎,才能删除表)。(单元测试常用)
#    drop-create: 在activiti启动时删除原来的旧表,然后在创建新表(不需要手动关闭引擎)。
    database-schema-update: true
    #默认不生成历史表,这里开启
    db-history-used: true
    #历史登记
#    none: 不记录历史流程,性能高,流程结束后不可读取
#    activity: 归档流程实例和活动实例,流程变量不同步
#    audit: 默认值,在activiti基础上同步变量值,保存表单属性
#    full: 性能较差,记录所有实例和变量细节变化,最完整的历史记录,如果需要日后跟踪详细可以开启full(一般不建议开启)
    history-level: full
    deployment-mode:  never-fail # 关闭 SpringAutoDeployment

除了数据库的配置信息以外,在我们的 demo 演示环境,Activiti 的配置如下:

  • database-schema-update 配置为 true,即每次项目启动,都会对数据库进行更新操作,如果表不存在,则自动创建

  • db-history-used 配置为 true 由于默认是不生成历史表的,配置为 true,表示需要生成

  • history-level 配置 为 full,表示记录最完整的历史记录

  • deployment-mode 配置为 never-fail ,即关闭掉 SpringAutoDeployment。如果不关闭,每次重新启动项目的时候,总是会在 ACT_RE_DEPLOYMENT 自动创建一个名为 SpringAutoDeployment 工作流记录。但是在开发阶段,需要经常重启项目,久而久之就会导致 ACT_RE_DEPLOYMENT 的记录越来越大了

2 表结构创建

首次启动时,根据以上的配置,Activiti 会进行表结构的自动创建,以下是项目初次启动时的日志

图片

由于上述我们配置了开启历史表的使用开关:db-history-used: true

所以,这里除了创建 Activiti 的非历史表结构以外,还创建了其历史表结构

项目启动之后,会发现,我们所配置的 activiti_spring 数据库中,便增加了 25 张 ACT 开头的表

图片

备注:这些表的说明,也可以参考上一篇文章:Activiti工作流(一):OA 上的那些请假流程如何快速实现呢?

3 流程部署

总得来说,可以分为两种方式:自动部署和手动部署 

自动部署,实际上是 Spring Boot 会在启动项目时,会自动部署项目目录下的 processes 文件夹中的所有流程,而没有在这个目录下的所有流程定义文件,则需要手动方式进行部署了

我们先来看一下自动部署的方式

1)自动部署

a) 创建流程定义

备注:具体创建方式本文不再赘述,大家可以参考上一篇文章:Activiti工作流(一):OA 上的那些请假流程如何快速实现呢?

 在 resources 目录下,创建一个 processes 文件夹,在该文件夹下分别创建两个简单的流程:

  • 请假申请(leaveApplication)

  • 出差申请(myEvection)

图片

其中,请假流程定义了两个步骤:

图片

创建申请和审批申请,其负责人分别定义为 ${assignee0} 和 $ {assignee1}

图片

图片

b)启动项目

项目启动时,完成 process 目录下的流程文件的自动部署

图片

观察日志,可以看到我们预先创建好的两个流程定义,均被自动部署了 

我们知道,Activiti 的逻辑,实际上是通过对一系列表的操作,来实现工作流的控制。而流程的部署,则是把我们所创建的流程定义文件,真正与数据库的表关联起来

那么,我们来看看看流程定义表:ACT_RE_PROCDEF

图片

发现表中已有相应的两条记录,验证了这两个流程的确完成了自动部署 

当然,如果定义好的流程文件没有在 processes 文件夹下,就需要我们手动部署了

2)手动部署

可以通过如下方式指定 bpmn 文件的目录:filePath 进行部署

repositoryService.createDeployment()
.addClasspathResource(filePath)
.deploy();

由于是对流程定义的操作,所以我们采用 RepositoryService 这个服务进行部署

4 查询流程定义

查询流程定义,依旧是对流程定义的操作,所以采用 RepositoryService 服务进行查询,具体代码与上篇文章中的查询方式类似

/**
 * 查询流程
 * @param key
 * @return
 */
@GetMapping(value = {"/list/{key}","/list"})
public ResponseResult getProcessList(@PathVariable(name = "key",required = false) String key) {
    ProcessDefinitionQuery definitionQuery = repositoryService.createProcessDefinitionQuery();
    List<ProcessDefinition> definitionList;
    if (key!=null){
        definitionList = definitionQuery
                .processDefinitionKey(key)
                .list();
    }

   definitionList = definitionQuery.list();
    //提取所有的流程名称
    List<String> processList = new ArrayList<>();
    for (ProcessDefinition processDefinition : definitionList) {
        processList.add(processDefinition.getName());
    }
    return ResponseResult.getSuccessResult(processList);
}

采用 Jack 用户登录之后,带上其 Cookie 头参数,进行如下访问

图片

成功访问

5 启动流程

本次实战,我们来启动请假申请(leaveApplication)流程,由于是运行时状态,所以采用 RuntimeService 的 startProcessInstanceByKey(key,map) 方法进行启动,具体代码如下:

/**
 * 启动流程定义(由流程定义-》流程实例)
 * @param key
 * @return
 */
@PostMapping("start/{key}")
public ResponseResult startProcess(@PathVariable(name = "key") String key){
    Map<String,Object> map = new HashMap<>();
    map.put("assignee0","Jack");
    map.put("assignee1","Marry");
    //启动 key 标识的流程定义,并指定 流程定义中的两个参数:assignee0和assignee1
    ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(key,map);
    ResponseResult result = ResponseResult.getSuccessResult(processInstance.getProcessDefinitionName());
    log.info("流程实例的内容:{}",processInstance);
    return result;

}

测试启动:

图片

指定参数 key 为 leaveApplication ,进行启动操作 

访问成功后,看看表中的数据有没有变化呢,查看 ACT_RU_TASK 表,该表中新增了一条记录,为创建申请,负责人为 Jack,表示我们的这个流程已经成功启动,目前进行到了创建申请这一任务了,其负责人为 Jack

图片

当前登录的用户就是 Jack,此时我们通过接口 /process/task/list 接口来查看 Jack 的任务列表

图片

发现目前在 Jack 名下的确存在一个待办任务:创建申请

6 Jack -创建申请

对任务的完成,依旧是采用 TaskService 进行,具体代码如下:

/**
 * 完成任务
 * @param key
 * @param assigne
 * @return
 */
@PostMapping("complete")
public ResponseResult doTask(@RequestParam(name = "key") String key,@RequestParam(name = "assignee")String assigne){
    List<Task> tasks = taskService.createTaskQuery().processDefinitionKey(key)
            .taskAssignee(assigne)
            .list();
    if (tasks!=null && tasks.size()>0){
        for (Task task : tasks) {
            log.info("任务名称:{}",task.getName());
            taskService.complete(task.getId());
            log.info("{},任务已完成",task.getName());

        }
    }
    return ResponseResult.getSuccessResult(null);

}

由 Jack 来完成“创建申请”的待办任务,分别指定 key 为 leaveApplication ,assignee 为 Jack ,进行任务的完成操作:

图片

访问成功之后,我们再来查看 ACT_RU_TASK(当前任务表)

图片

按照预期,此时正要处理的任务为审批申请,并且负责人为 Marry

当前我们采用 Jack 登录的这个 Cookie 来进行查询,发现 task 列表已经变成了空的

图片

7 Marry -审批申请

1)登录

访问 localhost:8080/logout 登出,会跳转到登录页面

图片

此时,采用 Marry 进行登录

2)查看其名下的任务列表

发现的确存在一个任务:审批申请

3)完成任务

指定 key 为 leaveApplication,assignee 为 Marry,访问接口 /process/complete

图片

接口返回成功 此时,再次查询我们的 ACT_RU_TASK 表,会发现我们已经没有了正在处理的任务,说明我们已完成了所有的任务

图片

再次查看历史表 ACT_HI_TASKINST,会看到我们所处理过的两个任务

图片

好了,走到这一步,我们便成功完成了 Spring Boot 集成 Activiti 进行流程定义、流程部署、流程启动、任务完成等工作流管理的一系列操作

总结

今天带领大家完成了 Spring Boot 集成 Activiti 的功能,网上目前可能很难找到类似的案例,因为我们这里采用的 Spring Boot 和 Activiti 以及 Security 均是当前最新的版本

Spring Boot 与 Activiti 集成,使得我们不仅可以继续使用 Activiti 本身所提供的简易的 API, 而且 Spring Boot 还提供了很多自动化的东西,包括启动时自动创建表结构,自动完成 processes 目录下的流程部署操作等等

真心觉得 Spring Boot 的衍生的确是程序员的福音呢,它使得技术越来越简单化,让我们的学习与使用成本大大降低了呢

文章演示代码地址:github.com/helemile/Sp…

嗯,就这样。每天学习一点,时间会见证你的强大~

欢迎大家关注我们的公众号,一起持续性学习吧~

往期精彩回顾

总结复盘

架构设计读书笔记与感悟总结

带领新人团队的沉淀总结

复盘篇:问题解决经验总结复盘

网络篇

网络篇(四):《图解 TCP/IP》读书笔记

网络篇(一):《趣谈网络协议》读书笔记(一)

事务篇章

事务篇(四):Spring事务并发问题解决

事务篇(三):分享一个隐性事务失效场景

事务篇(一):毕业三年,你真的学会事务了吗?

Docker篇章

Docker篇(六):Docker Compose如何管理多个容器?

Docker篇(二):Docker实战,命令解析

Docker篇(一):为什么要用Docker?

..........

SpringCloud篇章

Spring Cloud(十三):Feign居然这么强大?

Spring Cloud(十):消息中心篇-Kafka经典面试题,你都会吗?

Spring Cloud(九):注册中心选型篇-四种注册中心特点超全总结

Spring Cloud(四):公司内部,关于Eureka和zookeeper的一场辩论赛

..........

Spring Boot篇章

Spring Boot(十二):陌生又熟悉的 OAuth2.0 协议,实际上每个人都在用

Spring Boot(七):你不能不知道的Mybatis缓存机制!

Spring Boot(六):那些好用的数据库连接池们

Spring Boot(四):让人又爱又恨的JPA

SpringBoot(一):特性概览

..........

翻译

[译]用 Mint 这门强大的语言来创建一个 Web 应用

【译】基于 50 万个浏览器指纹的新发现

使用 CSS 提升页面渲染速度

WebTransport 会在不久的将来取代 WebRTC 吗?

.........

职业、生活感悟

你有没有想过,旅行的意义是什么?

程序员的职业规划

灵魂拷问:人生最重要的是什么?

如何高效学习一个新技术?

如何让自己更坦然地度过一天?

..........