最近新项目要用工作流,查了几天资料,主要集中在 Activiti7、Flowable、Camunda 三个,同是 jbpm 框架发展而来,各有优劣。最终选择了 Activiti7,原因无他,仅是手头参与的其他项目用这个,方便尽快上手,下个项目应该会试试另外两个。
本次项目采用 RuoYi-Vue-v3.8 开发,使用的 Springboot 版本为 v2.5.8。记录一下接入使用过程及踩坑信息,便于自己以后查看,如果对其他人有一些帮助也算意外之喜吧。以下内容主要针对于本次项目,可能一些说明不准确或解决方式不完善的地方,能力有限,只能留有遗憾了。
一、项目引入 Activiti 依赖
在 pom 文件中添加 Activiti 相关依赖:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter</artifactId>
<version>7.1.0.M4</version>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.activiti.dependencies</groupId>
<artifactId>activiti-dependencies</artifactId>
<version>7.1.0.M4</version>
<type>pom</type>
</dependency>
<!-- Activiti 生成流程图 -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-image-generator</artifactId>
<version>7.1.0.M4</version>
</dependency>项目使用 Mysql 数据库,已引入驱动,因此这里不再添加。Activiti 默认使用 H2 数据库,如项目未使用数据库,应引入数据库驱动。
二、 添加 Activiti 相关配置
修改 application.yml 配置文件,添加内容如下:
1
2
3
4
5
6
7
8
9
10
11
12spring:
...
# 工作流
activiti:
deployment-mode: never-fail # 关闭 SpringAutoDeployment
check-process-definitions: false #自动部署验证设置:true- 开启(默认)、false- 关闭
database-schema-update: true #true 表示对数据库中所有表进行更新操作。如果表不存在,则自动创建。
history-level: full #full 表示全部记录历史,方便绘制流程图
db-history-used: true #true 表示使用历史表
main:
allow-bean-definition-overriding: true #不同配置文件中存在 id 或者 name 相同的 bean 定义,后面加载的 bean 定义会覆盖前面的 bean 定义
三、 启动项目,生成数据表
踩坑 1:不明白别人的项目为什么要加 “allow-bean-definition-overriding“ 属性,所以没加。结果很快就明白为什么要加了,项目启动出现如下错误(错误信息还给出了解决提醒,我真搞笑):
1
2
3
4
5Description:
The bean 'methodSecurityInterceptor', defined in class path resource [org/activiti/spring/boot/MethodSecurityConfig.class], could not be registered. A bean with that name has already been defined in class path resource [org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.class] and overriding is disabled.
Action:
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true踩坑 2:在上述步骤之后,重新运行项目,然后又出现了如下错误:
1
2
3
4
5
6
7
8
916:46:33.758 [restartedMain] INFO o.a.e.i.c.ProcessEngineConfigurationImpl - [configuratorsAfterInit,1571] - Executing configure() of class org.activiti.spring.process.conf.ProcessExtensionsConfiguratorAutoConfiguration$$EnhancerBySpringCGLIB$$3d85fc52 (priority:10000)
16:46:33.893 [restartedMain] ERROR o.a.e.i.i.CommandContext - [logException,149] - Error while closing command context
org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: java.sql.SQLSyntaxErrorException: Table 'zhhq.act_ge_property' doesn't exist
### The error may exist in org/activiti/db/mapping/entity/Property.xml
### The error may involve org.activiti.engine.impl.persistence.entity.PropertyEntityImpl.selectProperty-Inline
### The error occurred while setting parameters
### SQL: select * from ACT_GE_PROPERTY where NAME_ = ?
### Cause: java.sql.SQLSyntaxErrorException: Table 'zhhq.act_ge_property' doesn't exist- 解决方式:修改 mysql 连接字符串,添加 &nullCatalogMeansCurrent=true
1
2
3
4// 原来的 url
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
// 修改后 url,添加了 &nullCatalogMeansCurrent=true
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true- 网友给出的说明:因为 mysql 使用 schema 标识库名而不是 catalog,因此 mysql 会扫描所有的库来找表,如果其他库中有相同名称的表,activiti 就以为找到了,本质上这个表在当前数据库中并不存在。设置 nullCatalogMeansCurrent=true,表示 mysql 默认当前数据库操作,在 mysql-connector-java 5.xxx 该参数默认为 true,在 6.xxx 以上默认为 false,因此需要设置 nullCatalogMeansCurrent=true。
经过以上修改步骤,再次运行项目。好吧,这次我成功了,项目顺利启动,且数据库增加了 25 个表。
留个坑,其实这版 Activiti 自动生成的表字段不全,下面有补充说明。
四、 Activiti7 整合 Spring Security
- Activiti7 没有身份管理的表,其能力依赖和 Spring Security 整合,新 Api 包括 TaskRuntime 和 ProcessRuntime 都会强制使用 Security 验证用户权限。
- 查看 ProcessRuntime 类的源码可发现,需要“ACTIVITI_USER”角色权限。因此,处理思路是,在登录验证时,给登录用户增加对应权限。
- 在 RuoYi 里,登录验证的实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35public String login(String username, String password, String code, String uuid)
{
boolean captchaOnOff = configService.selectCaptchaOnOff();
// 验证码开关
if (captchaOnOff)
{
validateCaptcha(username, code, uuid);
}
// 用户验证
Authentication authentication = null;
try
{
// 该方法会去调用 UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}
catch (Exception e)
{
if (e instanceof BadCredentialsException)
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
throw new UserPasswordNotMatchException();
}
else
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
throw new ServiceException(e.getMessage());
}
}
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
recordLoginInfo(loginUser.getUserId());
// 生成 token
return tokenService.createToken(loginUser);
} - 根据上述代码中,需要在 UserDetailsServiceImpl.loadUserByUsername 中加入对应逻辑:
- 原代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class UserDetailsServiceImpl implements UserDetailsService
{
private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
private ISysUserService userService;
private SysPermissionService permissionService;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
SysUser user = userService.selectUserByUserName(username);
if (StringUtils.isNull(user))
{
log.info(" 登录用户:{} 不存在.", username);
throw new ServiceException(" 登录用户:" + username + " 不存在 ");
}
else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
{
log.info(" 登录用户:{} 已被删除.", username);
throw new ServiceException(" 对不起,您的账号:" + username + " 已被删除 ");
}
else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
{
log.info(" 登录用户:{} 已被停用.", username);
throw new ServiceException(" 对不起,您的账号:" + username + " 已停用 ");
}
return createLoginUser(user);
}
public UserDetails createLoginUser(SysUser user)
{
return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
}
}- 修改后的代码:
1
2
3
4
5
6
7
8// 有变更的方法,在原来的用户信息类中加入了需要的权限信息
public UserDetails createLoginUser(SysUser user) {
Set<String> postCode = sysPostService.selectPostCodeByUserId(user.getUserId());
postCode = postCode.parallelStream().map(s -> "GROUP_" + s).collect(Collectors.toSet());
postCode.add("ROLE_ACTIVITI_USER");
List<SimpleGrantedAuthority> collect = postCode.stream().map(s -> new SimpleGrantedAuthority(s)).collect(Collectors.toList());
return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user), collect);
} - 修改相应的用户信息类 LoginUser
- 原代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67public class LoginUser implements UserDetails
{
private static final long serialVersionUID = 1L;
/**
* 用户 ID
*/
private Long userId;
/**
* 部门 ID
*/
private Long deptId;
/**
* 用户唯一标识
*/
private String token;
/**
* 登录时间
*/
private Long loginTime;
/**
* 过期时间
*/
private Long expireTime;
/**
* 登录 IP 地址
*/
private String ipaddr;
/**
* 登录地点
*/
private String loginLocation;
/**
* 浏览器类型
*/
private String browser;
/**
* 操作系统
*/
private String os;
/**
* 权限列表
*/
private Set<String> permissions;
/**
* 用户信息
*/
private SysUser user;
// 省去 set/get 方法
public Collection<? extends GrantedAuthority> getAuthorities()
{
return null;
}
}- 修改后的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class LoginUser implements UserDetails
{
// 去掉不变的代码
// +++++
private List<SimpleGrantedAuthority> authorities;
// +++++
public void setAuthorities(List<SimpleGrantedAuthority> authorities) {
this.authorities = authorities;
}
public Collection<? extends GrantedAuthority> getAuthorities()
{
return authorities; // 变更
}
}- 主要是实体类中,增加了权限属性,且修改了对应的 get 方法。
- 现在,Activiti 就可以识别到登录用户,且有了新 Api 对应的权限。
- 总结一下,对于 Ruoyi 框架原来的代码实现而言,修改了两处,LoginUser 类和 UserDetailsServiceImpl 类,当然,依赖方法增加了相应的查询岗位(postCode)的方法。
- 整合工作就是这样,实在是不清不楚,马马虎虎。鉴于现在的时间和精力,只能紧着项目的实际问题来,希望以后有机会系统地学习一下 Activiti 吧。
五、 工作流简单测试
- 准备工作流(bpmn)文件,因为之后项目要接入 web 流程设计器,所以没在 IDE 上安装设计器插件,在旧项目里随便找了个 bpmn 文件测试用;
- 编写单元测试,先进行工作流部署测试
- 编写单元测试代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ActivitiTest {
private RepositoryService repositoryService;
public void deployTest() {
Deployment deployment = repositoryService.createDeployment()
.addClasspathResource("leave.bpmn")
.name(" 测试流程 ")
.deploy();
System.out.println(" 部署 ID:" + deployment.getId());
}
} - 运行后,出现缺失表字段错误:
1
2### SQL: insert into ACT_RE_DEPLOYMENT(ID_, NAME_, CATEGORY_, KEY_, TENANT_ID_, DEPLOY_TIME_, ENGINE_VERSION_, VERSION_, PROJECT_RELEASE_VERSION_) values(?, ?, ?, ?, ?, ?, ?, ?, ?)
### Cause: java.sql.SQLSyntaxErrorException: Unknown column 'VERSION_' in 'field list' - 好吧,Activiti7.1.0.M4 这版通过自动创建的表结构是少字段的,这 BUG 之前有网友提过,自己忘记这茬了,现在补坑吧:
1
2
3-- 修复 Activiti7 的 M4 版本缺失字段 Bug
alter table ACT_RE_DEPLOYMENT add column PROJECT_RELEASE_VERSION_ varchar(255) DEFAULT NULL;
alter table ACT_RE_DEPLOYMENT add column VERSION_ varchar(255) DEFAULT NULL; - 再次运行,测试顺利通过,打印部署成功日志:
1
15:59:08.035 [main] INFO o.a.e.i.b.d.BpmnDeployer - [dispatchProcessDefinitionEntityInitializedEvent,234] - Process deployed: {id: leave:1:f67ecd7e-a047-11ec-a9cd-d45d64273150, key: leave, name: 请假流程 - 普通表单 }
- 进行创建流程实例测试
- 编写测试代码:
1
2
3
4
5
6
7
8
9
public void startProcessTest() {
ProcessInstance processInstance = processRuntime.start(ProcessPayloadBuilder
.start()
.withProcessDefinitionKey("test")
.withName(" 请假测试 ")
.build());
System.out.println(" 实例 ID:" + processInstance.getId());
} - 运行,如果出现以下错误:
1
org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
- 是因为没有整合 SpringSecurity ,上面整合内容里描述了新提供 Apl,比如 ProcessRuntime 必须有身份验证,看源码可知:
1
2
public class ProcessRuntimeImpl implements ProcessRuntime {} - 两种解决思路:一种是整合 SpringSecurity;一种是试一下旧版 Api。那么试下旧版 Api 吧,重新修改代码:
1
2
3
4
5
6
7
public void startProcessTest() {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("deptLeader", "test01");
ProcessInstance pi = runtimeService.startProcessInstanceByKey("leave", " 测试请假 ", paramMap);
System.out.println(" 实例 ID:" + pi.getId());
} - 再次运行,顺利通过测试。
- 单元测试就算完了,证明 Activiti 顺利接入到了项目中,等前端流程设计器接入了,才能真正方便地用起来吧。
六、 数据表命名规则说明
- 简单记录一下 Activiti 表的命名规则,留个印象吧,想知道具体信息还是得对应到每个表;
- Activiti 的表都以 ”ACT_“ 开头。第二部分是表示表的用途的两个字母标识。用途也和服务的 API 对应。
- act_hi_*:’hi’表示 history,此前缀的表包含历史数据,如历史 (结束) 流程实例,变量,任务等等。
- act_ge_*:’ge’表示 general,此前缀的表为通用数据,用于不同场景中。
- act_evt_*:’evt’表示 event,此前缀的表为事件日志。
- act_procdef_*:’procdef’表示 processdefine,此前缀的表为记录流程定义信息。
- act_re_*:’re’表示 repository,此前缀的表包含了流程定义和流程静态资源(图片,规则等等)。
- act_ru_*:’ru’表示 runtime,此前缀的表是记录运行时的数据,包含流程实例,任务,变量,异步任务等运行中的数据。Activiti 只在流程实例执行过程中保存这些数据,在流程结束时就会删除这些记录。
后记
很基础的一些东西,算是项目使用工作流的第一步吧。接下来,就是在前端项目中接入流程设计器了,然后是实际使用中一些处理和技巧,一步步来。
原文链接: https://xinghuipeng.pages.dev/2022/03/09/work/java/Springboot接入Activiti7/
版权声明: 转载请注明出处.