Springboot接入Activiti7

前言

  最近新项目要用工作流,查了几天资料,主要集中在 Activiti7、Flowable、Camunda 三个,同是 jbpm 框架发展而来,各有优劣。最终选择了Activiti7,原因无他,仅是手头参与的其他项目用这个,方便尽快上手,下个项目应该会试试另外两个。
  本次项目采用 RuoYi-Vue-v3.8 开发,使用的 Springboot 版本为v2.5.8。记录一下接入使用过程及踩坑信息,便于自己以后查看,如果对其他人有一些帮助也算意外之喜吧。以下内容主要针对于本次项目,可能一些说明不准确或解决方式不完善的地方,能力有限,只能留有遗憾了。

一、项目引入 Activiti 依赖

  1. 在 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>
    
  2. 项目使用 Mysql 数据库,已引入驱动,因此这里不再添加。Activiti 默认使用 H2 数据库,如项目未使用数据库,应引入数据库驱动。

二、 添加 Activiti 相关配置

  1. 修改 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. 踩坑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. 踩坑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。
  1. 经过以上修改步骤,再次运行项目。好吧,这次我成功了,项目顺利启动,且数据库增加了25个表。
  2. 留个坑,其实这版 Activiti 自动生成的表字段不全,下面有补充说明。

四、 Activiti7 整合 Spring Security

  1. Activiti7 没有身份管理的表,其能力依赖和Spring Security整合,新 Api 包括 TaskRuntime 和 ProcessRuntime 都会强制使用 Security 验证用户权限。
  2. 查看 ProcessRuntime 类的源码可发现,需要“ACTIVITI_USER”角色权限。因此,处理思路是,在登录验证时,给登录用户增加对应权限。
  3. 在 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);
     }
    
  4. 根据上述代码中,需要在 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);
    }
    
  1. 修改相应的用户信息类 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方法。
  1. 现在,Activiti 就可以识别到登录用户,且有了新 Api 对应的权限。
  2. 总结一下,对于 Ruoyi 框架原来的代码实现而言,修改了两处,LoginUser 类和 UserDetailsServiceImpl 类,当然,依赖方法增加了相应的查询岗位(postCode)的方法。
  3. 整合工作就是这样,实在是不清不楚,马马虎虎。鉴于现在的时间和精力,只能紧着项目的实际问题来,希望以后有机会系统地学习一下 Activiti 吧。

五、 工作流简单测试

  1. 准备工作流(bpmn)文件,因为之后项目要接入 web 流程设计器,所以没在 IDE 上安装设计器插件,在旧项目里随便找了个bpmn文件测试用;
  2. 编写单元测试,先进行工作流部署测试
  • 编写单元测试代码:
    @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: 请假流程-普通表单 }
    
  1. 进行创建流程实例测试
  • 编写测试代码:
    @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());
    }
    
  • 再次运行,顺利通过测试。
  1. 单元测试就算完了,证明 Activiti 顺利接入到了项目中,等前端流程设计器接入了,才能真正方便地用起来吧。

六、 数据表命名规则说明

  1. 简单记录一下 Activiti 表的命名规则,留个印象吧,想知道具体信息还是得对应到每个表;
  2. 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只在流程实例执行过程中保存这些数据,在流程结束时就会删除这些记录。

后记

  很基础的一些东西,算是项目使用工作流的第一步吧。接下来,就是在前端项目中接入流程设计器了,然后是实际使用中一些处理和技巧,一步步来吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值