高 级主题
以下章节介绍 Flowable 的高级用例,超出 BPMN 2.0 流程的典型执行范围。因此,理解这里描述的主题需要具备一定的 Flowable 熟练程度和经验。
异步执行器
异步执行器设计
作业(job)有两种类型:定时器(例如属于用户任务上边界事件的那些)和异步继续(async continuation,属于带有 flowable:async="true" 属性的服务任务)。
定时器最容易解释:它们以某个到期时间持久化在 ACT_RU_TIMER_JOB 表中。异步执行器中有一个线程会定期检查是否有新的定时器应该触发(也就是说,到期时间'早于'当前时间)。当发生这种情况时,该定时器被移除,并创建一个异步作业。
一个异步作业是在执行流程实例步骤的过程中(也就是在某次 API 调用执行期间)插入数据库的。如果当前 Flowable 引擎的异步执行器处于活动状态,该异步作业实际上已经被锁定。这意味着作业条目被插入 ACT_RU_JOB 表,并设置 了锁持有者和锁过期时间。一个在 API 调用成功提交时触发的事务监听器,会触发同一引擎的异步执行器来执行该作业(从而保证数据已经在数据库中)。为此,异步执行器有一个可配置的线程池,从中取出一个线程来执行该作业并异步继续流程。如果 Flowable 引擎没有启用异步执行器,该异步作业会在未锁定的状态下插入 ACT_RU_JOB 表。
与检查新定时器的线程类似,异步执行器有一个'获取'新异步作业的线程。这些是表中存在的、未被锁定的作业。该线程会为当前 Flowable 引擎锁定这些作业,并把它交给异步执行器。
执行作业的线程池使用一个内存队列来取用作业。当队列已满(这是可配置的)时,该作业会被解锁并重新插入其表中。这样,其他异步执行器就可以拾取它。
如果作业执行期间发生异常,该异步作业会被转换为一个带有到期时间的定时器作业。之后,它会像常规定时器作业一样被拾取,并再次成为异步作业,很快被重试。当一个作业被重试了(可配置的)次数后仍然失败,该作业被认为'死亡',并被移到 ACT_RU_DEADLETTER_JOB。'死信'(deadletter)概念在许多其他系统中被广泛使用。管理员现在需要检查失败作业的异常,并决定最佳的处理方式。
流程定义和流程实例可以被挂起。与这些定义或实例相关的挂起作业被放入 ACT_RU_SUSPENDED_JOB 表,以确保获取作业的查询在其 where 子句中条件尽可能少。
从上面可以清楚看到一点:对 于熟悉旧版作业/异步执行器实现的人,主要目标是让'获取查询'尽可能简单。过去(V6 之前),所有作业类型/状态都使用一张表,这使得 'where' 条件很大,因为它要兼顾所有用例。这个问题现在已经解决,我们的基准测试证明,新设计带来了更好的性能和更高的可扩展性。
异步执行器配置
异步执行器是一个高度可配置的组件。始终建议查看异步执行器的默认设置,并验证它们是否符合你的流程需求。
另外,也可以扩展默认实现,或用你自己的实现替换 org.flowable.engine.impl.asyncexecutor.AsyncExecutor 接口。
以下属性可以通过 setter 在流程引擎配置上使用:
异步执行器配置选项
| 名称 | 默认值 | 描述 |
|---|---|---|
| asyncExecutorThreadPoolQueueSize | 100 | 作业被获取后、在被线程池中的线程实际执行之前所放置的队列大小 |
| asyncExecutorCorePoolSize | 8 | 线程池中为执行作业而保持存活的最小线程数。 |
| asyncExecutorMaxPoolSize | 8 | 线程池中为执行作业而创建的最大线程数。 |
| asyncExecutorThreadKeepAliveTime | 5000 | 用于执行作业的线程在被销毁之前必须保持存活的时间(毫秒)。设 置 > 0 会占用资源,但在作业执行频繁时可以避免一直创建新线程。如果为 0,线程在用于执行作业后即被销毁。 |
| asyncExecutorNumberOfRetries | 3 | 作业在被移到'死信'表之前的重试次数。 |
| asyncExecutorMaxTimerJobsPerAcquisition | 1 | 一次查询中获取的定时器作业数量。默认值为 1,因为这降低了乐观锁异常的可能性。较大的值可能性能更好,但不同引擎之间发生乐观锁异常的概率也会变大。 |
| asyncExecutorMaxAsyncJobsDuePerAcquisition | 1 | 一次查询中获取的异步作业数量。默认值为 1,因为这降低了乐观锁异常的可能性。较大的值可能性能更好,但不同引擎之间发生乐观锁异常的概率也会变大。 |
| asyncExecutorDefaultTimerJobAcquireWaitTime | 10000 | 定时器获取线程等待执行下一次查询的时间(毫秒)。当未发现新的定时器作业,或获取到的定时器作业少于 asyncExecutorMaxTimerJobsPerAcquisition 所设置的数量时发生。 |
| asyncExecutorDefaultAsyncJobAcquireWaitTime | 10000 | 异步作业获取线程等待执行下一次查询的时间(毫秒)。当未发现新的异步作业,或获取到的异步作业少于 asyncExecutorMaxAsyncJobsDuePerAcquisition 所设置的数量时发生。 |
| asyncExecutorDefaultQueueSizeFullWaitTime | 0 | 当内部作业队列已满时,异步作业(定时器和异步继续)获取线程等待执行下一次查询的时间(毫秒)。默认为 0(为向后兼容)。将该属性设为更高的值,可以让异步执行器有机会清理队列。 |
| asyncExecutorTimerLockTimeInMillis | 5 minutes | 定时器作业被异步执行器获取时锁定的时长(毫秒)。在此期间,其他异步执行器不会尝试获取并锁定该作业。 |
| asyncExecutorAsyncJobLockTimeInMillis | 5 minutes | 异步作业被异步执行器获取时锁定的时长(毫秒)。在此期间,其他异步执行器不会尝试获取并锁定该作业。 |
| asyncExecutorSecondsToWaitOnShutdown | 60 | 当请求关闭执行器(或流程引擎)时,等待优雅关闭用于执行作业的线程池的时间(秒)。 |
| asyncExecutorResetExpiredJobsInterval | 60 seconds | 两次连续检查'过期作业'之间的时间(毫秒)。过期作业是指已被锁定(某个执行器写入了锁持有者和时间,但作业从未完成)的作业。在这样的检查中,过期的作业被重新开放,即锁持有者和锁时间会被移除。其他执行器现在就可以拾取它。如果锁时间早于当前日期,该作业被视为过期。 |
| asyncExecutorResetExpiredJobsPageSize | 3 | 异步执行器的'重置过期'线程一次获取的作业数量。 |
挂接流程解析
BPMN 2.0 XML 需要被解析为 Flowable 内部模型,才能在 Flowable 引擎上执行。该解析发生在流程部署时,或者当内存中找不到流程、而从数据库取回 XML 时。
对于每个这样的流程,BpmnParser 类会创建一个新的 BpmnParse 实例。该实例将作为解析过程中所做一切的容器。解析本身非常简单:对于每个 BPMN 2.0 元素,引擎中都有一个对应的 org.flowable.engine.parse.BpmnParseHandler 实例。因此,解析器有一个 map,基本上把 BPMN 2.0 元素类映射到 BpmnParseHandler 实例。默认情况下,Flowable 有处理所有受支持元素的 BpmnParseHandler 实例,并用它向流程步骤附加执行监听器以创建历史。
可以向 Flowable 引擎添加自定义的 org.flowable.engine.parse.BpmnParseHandler 实例。一个常见用例是向某些步骤添加执行监听器,将事件发送到某个队列进行事件处理。历史处理在 Flowable 内部就是以这种方式完成的。要添加这样的自定义处理器,需要调整 Flowable 配置:
<property name="preBpmnParseHandlers">
<list>
<bean class="org.flowable.parsing.MyFirstBpmnParseHandler" />
</list>
</property>
<property name="postBpmnParseHandlers">
<list>
<bean class="org.flowable.parsing.MySecondBpmnParseHandler" />
<bean class="org.flowable.parsing.MyThirdBpmnParseHandler" />
</list>
</property>
配置在 preBpmnParseHandlers 属性中的 BpmnParseHandler 实例列表,会添加在所有默认处理器之前。同样,postBpmnParseHandlers 添加在它们之后。如果自定义解析处理器中的逻辑对顺序有要求,这一点可能很重要。
org.flowable.engine.parse.BpmnParseHandler 是一个简单接口:
public interface BpmnParseHandler {
Collection<Class>? extends BaseElement>> getHandledTypes();
void parse(BpmnParse bpmnParse, BaseElement element);
}
getHandledTypes() 方法返回该解析器处理的所有类型的集合。可能的类型是 BaseElement 的子类,由集合的泛型类型指定。你也可以扩展 AbstractBpmnParseHandler 类并覆写 getHandledType() 方法,它只返回一个 Class 而不是集合。该类还包含许多默认解析处理器共享的一些辅助方法。当解析器遇到该方法返回的任一类型时,就会调用该 BpmnParseHandler 实例。在下面的例子中,每当遇到某个 BPMN 2.0 XML 中包含的流程时,就会执行 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 构件的任何内部类。这会导致难以查找的 bug。实现自定义处理器的安全方式是实现 BpmnParseHandler 接口,或扩展内部抽象类 org.flowable.engine.impl.bpmn.parser.handler.AbstractBpmnParseHandler。
可以(但较少见)替换负责将 BPMN 2.0 元素解析为 Flowable 内部模型的默认 BpmnParseHandler 实例。可以通过以下逻辑片段完成:
<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
serviceTask.setAsynchronous(true);
}
}
面向高并发的 UUID ID 生成器
在某些(非常)高并发负载场景下,默认 ID 生成器可能因为无法足够快地获取新的 ID 块而导致异常。每个流程引擎有一个 ID 生成器。默认 ID 生成器在数据库中预留一个 ID 块,使其他引擎无法使用同一 ID 块中的 ID。在引擎运行期间,当默认 ID 生成器发现 ID 块用完时,会启动一个新事务来获取新块。在(非常)有限的用例中,当存在真实高负载时这可能导致问题。对大多数用例,默认 ID 生成器完全够用。默认的 org.flowable.engine.impl.db.DbIdGenerator 还有一个 idBlockSize 属性,可以配置预留 ID 块的大小并调整 ID 获取的行为。
默认 ID 生成器的替代方案是 org.flowable.engine.impl.persistence.StrongUuidGenerator,它使用 UUID 作为实体的 ID。这样就消除了在数据库中预留 ID 块的需求,从而避免了高并发负载下的上述问题。其代价是 ID 较长(36 个字符),并且在某些数据库上(例如在该列上建索引时)可能对性能有影响。
UUID 生成器可以按如下方式在 Flowable 配置中设置:
<property name="idGenerator">
<bean class="org.flowable.engine.impl.persistence.StrongUuidGenerator" />
</property>
使用 UUID ID 生成器有以下额外依赖:
<dependency>
<groupId>com.fasterxml.uuid</groupId>
<artifactId>java-uuid-generator</artifactId>
<version>3.1.3</version>
</dependency>
多租户
多租户总体上是指软件能够服务多个不同组织(或称'租户')、同时保持其数据彼此隔离的概念。
请注意,这与多实例设置有根本不同:多实例是在多台机器上运行多个 Flowable 流程引擎实例,而所有实例共享同一数据库(从而共享同一数据)。
Flowable 中的多租户主要围绕数据分区实现。重要的是要注意,Flowable 中的多租户并不意味着多个数据库/schema——所有数据都在同一个数据库中,只是用租户标识加以区分。
将流程定义部署到 Flowable 流程引擎时,可以传入一个租户标识。这在多租户场景中很有用:多个独立组织使用同一引擎,但各自的流程定义、流程实例等彼此隔离。
repositoryService.createDeployment()
.addClassPathResource(...)
.tenantId("myTenantId")
.deploy();
在部署期间传入租户 ID 有以下影响:
- 部署中包含的所有流程定义都从该部署继承租户标识
- 从这些流程定义启动的所有流程实例都从流程定义继承该租户标识
- 执行流程实例时在运行时创建的所有任务都从流程实例继承该租户标识
- 流程实例执行期间创建的所有执行实例都从流程实例继承该租户标识
- 触发信号抛出事件(在流程内部或通过 API)时,可以同时提供租户标识
- 所有作业(定时器和异步继续)从流程定义或流程实例继承租户标识
- 所有历史实体(历史流程实例、任务和活动)从流程实例继承租户标识
- 顺便一提,模型也可以有租户标识(模型例如被 Flowable modeler 用于存储流程模型)
要真正在流程数据上利用租户标识,所有查询 API 都具备按租户标识过滤的能力,只返回属于特定租户的 数据。
runtimeService.createProcessInstanceQuery()
.processInstanceTenantId("myTenantId")
.processDefinitionKey("myProcessDefinitionKey")
.variableValueEquals("myVar", "someValue")
.list()
查询 API 还允许使用 like 语义按租户标识过滤,也可以过滤掉属于某个租户的实体。
**重要实现细节:**由于数据库的怪癖(更具体地说:唯一约束中的 null 处理),当使用租户标识时,引擎会在内部存储租户标识。未设置租户标识时,内部使用一个特定值(而非 null)以使其行为保持一致。
请注意,以上所有内容与在集群中运行多个 Flowable 实例并不冲突。
[实验性] 可以通过调用 changeDeploymentTenantId(String deploymentId, String newTenantId) 方法来更改部署的租户标识。这会更改该部署中所有流程定义的租户标识。请注意,已经从这些定义启动的流程实例不会被修改。
执行自定义 SQL
Flowable API 允许使用高层 API 与数据库交互。例如,取回当前所有流程定义、查询任务等。然而,有时需要执行一些内置查询无法表达的自定义 SQL。
为了定义自定义 SQL 语句,Flowable 引擎利用其底层框架 MyBatis 的能力。映射语句可以通过两种方式定义:基于注解或基于 XML。
基于注解的映射语句
使用基于注解的映射语句时,第一件事是创建一个 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.flowable.standalone.cfg.MyTestMapper</value>
</set>
</property>
...
请注意这是一个接口。底层 MyBatis 框架会创建它的实例,该实例可以用于执行自定义 SQL。
要执行上面的查询,必须使用 managementService.executeCustomSql 方法。该方法接收一个实现 AbstractCustomSqlExecution 的实例。
遗憾的是,Java 泛型使其可读性略差。下面的两个泛型类型分别是 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);
上面列表中的 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);
使用该方法,任务表将与变量表连接。只返回变量具有某特定值的任务。
关于使用基于注解的映射语句的可运行示例,参见 org.flowable.standalone.cfg 包中的单元测试。
基于 XML 的映射语句
使用基于 XML 的映射语句时,语句定义在 XML 文件中。对于不需要整个自定义查询对象的用例,这很有用。
<mapper namespace="org.flowable.standalone.cfg.TaskMapper">
<resultMap id="customTaskResultMap" type="org.flowable.standalone.cfg.CustomTask">
<id property="id" column="ID_" jdbcType="VARCHAR"/>
<result property="name" column="NAME_" jdbcType="VARCHAR"/>
<result property="createTime" column="CREATE_TIME_" jdbcType="TIMESTAMP" />
</resultMap>
<select id="selectCustomTaskList" resultMap="customTaskResultMap">
select RES.ID_, RES.NAME_, RES.CREATE_TIME_ from ACT_RU_TASK RES
</select>
</mapper>
结果被映射到 org.flowable.standalone.cfg.CustomTask 类的实例,该类可以如下所示:
public class CustomTask {
protected String id;
protected String name;
protected Date createTime;
public String getId() {
return id;
}
public String getName() {
return name;
}
public Date getCreateTime() {
return createTime;
}
}
Mapper XML 文件必须按如下方式提供给流程引擎配置:
...
<property name="customMybatisXMLMappers">
<set>
<value>org/flowable/standalone/cfg/custom-mappers/CustomTaskMapper.xml</value>
</set>
</property>
...
该语句可以按如下方式执行:
List<CustomTask> tasks = managementService.executeCommand(new Command<List<CustomTask>>() {
@SuppressWarnings("unchecked")
@Override
public List<CustomTask> execute(CommandContext commandContext) {
return (List<CustomTask>) CommandContextUtil.getDbSqlSession().selectList("selectCustomTaskList");
}
});
对于需要更复杂语句的用例,XML 映射语句会有帮助。由于 Flowable 使用 MyBatis,MyBatis 的所有能力都可以使用。
假设某个用例需要能够基于 id、name、type、userId 等查询附件数据。可以通过扩展 AbstractQuery 类来实现。
public class AttachmentQuery extends AbstractQuery<AttachmentQuery, Attachment> {
protected String attachmentId;
protected String attachmentName;
protected String attachmentType;
protected String userId;
public AttachmentQuery(ManagementService managementService) {
super(managementService);
}
public AttachmentQuery attachmentId(String attachmentId){
this.attachmentId = attachmentId;
return this;
}
public AttachmentQuery attachmentName(String attachmentName){
this.attachmentName = attachmentName;
return this;
}
public AttachmentQuery attachmentType(String attachmentType){
this.attachmentType = attachmentType;
return this;
}
public AttachmentQuery userId(String userId){
this.userId = userId;
return this;
}
@Override
public long executeCount(CommandContext commandContext) {
return (Long) CommandContextUtil.getDbSqlSession()
.selectOne("selectAttachmentCountByQueryCriteria", this);
}
@Override
public List<Attachment> executeList(CommandContext commandContext, Page page) {
return CommandContextUtil.getDbSqlSession()
.selectList("selectAttachmentByQueryCriteria", this);
}
请注意,扩展 AbstractQuery 时,扩展类应向超类传入 ManagementService 的实例。
包含映射语句的 XML 文件可以如下所示:
<mapper namespace="org.flowable.standalone.cfg.AttachmentMapper">
<select id="selectAttachmentCountByQueryCriteria" parameterType="org.flowable.standalone.cfg.AttachmentQuery" resultType="long">
select count(distinct RES.ID_)
<include refid="selectAttachmentByQueryCriteriaSql"/>
</select>
<select id="selectAttachmentByQueryCriteria" parameterType="org.flowable.standalone.cfg.AttachmentQuery" resultMap="org.flowable.engine.impl.persistence.entity.AttachmentEntity.attachmentResultMap">
${limitBefore}
select distinct RES.* ${limitBetween}
<include refid="selectAttachmentByQueryCriteriaSql"/>
${orderBy}
${limitAfter}
</select>
<sql id="selectAttachmentByQueryCriteriaSql">
from ${prefix}ACT_HI_ATTACHMENT RES
<where>
<if test="attachmentId != null">
RES.ID_ = #{attachmentId}
</if>
<if test="attachmentName != null">
and RES.NAME_ = #{attachmentName}
</if>
<if test="attachmentType != null">
and RES.TYPE_ = #{attachmentType}
</if>
<if test="userId != null">
and RES.USER_ID_ = #{userId}
</if>
</where>
</sql>
</mapper>
诸如分页、排序、表名前缀等能力都是可用的,可以在语句中使用。
最后,AttachmentQuery 可以按如下方式使用:
....
// Get the total number of attachments
long count = new AttachmentQuery(managementService).count();
// Get attachment with id 10025
Attachment attachment = new AttachmentQuery(managementService).attachmentId("10025").singleResult();
// Get first 10 attachments
List<Attachment> attachments = new AttachmentQuery(managementService).listPage(0, 10);
// Get all attachments uploaded by user kermit
attachments = new AttachmentQuery(managementService).userId("kermit").list();
....
关于使用 XML 映射语句的可运行示例,参见 org.flowable.standalone.cfg 包中的单元测试。
使用 ProcessEngineConfigurator 的高级流程引擎配置
挂接流程引擎配置的一种高级方式是使用 ProcessEngineConfigurator。它允许在流程引擎构建期间执行自定义逻辑。
<bean id="processEngineConfiguration" class="...SomeProcessEngineConfigurationClass">
...
<property name="configurators">
<list>
<bean class="com.mycompany.MyConfigurator">
...
</bean>
</list>
</property>
...
</bean>
实现该接口需要两个方法。configure 方法接收一个 ProcessEngineConfigurationImpl 实例,可以用来设置或替换其上的任何属性/bean。getPriority 方法返回配置器的优先级,决定多个配置器之间的执行顺序。
这样的配置器的一个例子是 LDAP 集成,其中配置器用 LDAP 版本的身份服务替换了默认身份服务。
public class ProcessDefinitionCacheConfigurator extends AbstractProcessEngineConfigurator {
public void configure(ProcessEngineConfigurationImpl processEngineConfiguration) {
MyCache myCache = new MyCache();
processEngineConfiguration.setProcessDefinitionCache(enterpriseProcessDefinitionCache);
}
}
流程引擎配置器也可以通过 ServiceLoader 从 classpath 自动发现。这样就不需要在配置中显式声明配置器了。
INFO org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl - Found 1 auto-discoverable Process Engine Configurators
INFO org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl - Found 1 Process Engine Configurators in total:
INFO org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl - class org.flowable.MyCustomConfigurator
请注意,这种 ServiceLoader 方式在某些环境中可能不工作。可以使用相应的属性显式禁用自动服务发现。