网站首页 > 文章精选 正文
大家好,我是杰哥
上一篇文章,我们介绍了一个常用工作流框架: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 的衍生的确是程序员的福音呢,它使得技术越来越简单化,让我们的学习与使用成本大大降低了呢
文章演示代码地址: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 吗?
.........
职业、生活感悟
你有没有想过,旅行的意义是什么?
程序员的职业规划
灵魂拷问:人生最重要的是什么?
如何高效学习一个新技术?
如何让自己更坦然地度过一天?
..........
猜你喜欢
- 2024-12-30 简单的使用SpringBoot整合SpringSecurity
- 2024-12-30 Spring Security 整合OAuth2 springsecurity整合oauth2+jwt+vue
- 2024-12-30 DeepSeek-Coder-V2震撼发布,尝鲜体验
- 2024-12-30 一个数组一行代码,Spring Security就接管了Swagger认证授权
- 2024-12-30 简单漂亮的(图床工具)开源图片上传工具——PicGo
- 2024-12-30 Spring Boot(十一):Spring Security 实现权限控制
- 2024-12-30 绝了!万字搞定 Spring Security,写得太好了
- 2024-12-30 SpringBoot集成Spring Security springboot集成springsecurity
- 2024-12-30 SpringSecurity密码加密方式简介 spring 密码加密
- 2024-12-30 Spring cloud Alibaba 从入门到放弃
- 最近发表
- 标签列表
-
- newcoder (56)
- 字符串的长度是指 (45)
- drawcontours()参数说明 (60)
- unsignedshortint (59)
- postman并发请求 (47)
- python列表删除 (50)
- 左程云什么水平 (56)
- 计算机网络的拓扑结构是指() (45)
- 稳压管的稳压区是工作在什么区 (45)
- 编程题 (64)
- postgresql默认端口 (66)
- 数据库的概念模型独立于 (48)
- 产生系统死锁的原因可能是由于 (51)
- 数据库中只存放视图的 (62)
- 在vi中退出不保存的命令是 (53)
- 哪个命令可以将普通用户转换成超级用户 (49)
- noscript标签的作用 (48)
- 联合利华网申 (49)
- swagger和postman (46)
- 结构化程序设计主要强调 (53)
- 172.1 (57)
- apipostwebsocket (47)
- 唯品会后台 (61)
- 简历助手 (56)
- offshow (61)