工作流之前一直有在了解,也做过预研,算是参加工作后第一个预研的项目吧,以前的工作流对数据和流程控制的耦合太严重,对于系统之间的集成较困难,所以当时预研的一个结论就是要把数据和流程控制分开,也做了些DEMO,最后进去另外项目组了,此事就没有进行下去了。
现在工作中需要使用工作流,选型用Activiti,可能是Actitivi比较简单,在国内受欢迎程度较高吧,用的人比较多。但是文档相对较少,而且貌似大家都在使用较老的版本,目前Activiti 7.0.0是已经有的,Activiti 6.0.0在2017年发布,应该出来时间也算比较久了,但是在一些技术交流群里面大家还是在用5.22.0版本。 目前网上对于Activiti 5.x 的DEMO 还是比较多的,如使用自定义数据源,整合自有业务用户角色关系,整合建模工具等等。Activiti 6.0.0 相对于 5.x包结构和类都做了一些优化,整合一起遇到了一些困难,记录一下分享到给各位伙伴。
此处 有Activiti 5.x 迁移到 6.0.0的主要变化,遇到问题可以参考。
目录 (貌似导航无用哦 应该是简书的问题)
Spring boot 1.5.6 + Activiti 6.0.0
先给出项目中的最后的POM文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>workflow</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<properties>
<activiti.version>6.0.0</activiti.version>
<activiti-exp.version>5.22.0</activiti-exp.version>
</properties>
<dependencies>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter-basic</artifactId>
<version>${activiti.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.197</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.driver.version}</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter-rest-api</artifactId>
<version>6.0.0</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter-actuator</artifactId>
<version>${activiti.version}</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter-jpa</artifactId>
<version>${activiti.version}</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-explorer</artifactId>
<version>${activiti-exp.version}</version>
</dependency>
<!--<dependency> 这个包看起来没什么用,一旦加入的话由于他依赖于5.22的activiti-engine包,所以不能兼容6.0.0
<groupId>org.activiti</groupId>
<artifactId>activiti-diagram-rest</artifactId>
<version>${activiti-exp.version}</version>
</dependency>-->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-simple-workflow</artifactId>
<version>${activiti-exp.version}</version>
</dependency>
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-codec</artifactId>
<version>1.7</version>
</dependency>
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-css</artifactId>
<version>1.7</version>
</dependency>
<!-- Testing -->
<!-- https://mvnrepository.com/artifact/org.springframework/spring-test -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.3.18.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.subethamail</groupId>
<artifactId>subethasmtp-wiser</artifactId>
<version>1.2</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
配置Activiti数据源
一般会有两个数据源,业务数据源+Activiti(自带)数据库, 业务数据源的配置这里不提,大家应该个有自己的配置方式,简单的直接使用spring boot默认的属性配置方式即可,这里重点列出配置Activiti自带数据源(自定义节点spring.activiti.datasource来存储activiti数据源配置):
spring:
activiti:
async-executor-activate: true
mail-server-use-ssl: true
#自动更新数据库
database-schema-update: true
#校验流程文件,默认校验resources下的processes文件夹里的流程文件
#check-process-definitions: false
#restApiEnabled: false
datasource:
driverClassName: com.mysql.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:mysql://127.0.0.1:3306/activiti6ui?useUnicode=true&characterEncoding=UTF-8
username: root
password: 123456
hikari:
maximum-pool-size: 30
idle-timeout: 30000
connection-test-query: select 1 from DUAL
auto-commit: true
minimum-idle: 5
connection-timeout: 30000
pool-name: activiti-datasource-pool
这里使用的属性是spring.activiti.datasource 属于自定义属性,所以需要如下配置:
@Configuration
@EnableTransactionManagement//开启事物管理
@EnableJpaRepositories(//自定义数据管理的配置
entityManagerFactoryRef = "activitiEntityManagerFactory", //指定EntityManager的创建工厂Bean
transactionManagerRef = "activitiTransactionManager",//指定事物管理的Bean
basePackages = {"com.fintech.insurance.micro.workflow.persist.entity"})//指定管理的实体位置
public class ActivitiConfiguration extends AbstractProcessEngineAutoConfiguration {
@Bean("activitiDataSourceProperties")
@ConfigurationProperties(prefix = "spring.activiti.datasource")
public DataSourceProperties activitiDataSourceProperties() {
return new DataSourceProperties();
}
@Bean("activitiDataSource")
public HikariDataSource activitiDataSource() {
DataSourceProperties activitiDataSourceProperties = activitiDataSourceProperties();
return (HikariDataSource)activitiDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
@Primary
@Bean("insuranceActivitiConfig")
public SpringProcessEngineConfiguration insuranceActivitiConfig(PlatformTransactionManager transactionManager,
SpringAsyncExecutor springAsyncExecutor) throws IOException {
SpringProcessEngineConfiguration springProcessEngineConfiguration = baseSpringProcessEngineConfiguration(
activitiDataSource(),
transactionManager,
springAsyncExecutor);
return springProcessEngineConfiguration;
}
}
这里使用spring boot的配置重载了SpringProcessEngineConfiguration, 使得springProcessEngineConfiguration能读取activiti自有数据源。
配置自定义用户-组/角色管理
Activiti有自己的用户、角色管理功能以及数据库表,用于给工作流中的工作项分配、指派任务,Activiti中与此相关的表主要有:
身份相关的表:
act_id_user:用户表
act_id_info:用户信息表
act_id_group:组(或角色)
act_id_memership:用户与组的关系 ,查询任务获选人需关联的表
自己的业务系统一般也有自己的用户-角色(组)管理模块,可参考链接 ,上面简要说了几种解决方案,可以根据自己的现实情况进行决策。我这边是重新自己实现了UserEntityManager, GroupEntityManager接口,把自己业务中的用户-角色(组管理)注入到Activiti引擎之中,这里只重写一两个比较重要的方法,大部分方法不需要实现,基本能满足需求。这部分跟Activiti 5.x的整合不一样,网上至今未看到例子。
- 需要自定义ActivitiUser类,实现org.activiti.engine.impl.persistence.entity.UserEntity接口
- 需要自定义ActivitiRole类,实现org.activiti.engine.impl.persistence.entity.GroupEntity接口
- 下面代码中ActivitiUserGroupFeign是自有业务系统的用户角色(组)服务
import org.activiti.engine.identity.Group;
import org.activiti.engine.identity.Picture;
import org.activiti.engine.identity.User;
import org.activiti.engine.identity.UserQuery;
import org.activiti.engine.impl.Page;
import org.activiti.engine.impl.UserQueryImpl;
import org.activiti.engine.impl.interceptor.Session;
import org.activiti.engine.impl.persistence.entity.UserEntity;
import org.activiti.engine.impl.persistence.entity.UserEntityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @Description: (some words)
* @Author: Yong Li
* @Date: 2018/8/8 16:52
*/
@Component
public class CustomUserEntityManager implements UserEntityManager, Session {
@Autowired
private ActivitiUserGroupFeign activitiUserGroupFeign;
// session interface
@Override
public void flush() {
// do nothing
}
// session interface
@Override
public void close() {
// do nothing
}
@Override
public User createNewUser(String userId) {
return new ActivitiUser();
}
@Override
public void updateUser(User updatedUser) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public List<User> findUserByQueryCriteria(UserQueryImpl query, Page page) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public long findUserCountByQueryCriteria(UserQueryImpl query) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public List<Group> findGroupsByUser(String userId) {
List<ActivitiRole> roles = activitiUserGroupFeign.listRolesByUserMobile(userId).getDataNoPatience();
List<Group> result = new ArrayList<>();
if (null != roles) {
for (ActivitiRole r : roles) {
result.add(r);
}
}
return result;
}
@Override
public UserQuery createNewUserQuery() {
return new UserQueryImpl();
}
@Override
public Boolean checkPassword(String userId, String password) {
ActivitiUser user = activitiUserGroupFeign.findUserByMobile(userId).getDataNoPatience();
return null != user && user.getPassword().equals(password);
}
@Override
public List<User> findUsersByNativeQuery(Map<String, Object> parameterMap, int firstResult, int maxResults) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public long findUserCountByNativeQuery(Map<String, Object> parameterMap) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public boolean isNewUser(User user) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public Picture getUserPicture(String userId) {
ActivitiUser user = activitiUserGroupFeign.findUserByMobile(userId).getDataNoPatience();
return user.getPicture();
}
@Override
public void setUserPicture(String userId, Picture picture) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public void deletePicture(User user) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public UserEntity create() {
return new ActivitiUser();
}
@Override
public UserEntity findById(String entityId) {
ActivitiUser user = activitiUserGroupFeign.findUserByMobile(entityId).getDataNoPatience();
return user;
}
@Override
public void insert(UserEntity entity) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public void insert(UserEntity entity, boolean fireCreateEvent) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public UserEntity update(UserEntity entity) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public UserEntity update(UserEntity entity, boolean fireUpdateEvent) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public void delete(String id) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public void delete(UserEntity entity) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public void delete(UserEntity entity, boolean fireDeleteEvent) {
throw BaseExceptionFactory.buildBaseException(201001);
}
}
import org.activiti.engine.identity.*;
import org.activiti.engine.impl.GroupQueryImpl;
import org.activiti.engine.impl.Page;
import org.activiti.engine.impl.interceptor.Session;
import org.activiti.engine.impl.persistence.entity.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @Description: (some words)
* @Author: Yong Li
* @Date: 2018/8/8 17:53
*/
@Component
public class CustomGroupEntityManager implements GroupEntityManager, Session {
@Autowired
private ActivitiUserGroupFeign activitiUserGroupFeign;
// session interface
@Override
public void flush() {
// do nothing
}
// session interface
@Override
public void close() {
// do nothing
}
@Override
public Group createNewGroup(String groupId) {
return new ActivitiRole();
}
@Override
public GroupQuery createNewGroupQuery() {
return new GroupQueryImpl();
}
@Override
public List<Group> findGroupByQueryCriteria(GroupQueryImpl query, Page page) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public long findGroupCountByQueryCriteria(GroupQueryImpl query) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public List<Group> findGroupsByUser(String userId) {
List<ActivitiRole> roles = activitiUserGroupFeign.listRolesByUserMobile(userId).getDataNoPatience();
List<Group> result = new ArrayList<>();
if (null != roles) {
for (ActivitiRole r : roles) {
result.add(r);
}
}
return result;
}
@Override
public List<Group> findGroupsByNativeQuery(Map<String, Object> parameterMap, int firstResult, int maxResults) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public long findGroupCountByNativeQuery(Map<String, Object> parameterMap) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public boolean isNewGroup(Group group) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public GroupEntity create() {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public GroupEntity findById(String entityId) {
return activitiUserGroupFeign.findRoleByCode(entityId).getDataNoPatience();
}
@Override
public void insert(GroupEntity entity) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public void insert(GroupEntity entity, boolean fireCreateEvent) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public GroupEntity update(GroupEntity entity) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public GroupEntity update(GroupEntity entity, boolean fireUpdateEvent) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public void delete(String id) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public void delete(GroupEntity entity) {
throw BaseExceptionFactory.buildBaseException(201001);
}
@Override
public void delete(GroupEntity entity, boolean fireDeleteEvent) {
throw BaseExceptionFactory.buildBaseException(201001);
}
}
再分别实现CustomUserEntityManagerFactory, CustomGroupEntityManagerFactory
@Service
public class CustomUserEntityManagerFactory implements SessionFactory {
// 使用自定义的User管理类
@Autowired
private CustomUserEntityManager customUserEntityManager;
@Autowired
public Class<?> getSessionType() {
//注意此处也必须为Activiti原生类
return UserEntityManager.class;
}
@Override
public Session openSession(CommandContext commandContext) {
return customUserEntityManager;
}
}
@Service
public class CustomGroupEntityManagerFactory implements SessionFactory {
@Autowired
private CustomGroupEntityManager customGroupEntityManager;
@Override
public Class<?> getSessionType() {
//注意此处必须为Activiti原生的类,否则自定义类不会生效
return GroupEntityManager.class;
}
@Override
public Session openSession(CommandContext commandContext) {
return customGroupEntityManager;
}
}
最后把自定义的用户-组管理注入到Activiti配置中:
@Primary
@Bean("insuranceActivitiConfig")
public SpringProcessEngineConfiguration insuranceActivitiConfig(PlatformTransactionManager transactionManager,
SpringAsyncExecutor springAsyncExecutor) throws IOException {
SpringProcessEngineConfiguration springProcessEngineConfiguration = baseSpringProcessEngineConfiguration(
activitiDataSource(),
transactionManager,
springAsyncExecutor);
// 配置自定义的用户和组管理
springProcessEngineConfiguration.setUserEntityManager(customUserEntityManager);
springProcessEngineConfiguration.setGroupEntityManager(customGroupEntityManager);
List<SessionFactory> customSessionFactories = new ArrayList<>();
customSessionFactories.add(customUserEntityManagerFactory);
customSessionFactories.add(customGroupEntityManagerFactory);
springProcessEngineConfiguration.setCustomSessionFactories(customSessionFactories);
return springProcessEngineConfiguration;
}
自定义用户-角色(组)配置完成。
流程测试用例编写
流程设计完之后,需要对流程的过程进行测试,特别对于刚入门的新人,不熟悉BPMN每个元素的作用,需要通过实验或测试观察结果是否与期望的结果一致。如果直接使用spring boot环境,或spring cloud,在多个微服务集成的环境中进行测试,前期特别耗费时间,所以建议可以用如下的测试方法: 数据源使用h2内存数据源, 在test/resources下面准备文件activiti.cfg.xml如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="processEngineConfiguration" class="org.activiti.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">
<property name="jdbcUrl" value="jdbc:h2:mem:activiti;DB_CLOSE_DELAY=1000" />
<property name="jdbcDriver" value="org.h2.Driver" />
<property name="jdbcUsername" value="sa" />
<property name="jdbcPassword" value="" />
<!-- Database configurations -->
<property name="databaseSchemaUpdate" value="drop-create" />
<!-- job executor configurations 如果有定时任务,比如 Time Boundoury Activiti, 需要开启下面配置-->
<property name="asyncExecutorActivate" value="true" />
<!-- mail server configurations -->
<property name="mailServerPort" value="5025" />
</bean>
</beans>
测试用例例子
public class RequisitionFlowTest extends PluggableActivitiTestCase {
protected static final long REQUISITION_CANCEL_TIMEOUT_SECONDES = 4;
protected static final long REQUISITION_PAYMENT_TIMEOUT_SECONDES = 4;
protected Map<String, Object> processVars;
@Override
protected void setUp() throws Exception {
super.setUp();
processVars = new HashMap<>();
processVars.put("requisitionFlowService", new RequisitionFlowService());
processVars.put("requisitionNumber", "test_requisition");
String cancelTime = DateCommonUtils.formatDateISO8601(DateCommonUtils.getAfterSeconds(new Date(), (int)REQUISITION_CANCEL_TIMEOUT_SECONDES));
System.out.println("=====cancel time expression:" + cancelTime);
processVars.put("confirmTimeoutTime", cancelTime);
}
@Override
public void tearDown() throws Exception {
super.tearDown();
}
@Test
@Deployment(resources = {"processes/Insurance_Requisition.bpmn20.xml"})
public void testCustomerTimeoutForConfirm() throws InterruptedException {
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("requisitionV1", processVars);
// 检查流程是否未结束
ProcessInstance qpi = runtimeService.createProcessInstanceQuery().processInstanceId(processInstance.getId()).singleResult();
Assert.assertNotNull(qpi);
Task task1 = taskService.createTaskQuery().processDefinitionKey(processInstance.getProcessDefinitionKey()).taskDefinitionKey("customerConfirmTask").singleResult();
Assert.assertNotNull(task1);
// 等待用户任务完成
Thread.sleep((REQUISITION_CANCEL_TIMEOUT_SECONDES + 12) * 1000); //定时任务貌似每隔10秒重新扫一次
// 检查历史流程实例是否已经结束
Assert.assertNull(runtimeService.createProcessInstanceQuery().processInstanceId(processInstance.getId()).singleResult());
}
protected String generatePayDueDateVariable() {
return DateCommonUtils.formatDateISO8601(DateCommonUtils.getAfterSeconds(new Date(), (int)REQUISITION_PAYMENT_TIMEOUT_SECONDES));
}
}
使用这种测试用例进行测试时,基本可以完成所有与用户无关的活动连接测试,比如从节点A到节点F的逻辑,对于初步的练习和测试来说是非常高效的。
整合Activiti-Explorer
Activiti 建模工具提供有两种: 一种是eclipse plugins, 一种的自带的web 版的,可以从官方网站上下载,在activiti-6.0.0 下面wars/activiti-app.war。我比较喜欢后者,如果能在自己的业务系统中提供一个如下的工作流流程设计器给运营或者产品经理,是一个不错的选择。
遗憾的是activiti 6.0.0并未提供流程设计器依赖的jar包(也许是我没有找到),所以我这还是Activiti engine 6.0.0 + Activiti Explorer 5.22.0。
我这里参考github 项目 , 只需要把pom.xml文件依赖文件换成最开始提到的pom就好了。
最终,就能在自己的项目里面嵌入了流程设计器,并且能在上面编辑/创建流程, 并对流程进行在线发布。
由于版本的不匹配,是否会对流程设计器最终设计的图在engine6.0上有什么样的问题,目前还未知。
网友评论
它适用于一些我们通常意义上的流程,如请假流程,订外卖流程,审批流程等等。