目录
前言
最近新项目要用工作流,查了几天资料,主要集中在 Activiti7、Flowable、Camunda 三个,同是 jbpm 框架发展而来,各有优劣。最终选择了Activiti7,原因无他,仅是手头参与的其他项目用这个,方便尽快上手,下个项目应该会试试另外两个。
本次项目采用 RuoYi-Vue-v3.8 开发,使用的 Springboot 版本为v2.5.8。记录一下接入使用过程及踩坑信息,便于自己以后查看,如果对其他人有一些帮助也算意外之喜吧。以下内容主要针对于本次项目,可能一些说明不准确或解决方式不完善的地方,能力有限,只能留有遗憾了。
一、项目引入 Activiti 依赖
-
在 pom 文件中添加 Activiti 相关依赖:
<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 配置文件,添加内容如下:
spring: ... # 工作流 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“ 属性,所以没加。结果很快就明白为什么要加了,项目启动出现如下错误(错误信息还给出了解决提醒,我真搞笑):
Description: 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:在上述步骤之后,重新运行项目,然后又出现了如下错误:
16: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
// 原来的 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 里,登录验证的实现如下:
public 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 中加入对应逻辑:
- 原代码:
@Service public class UserDetailsServiceImpl implements UserDetailsService { private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class); @Autowired private ISysUserService userService; @Autowired private SysPermissionService permissionService; @Override 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)); } }
- 修改后的代码:
// 有变更的方法,在原来的用户信息类中加入了需要的权限信息 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
- 原代码:
public 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 方法 @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } }
- 修改后的代码:
public class LoginUser implements UserDetails { // 去掉不变的代码 // +++++ private List<SimpleGrantedAuthority> authorities; // +++++ public void setAuthorities(List<SimpleGrantedAuthority> authorities) { this.authorities = authorities; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; // 变更 } }
- 主要是实体类中,增加了权限属性,且修改了对应的get方法。
- 现在,Activiti 就可以识别到登录用户,且有了新 Api 对应的权限。
- 总结一下,对于 Ruoyi 框架原来的代码实现而言,修改了两处,LoginUser 类和 UserDetailsServiceImpl 类,当然,依赖方法增加了相应的查询岗位(postCode)的方法。
- 整合工作就是这样,实在是不清不楚,马马虎虎。鉴于现在的时间和精力,只能紧着项目的实际问题来,希望以后有机会系统地学习一下 Activiti 吧。
五、 工作流简单测试
- 准备工作流(bpmn)文件,因为之后项目要接入 web 流程设计器,所以没在 IDE 上安装设计器插件,在旧项目里随便找了个bpmn文件测试用;
- 编写单元测试,先进行工作流部署测试
- 编写单元测试代码:
@SpringBootTest() public class ActivitiTest { @Autowired private RepositoryService repositoryService; @Test public void deployTest() { Deployment deployment = repositoryService.createDeployment() .addClasspathResource("leave.bpmn") .name("测试流程") .deploy(); System.out.println("部署ID:" + deployment.getId()); } }
- 运行后,出现缺失表字段错误:
### 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 之前有网友提过,自己忘记这茬了,现在补坑吧:
-- 修复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;
- 再次运行,测试顺利通过,打印部署成功日志:
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: 请假流程-普通表单 }
- 进行创建流程实例测试
- 编写测试代码:
@Test public void startProcessTest() { ProcessInstance processInstance = processRuntime.start(ProcessPayloadBuilder .start() .withProcessDefinitionKey("test") .withName("请假测试") .build()); System.out.println("实例ID:" + processInstance.getId()); }
- 运行,如果出现以下错误:
org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
- 是因为没有整合SpringSecurity ,上面整合内容里描述了新提供 Apl,比如 ProcessRuntime 必须有身份验证,看源码可知:
@PreAuthorize("hasRole('ACTIVITI_USER')") public class ProcessRuntimeImpl implements ProcessRuntime {}
- 两种解决思路:一种是整合 SpringSecurity;一种是试一下旧版 Api。那么试下旧版 Api 吧,重新修改代码:
@Test 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只在流程实例执行过程中保存这些数据,在流程结束时就会删除这些记录。
后记
很基础的一些东西,算是项目使用工作流的第一步吧。接下来,就是在前端项目中接入流程设计器了,然后是实际使用中一些处理和技巧,一步步来吧。