程序员求职经验分享与学习资料整理平台

网站首页 > 文章精选 正文

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

balukai 2024-12-30 01:58:16 文章精选 11 ℃

大家好,我是杰哥

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

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

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

据我了解,很多 Spring Boot 框架的 WEB 项目中,往往就是直接采用这两个方式一起进行权限管理的工作流控制的,所以,为了完整起见,我们今天就直接来学习 ActivitiSpring 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 项目的演示需要,在之前的基础上,分别增加了两个用户:JackMarry,以及他们对应的新角色: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 数据库中,便增加了 25ACT 开头的表

备注:这些表的说明,也可以参考上一篇文章: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)流程,由于是运行时状态,所以采用 RuntimeServicestartProcessInstanceByKey(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;
}

测试启动:

指定参数 keyleaveApplication ,进行启动操作

访问成功后,看看表中的数据有没有变化呢,查看 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 来完成“创建申请”的待办任务,分别指定 keyleaveApplicationassigneeJack ,进行任务的完成操作:

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

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

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

7 Marry -审批申请

1)登录

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

此时,采用 Marry 进行登录

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

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

3)完成任务

指定 keyleaveApplicationassigneeMarry,访问接口 /process/complete

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

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

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

总结

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

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

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

文章演示代码地址:https://github.com/helemile/Spring-Boot-Notes/tree/master/activiti/activiti


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


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



往期精彩回顾


总结复盘

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

带领新人团队的沉淀总结

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

网络篇

网络篇(四):《图解 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 吗?

.........

职业、生活感悟

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

程序员的职业规划

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

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

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

..........

Tags:

最近发表
标签列表