工作流引擎 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 增加配置:
当自定义处理器内部逻辑对处理顺序有要求时需要考虑:
配置到 preBpmnParseHandlers 的 BpmnParseHandler 实例会添加在默认处理器的前面
配置到 postBpmnParseHandlers 的 BpmnParseHandler 实例会添加在默认处理器的后面
接口- org.activiti.engine.parse.BpmnParseHandler:
在 BpmnParseHandler 接口中:
getHandledTypes()方法会翻译这个解析器处理的所有类型的集合,这些都是 BaseElement 的子类,返回集合的泛型限制也说明了这一点
也可以继承 AbstractBpmnParseHandler 类并重写**getHandledType()**方法,这样就只需要返回一个类型,而不是一个集合
这个类也包含需要默认解析处理器所需要的方法
BpmnParseHandler 实例只有在解析器访问到这个方法返回的类型时才会被调用
示例:
当 BPMN 2.0 xml 包含 process 元素时,就会执行 executeParse 方法中的逻辑
这是一个已经完成类型转换的方法,替换 BpmnParseHandler 接口中的 parse 方法
注意:
在编写自定义解析处理器时,不要使用任何解析 BPMN 2.0 结构的内部类,这会导致出现问题很难定义
应该实现 BpmnParseHandler 接口或集成内部抽象类 org.activiti.engine.impl.bpmn.parser.handler.AbstractBpmnParseHandler
也可以替换默认的 BpmnParseHandler 实例,把解析 BPMN 2.0 元素替换为解析 Activiti 内部模型:
示例: 将所有任务强制设置为异步
支持高并发的 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:
使用 UUID 生成器需要添加依赖:
多租户
多租户:
通常是在软件需要作为多个不同组织服务时产生的概念
关键是数据分片,组织不能看到其余组织的数据
在这种场景下,组织,部门,小组就叫做租户
多租户和安装多个实例是从基本上不同的:
多租户是一个 Activiti 流程引擎实例为每个组织分别运行,对应不同的数据表
安装多个 Activiti 流程引擎实例时,虽然 Activiti 是轻量级的,运行流程引擎不会消耗很多资源,但是增加了复杂性,并需要更多维护工作.然而对于一些场景,也是正确的解决方案
Activiti 的多租户主要围绕着数据分片来实现:
Activiti 没有强行校验多租户的规则,即 Activiti 不会校验查询和使用数据时用户是否使用了正确的租户
校验由 Activiti 引擎的调用者层负责完成
Activiti 只确认租户信息会被保存,并在查询流程数据时会被用到
在向 Activiti 流程引擎发布流程定义时,需要传递一个租户标识.是一个字符串,限制在 256 字符内,作为租户的唯一标识
通过部署传递租户 Id 有以下作用:
所有包含在部署中的流程定义都会继承部署的 tenantId
所有从这些流程定义发起的流程实例,都会继承流程定义的 tenantId
所有流程实例运行阶段创建的任务都会继承流程实例的 tenantId.单独运行的 task 也可以包含 tenantId
所有流程实例运行阶段创建的分支都会继承流程实例的 tenantId
在流程本身或通过 API 触发一个信号抛出事件可以通过 tenantId 实现.信号只会在租户环境下执行:如果有多个信号捕获事件,并且名字相同,实际只有正确的 tenantId 下的事件会被调用
所有作业(定时器,异步调用)会集成 tenantId,或者来自流程定义(比如定时开始事件),或流程实例(运行期创建的作业,比如异步调用).这样其实潜在的可以支持为一些租户指定不同优先级的自定义 jobExecutor
所有历史实体(历史流程实例,任务和节点)会从对应的运行状态集成 tenantId
作为单独的一部分,model 也可以设置 tenantId.这里的 model 用来存储 Activiti modeler 设计的 bpmn 2.0 模型
为了确保流程数据使用 tenantId,所有的查询 API 都可以通过 tenantId 进行查询,可以使用其他的实体的对应查询实现替换:
查询 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 实现:
Mapper 需要设置到流程引擎配置中:
这个 Mapper 是一个接口:
MyBatis 框架会在运行阶段为这个接口创建一个实例
返回值是没有类型的,是一个 map 的 list,和对应的行列对应
如果需要也可以使用 MyBatis 映射
执行上面的查询:
可以使用 managementService.executeCustomSql 方法
这个方法需要一个 CustomSqlExecution 实体
这个实体类是一个封装类,隐藏了引擎的内部实现所需执行的信息
但是由于 Java 泛型,查询返回的结果可读性差
示例:
mapper 类和返回类型类
简单调用 mapper 方法 并返回结果
list 中的 Map 只包含 id,name 和 create time, 不是全部的任务对象
可以通过这样的方式执行任意 SQL:
使用这种方法,任务表会与变量表关联.只会获得对应名称的变量,任务 Id 和对应的数值会被返回
使用 ProcessEngineConfigurator 实现流程引擎配置
可以使用 ProcessEngineConfigurator 实现一种高级的扩展流程引擎的配置:
创建一个 org.activiti.engine.cfg.ProcessEngineConfigurator 接口的实现
注入到流程引擎配置里
实现 ProcessEngineConfigurator 接口需要实现两个方法:
configure: 将 ProcessEngineConfiguration 作为参数,可以通过这种方法添加自定义配置,这个方法在流程创建之前,在所有默认配置执行之前保证调用到
getPriority: 如果一些 configurator 存在依赖项的时候,允许对 configurator 进行排序
configurator 实例:
LDAP 集成:
这个 configurator 用来替换默认的 user 和 group 管理器类,使用处理 LDAP 用户存储的类
基本上一个 configurator 允许很大程度上修改或增强流程引擎,对高级的场景非常有用
使用自定义的版本替换流程定义缓存, 如下:
流程引擎配置器也可以通过 ServiceLoader 自动从 classpath 中加载:
放在 jar 中的 configurator 实现必须放在 classpath 下
并在 jar 的 META-INF/services 目录下包含一个 org.activiti.engine.cfg.ProcessEngineConfigurator 文件
文件的内容是自定义实现的全类名
当流程引擎启动时,日志会显示找到了哪些 configurator
这种 ServiceLoader 的方式在某些环境下可能无法正常运行.使用 ProcessEngineConfiguration 的 enableConfiguratorServiceLoader 属性来禁用这个功能,这个属性的默认值为 true
启动安全 BPMN 2.0xml
大多数情况下,BPMN 2.0 流程发布到 Activiti 引擎是在严格的控制下的
然而在某些情况下,可能需要把比较随意的 BPMN 2.0 xml 上传到引擎,这时就要要考虑恶意用户会攻击服务器
为了避免 BPMN 2.0xml 引擎服务器受到攻击,可以在引擎中设置 enableSafeBpmnXml:
默认这个功能没有开启.因为这个功能需要使用 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. 如果没有使用事件日志,可以删除这个表
启用数据库日志:
或者在流程引擎运行阶段:
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 传统历史管理器的附加品
虽然所有数据都在数据库表中,但是并没有为查询优化,不容易获取
真实的使用场景:
审计跟踪
将事件日志数据放到大数据存储中
版权声明: 本文为 InfoQ 作者【攻城狮Chova】的原创文章。
原文链接:【http://xie.infoq.cn/article/ba4381165eebecad7ddcc2037】。文章转载请联系作者。
评论