写点什么

工作流引擎 Activiti 使用进阶!详细解析工作流框架的高级功能使用示例

发布于: 2021 年 06 月 13 日
工作流引擎Activiti使用进阶!详细解析工作流框架的高级功能使用示例

Activiti 高级功能简介

  • Activit 的高级用例,会超越 BPMN 2.0 流程的范畴,使用 Activiti 高级功能需要有 Activiti 开发的明确目标和足够的 Activiti 开发经验

监听流程解析

  • bpmn 2.0 xml 文件需要被解析为 Activiti 内部模型,然后才能在 Activiti 引擎中运行.解析过程发生在发布流程或在内存中找不到对应流程的时候,这时会从数据库查询对应的 xml

  • 对于每个流程 ,BpmnParser 类都会创建一个新的 BpmnParse 实例.这个实例会作为解析过程中的容器来使用

  • 解析过程:

  • 对于每个 BPMN 2.0 元素,引擎中都会有一个对应的 org.activiti.engine.parse.BpmnParseHandler 实例

  • 解析器会保存一个 BPMN 2.0 元素与 BpmnParseHandler 实例的映射

  • 默认 Activiti 使用 BpmnParseHandler 来处理所有支持的元素

  • 同时也使用 BpmnParseHandler 来提供执行监听器,以支持流程历史

  • 可以向 Activiti 引擎中添加自定义的 org.activiti.engine.parse.BpmnParseHandler 实例

  • 经常使用的用例是把执行监听器添加到对应的环节,来处理一些事件队列.Activiti 在内部就是这样进行历史处理的

  • 要想添加这样的自定义处理器,需要为 Activit 增加配置:


<property name="preBpmnParseHandlers">  <list>    <bean class="org.activiti.parsing.MyFirstBpmnParseHandler" />  </list></property>
<property name="postBpmnParseHandlers"> <list> <bean class="org.activiti.parsing.MySecondBpmnParseHandler" /> <bean class="org.activiti.parsing.MyThirdBpmnParseHandler" /> </list></property>
复制代码


  • 当自定义处理器内部逻辑对处理顺序有要求时需要考虑:

  • 配置到 preBpmnParseHandlers 的 BpmnParseHandler 实例会添加在默认处理器的前面

  • 配置到 postBpmnParseHandlers 的 BpmnParseHandler 实例会添加在默认处理器的后面

  • 接口- org.activiti.engine.parse.BpmnParseHandler:


public interface BpmnParseHandler {
Collection<Class>? extends BaseElement>> getHandledTypes();
void parse(BpmnParse bpmnParse, BaseElement element);
}
复制代码


  • BpmnParseHandler 接口中:

  • getHandledTypes()方法会翻译这个解析器处理的所有类型的集合,这些都是 BaseElement 的子类,返回集合的泛型限制也说明了这一点

  • 也可以继承 AbstractBpmnParseHandler 类并重写**getHandledType()**方法,这样就只需要返回一个类型,而不是一个集合

  • 这个类也包含需要默认解析处理器所需要的方法

  • BpmnParseHandler 实例只有在解析器访问到这个方法返回的类型时才会被调用

  • 示例:

  • BPMN 2.0 xml 包含 process 元素时,就会执行 executeParse 方法中的逻辑

  • 这是一个已经完成类型转换的方法,替换 BpmnParseHandler 接口中的 parse 方法


public class TestBPMNParseHandler extends AbstractBpmnParseHandler<Process> {
protected Class<? extends BaseElement> getHandledType() { return Process.class; }
protected void executeParse(BpmnParse bpmnParse, Process element) { .. }
}
复制代码


  • 注意:

  • 在编写自定义解析处理器时,不要使用任何解析 BPMN 2.0 结构的内部类,这会导致出现问题很难定义

  • 应该实现 BpmnParseHandler 接口或集成内部抽象类 org.activiti.engine.impl.bpmn.parser.handler.AbstractBpmnParseHandler

  • 也可以替换默认的 BpmnParseHandler 实例,把解析 BPMN 2.0 元素替换为解析 Activiti 内部模型:


<property name="customDefaultBpmnParseHandlers">  <list>    ...  </list></property>
复制代码


  • 示例: 将所有任务强制设置为异步


public class CustomUserTaskBpmnParseHandler extends ServiceTaskParseHandler {
protected void executeParse(BpmnParse bpmnParse, ServiceTask serviceTask) {
// Do the regular stuff super.executeParse(bpmnParse, serviceTask);
// Make always async ActivityImpl activity = findActivity(bpmnParse, serviceTask.getId()); activity.setAsync(true); }
}
复制代码

支持高并发的 UUID 的 ID 生成器

  • 在高并发的场景中,默认的 ID 生成器可能因为无法很快的获取新 ID 区域而导致异常

  • 所有流程引擎都有一个 ID 生成器,默认的 ID 生成器会在数据库划取一块 ID 范围,其余引擎不能使用相同范围的 ID

  • 在引擎运行期间,当默认的 ID 生成器发现已经越过 ID 范围时,就会启动一个新事务来获得新范围.在极限的情况下,高负载会导致问题

  • 对于大部分情况,默认 ID 生成已经足够:

  • 默认的 org.activiti.engine.impl.db.DbIdGenerator 有一个 idBlockSize 属性,可以配置获取 ID 范围的大小,这样就可以改变获取 ID 的行为

  • 另一个可以选用的默认 ID 生成器是 org.activiti.engine.impl.persistence.StrongUuidGenerator:

  • 会在本地生成一个唯一的 UUID 作为所有实体的标识

  • 因为生成 UUID 不需要访问数据库,所以在高并发环境下的表现比较好

  • 默认 ID 生成器的性能依赖于运行硬件

  • 将 UUID 生成器配置到 Activiti:


<property name="idGenerator">    <bean class="org.activiti.engine.impl.persistence.StrongUuidGenerator" /></property>
复制代码


  • 使用 UUID 生成器需要添加依赖:


 <dependency>    <groupId>com.fasterxml.uuid</groupId>    <artifactId>java-uuid-generator</artifactId>    <version>3.1.3</version></dependency>
复制代码

多租户

  • 多租户:

  • 通常是在软件需要作为多个不同组织服务时产生的概念

  • 关键是数据分片,组织不能看到其余组织的数据

  • 在这种场景下,组织,部门,小组就叫做租户

  • 多租户和安装多个实例是从基本上不同的:

  • 多租户是一个 Activiti 流程引擎实例为每个组织分别运行,对应不同的数据表

  • 安装多个 Activiti 流程引擎实例时,虽然 Activiti 是轻量级的,运行流程引擎不会消耗很多资源,但是增加了复杂性,并需要更多维护工作.然而对于一些场景,也是正确的解决方案

  • Activiti 的多租户主要围绕着数据分片来实现:

  • Activiti 没有强行校验多租户的规则,即 Activiti 不会校验查询和使用数据时用户是否使用了正确的租户

  • 校验由 Activiti 引擎的调用者层负责完成

  • Activiti 只确认租户信息会被保存,并在查询流程数据时会被用到

  • 在向 Activiti 流程引擎发布流程定义时,需要传递一个租户标识.是一个字符串,限制在 256 字符内,作为租户的唯一标识


 repositoryService.createDeployment()            .addClassPathResource(...)            .tenantId("myTenantId")            .deploy();
复制代码


  • 通过部署传递租户 Id 有以下作用:

  • 所有包含在部署中的流程定义都会继承部署的 tenantId

  • 所有从这些流程定义发起的流程实例,都会继承流程定义的 tenantId

  • 所有流程实例运行阶段创建的任务都会继承流程实例的 tenantId.单独运行的 task 也可以包含 tenantId

  • 所有流程实例运行阶段创建的分支都会继承流程实例的 tenantId

  • 在流程本身或通过 API 触发一个信号抛出事件可以通过 tenantId 实现.信号只会在租户环境下执行:如果有多个信号捕获事件,并且名字相同,实际只有正确的 tenantId 下的事件会被调用

  • 所有作业(定时器,异步调用)会集成 tenantId,或者来自流程定义(比如定时开始事件),或流程实例(运行期创建的作业,比如异步调用).这样其实潜在的可以支持为一些租户指定不同优先级的自定义 jobExecutor

  • 所有历史实体(历史流程实例,任务和节点)会从对应的运行状态集成 tenantId

  • 作为单独的一部分,model 也可以设置 tenantId.这里的 model 用来存储 Activiti modeler 设计的 bpmn 2.0 模型

  • 为了确保流程数据使用 tenantId,所有的查询 API 都可以通过 tenantId 进行查询,可以使用其他的实体的对应查询实现替换:


runtimeService.createProcessInstanceQuery()    .processInstanceTenantId("myTenantId")    .processDefinitionKey("myProcessDefinitionKey")    .variableValueEquals("myVar", "someValue")    .list()
复制代码


查询 API 也允许对 tenantId 使用 like 语法, 也可以过滤未设置 tenantId 的实体


  • 重要注意点:

  • 因为数据库的限制,特别是处理 null 的唯一校验.默认表示未设置租户的 tenantId 的值是空字符串

  • 流程定义 key,流程定义 version,tenantId 的组合应该是唯一的,这个有数据库约束校验这个规则

  • 要注意 tenantId 不应设置为 null,会影响一些数据库 Oracle 的查询,会把空字符串当做 null 处理

  • 这也是为什么 withoutTenantId 查询会检查空字符串或 null.这意味着相同的流程定义,即流程定义 key 相同可以部署到不同的租户下,可以拥有各自的版本.当不使用租户时也不会影响使用

  • 这些限制不会影响 Activiti 在集群环境下运行

  • 可以通过调用 repositoryService changeDeploymentTenantId(String deploymentId, String newTenantId) 修改 tenantId. 会修改之前继承的所有 tenantId. 当需要从非多租户环境向多租户环境下切换时,会非常实用

执行自定义 SQL

  • Activiti API 允许使用高级 API 操作数据库:

  • 在查询数据方面,查询 API 和 Native Query API 是非常强大的

  • 但是对于某些情况,不够轻便

  • 使用完全自定义的 SQL 语句:select, insert, update 和 delete.可以执行在 Activiti 的数据存储之上,但是完全又可以配置在流程引擎中:比如使用事务

  • 为了使用自定义 SQL,Activiti 引擎使用 MyBatis 框架的功能:

  • 因此使用自定义 SQL 的第一件事,要创建 MyBatis 映射类

  • 假设不需要全部的任务数据,只需要其中的一小部分.可以使用 Mapper 实现:


public interface MyTestMapper {
@Select("SELECT ID_ as id, NAME_ as name, CREATE_TIME_ as createTime FROM ACT_RU_TASK") List<Map<String, Object>> selectTasks();
}
复制代码


  • Mapper 需要设置到流程引擎配置中:


...<property name="customMybatisMappers">  <set>    <value>org.activiti.standalone.cfg.MyTestMapper</value>  </set></property>...
复制代码


  • 这个 Mapper 是一个接口:

  • MyBatis 框架会在运行阶段为这个接口创建一个实例

  • 返回值是没有类型的,是一个 map 的 list,和对应的行列对应

  • 如果需要也可以使用 MyBatis 映射

  • 执行上面的查询:

  • 可以使用 managementService.executeCustomSql 方法

  • 这个方法需要一个 CustomSqlExecution 实体

  • 这个实体类是一个封装类,隐藏了引擎的内部实现所需执行的信息

  • 但是由于 Java 泛型,查询返回的结果可读性差

  • 示例:

  • mapper 类和返回类型类

  • 简单调用 mapper 方法 并返回结果


CustomSqlExecution<MyTestMapper, List<Map<String, Object>>> customSqlExecution =          new AbstractCustomSqlExecution<MyTestMapper, List<Map<String, Object>>>(MyTestMapper.class) {
public List<Map<String, Object>> execute(MyTestMapper customMapper) { return customMapper.selectTasks(); }
};
List<Map<String, Object>> results = managementService.executeCustomSql(customSqlExecution);
复制代码


list 中的 Map 只包含 id,name 和 create time, 不是全部的任务对象


  • 可以通过这样的方式执行任意 SQL:


 @Select({        "SELECT task.ID_ as taskId, variable.LONG_ as variableValue FROM ACT_RU_VARIABLE variable",        "inner join ACT_RU_TASK task on variable.TASK_ID_ = task.ID_",        "where variable.NAME_ = #{variableName}"    })    List<Map<String, Object>> selectTaskWithSpecificVariable(String variableName);
复制代码


使用这种方法,任务表会与变量表关联.只会获得对应名称的变量,任务 Id 和对应的数值会被返回

使用 ProcessEngineConfigurator 实现流程引擎配置

  • 可以使用 ProcessEngineConfigurator 实现一种高级的扩展流程引擎的配置:

  • 创建一个 org.activiti.engine.cfg.ProcessEngineConfigurator 接口的实现

  • 注入到流程引擎配置里


<bean id="processEngineConfiguration" class="...SomeProcessEngineConfigurationClass">
...
<property name="configurators"> <list> <bean class="com.mycompany.MyConfigurator"> ... </bean> </list> </property>
...
</bean>
复制代码


  • 实现 ProcessEngineConfigurator 接口需要实现两个方法:

  • configure: 将 ProcessEngineConfiguration 作为参数,可以通过这种方法添加自定义配置,这个方法在流程创建之前,在所有默认配置执行之前保证调用到

  • getPriority: 如果一些 configurator 存在依赖项的时候,允许对 configurator 进行排序

  • configurator 实例:

  • LDAP 集成:

  • 这个 configurator 用来替换默认的 user group 管理器类,使用处理 LDAP 用户存储的类

  • 基本上一个 configurator 允许很大程度上修改或增强流程引擎,对高级的场景非常有用

  • 使用自定义的版本替换流程定义缓存, 如下:


public class ProcessDefinitionCacheConfigurator extends AbstractProcessEngineConfigurator {
public void configure(ProcessEngineConfigurationImpl processEngineConfiguration) { MyCache myCache = new MyCache(); processEngineConfiguration.setProcessDefinitionCache(enterpriseProcessDefinitionCache); }
}
复制代码


  • 流程引擎配置器也可以通过 ServiceLoader 自动从 classpath 中加载:

  • 放在 jar 中的 configurator 实现必须放在 classpath

  • 并在 jar META-INF/services 目录下包含一个 org.activiti.engine.cfg.ProcessEngineConfigurator 文件

  • 文件的内容是自定义实现的全类名

  • 当流程引擎启动时,日志会显示找到了哪些 configurator


INFO  org.activiti.engine.impl.cfg.ProcessEngineConfigurationImpl  - Found 1 auto-discoverable Process Engine ConfiguratorsINFO  org.activiti.engine.impl.cfg.ProcessEngineConfigurationImpl  - Found 1 Process Engine Configurators in total:INFO  org.activiti.engine.impl.cfg.ProcessEngineConfigurationImpl  - class org.activiti.MyCustomConfigurator
复制代码


  • 这种 ServiceLoader 的方式在某些环境下可能无法正常运行.使用 ProcessEngineConfiguration enableConfiguratorServiceLoader 属性来禁用这个功能,这个属性的默认值为 true

启动安全 BPMN 2.0xml

  • 大多数情况下,BPMN 2.0 流程发布到 Activiti 引擎是在严格的控制下的

  • 然而在某些情况下,可能需要把比较随意的 BPMN 2.0 xml 上传到引擎,这时就要要考虑恶意用户会攻击服务器

  • 为了避免 BPMN 2.0xml 引擎服务器受到攻击,可以在引擎中设置 enableSafeBpmnXml:


<property name="enableSafeBpmnXml" value="true"/>
复制代码


  • 默认这个功能没有开启.因为这个功能需要使用 StaxSource

  • 由于 JDK6,JBoss 使用的是旧版的 xml 解析实现,无法使用 StaxSource 类,所以不能启用安全的 BPMN 2.0xml

  • 如果 Activiti 运行的平台支持安全的 BPMN 2.0xml 功能,建议打开

事件日志

  • 在 Activiti 5.16 版本中,添加了事件日志机制:

  • 这种日志机制构建在通用目的下的 Activiti 引擎的事件机制,默认是禁用的

  • 目的是由引擎产生的事件会被捕获,包含所有事件数据的 map 会被创建出来,并提供给 org.activiti.engine.impl.event.logger.EventFlusher, 会把数据刷新到别的地方

  • 默认会使用一个简单地基于数据库的事件处理器或者叫作刷新器,会使用 jackson map 转换为 JSON, 并保存到数据库中的 EventLogEntryEntity 实体

  • 默认会创建数据库日志表 ACT_EVT_LOG. 如果没有使用事件日志,可以删除这个表

  • 启用数据库日志:


processEngineConfiguration.setEnableDatabaseEventLogging(true);
复制代码


或者在流程引擎运行阶段:


databaseEventLogger = new EventLogger(processEngineConfiguration.getClock());runtimeService.addEventListener(databaseEventLogger);
复制代码


  • EventLogger 类可以继承:

  • 在需要使用自定义的数据日志时:

  • createEventFlusher() 方法需要返回一个 org.activiti.engine.impl.event.logger.EventFlusher 接口的实例

  • managementService.getEventLogEntries(startLogNr, size) 可以获取 Actviti 的 EventLogEntryEntity 实例

  • 可以使用大数据的 NoSQL 存储: MongoDb,Elastic Search 等等来存储 JSON。

  • 使用的类是可插拔的: org.activiti.engine.impl.event.logger.EventLogger/EventFlusher 和很多 EventHandler

  • 可以切换成自定义应用场景: 不在数据库中存储 JSON,而是放到队列或大数据存储中

  • 注意:

  • 事件日志机制是 Activiti 传统历史管理器的附加品

  • 虽然所有数据都在数据库表中,但是并没有为查询优化,不容易获取

  • 真实的使用场景:

  • 审计跟踪

  • 将事件日志数据放到大数据存储中

发布于: 2021 年 06 月 13 日阅读数: 19
用户头像

一位攻城狮的自我修养 2021.04.06 加入

分享技术干货,面试题和攻城狮故事。 你的关注支持是我持续进步的最大动力! https://github.com/ChovaVea

评论

发布
暂无评论
工作流引擎Activiti使用进阶!详细解析工作流框架的高级功能使用示例