跳到主要内容

高级主题

Doubao 中英对照 Advanced

以下章节介绍 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 在流程引擎配置上使用:

异步执行器配置选项

名称默认值描述
asyncExecutorThreadPoolQueueSize100作业被获取后、在被线程池中的线程实际执行之前所放置的队列大小
asyncExecutorCorePoolSize8线程池中为执行作业而保持存活的最小线程数。
asyncExecutorMaxPoolSize8线程池中为执行作业而创建的最大线程数。
asyncExecutorThreadKeepAliveTime5000用于执行作业的线程在被销毁之前必须保持存活的时间(毫秒)。设置 > 0 会占用资源,但在作业执行频繁时可以避免一直创建新线程。如果为 0,线程在用于执行作业后即被销毁。
asyncExecutorNumberOfRetries3作业在被移到'死信'表之前的重试次数。
asyncExecutorMaxTimerJobsPerAcquisition1一次查询中获取的定时器作业数量。默认值为 1,因为这降低了乐观锁异常的可能性。较大的值可能性能更好,但不同引擎之间发生乐观锁异常的概率也会变大。
asyncExecutorMaxAsyncJobsDuePerAcquisition1一次查询中获取的异步作业数量。默认值为 1,因为这降低了乐观锁异常的可能性。较大的值可能性能更好,但不同引擎之间发生乐观锁异常的概率也会变大。
asyncExecutorDefaultTimerJobAcquireWaitTime10000定时器获取线程等待执行下一次查询的时间(毫秒)。当未发现新的定时器作业,或获取到的定时器作业少于 asyncExecutorMaxTimerJobsPerAcquisition 所设置的数量时发生。
asyncExecutorDefaultAsyncJobAcquireWaitTime10000异步作业获取线程等待执行下一次查询的时间(毫秒)。当未发现新的异步作业,或获取到的异步作业少于 asyncExecutorMaxAsyncJobsDuePerAcquisition 所设置的数量时发生。
asyncExecutorDefaultQueueSizeFullWaitTime0当内部作业队列已满时,异步作业(定时器和异步继续)获取线程等待执行下一次查询的时间(毫秒)。默认为 0(为向后兼容)。将该属性设为更高的值,可以让异步执行器有机会清理队列。
asyncExecutorTimerLockTimeInMillis5 minutes定时器作业被异步执行器获取时锁定的时长(毫秒)。在此期间,其他异步执行器不会尝试获取并锁定该作业。
asyncExecutorAsyncJobLockTimeInMillis5 minutes异步作业被异步执行器获取时锁定的时长(毫秒)。在此期间,其他异步执行器不会尝试获取并锁定该作业。
asyncExecutorSecondsToWaitOnShutdown60当请求关闭执行器(或流程引擎)时,等待优雅关闭用于执行作业的线程池的时间(秒)。
asyncExecutorResetExpiredJobsInterval60 seconds两次连续检查'过期作业'之间的时间(毫秒)。过期作业是指已被锁定(某个执行器写入了锁持有者和时间,但作业从未完成)的作业。在这样的检查中,过期的作业被重新开放,即锁持有者和锁时间会被移除。其他执行器现在就可以拾取它。如果锁时间早于当前日期,该作业被视为过期。
asyncExecutorResetExpiredJobsPageSize3异步执行器的'重置过期'线程一次获取的作业数量。

挂接流程解析​

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 方式在某些环境中可能不工作。可以使用相应的属性显式禁用自动服务发现。

高级查询 API:在运行时任务查询与历史任务查询之间无缝切换​

任何 BPM 用户界面的核心组件之一是任务列表。通常,最终用户处理的是开放的运行时任务。然而,有时也需要查询已完成的历史任务。

这个公共接口是 org.flowable.engine.task.TaskInfoQuery 类。org.flowable.engine.task.TaskQuery 和 org.flowable.engine.history.HistoricTaskInstanceQuery 都继承自它,因此可以以同样的方式使用它们。

TaskInfoQuery<? extends TaskInfoQuery<?,?>, ? extends TaskInfo> taskInfoQuery

呃,好吧。为了'解决'这个问题,可以使用 org.flowable.engine.task.TaskInfoQueryWrapper 类来避免这种泛型噩梦。

TaskInfoQueryWrapper taskInfoQueryWrapper = null;
if (runtimeQuery) {
taskInfoQueryWrapper = new TaskInfoQueryWrapper(taskService.createTaskQuery());
} else {
taskInfoQueryWrapper = new TaskInfoQueryWrapper(historyService.createHistoricTaskInstanceQuery());
}

List<? extends TaskInfo> taskInfos = taskInfoQueryWrapper.getTaskInfoQuery().or()
.taskNameLike("%k1%")
.taskDueAfter(new Date(now.getTime() + (3 * 24L * 60L * 60L * 1000L)))
.endOr()
.list();

通过覆写标准 SessionFactory 实现自定义身份管理​

如果你不想像 LDAP 集成 中那样使用完整的 ProcessEngineConfigurator 实现,可以选择只替换身份服务。

<bean id="processEngineConfiguration" class="...SomeProcessEngineConfigurationClass">

...

<property name="idmIdentityService">
<bean class="com.mycompany.IdmIdentityServiceBean"/>
</property>

...

</bean>

查看 LDAPIdentityServiceImpl 类的实现,可以获得如何实现自定义身份服务的好例子。
你需要弄清楚要在自定义身份服务类中实现哪些方法。例如下面这行代码:

long potentialOwners = identityService.createUserQuery().memberOfGroup("management").count();

会导致调用 IdmIdentityService 接口的以下成员:

UserQuery createUserQuery();

LDAP 集成 的代码包含如何实现自定义身份服务的完整示例。

启用安全 BPMN 2.0 XML​

在大多数情况下,部署到 Flowable 引擎的 BPMN 2.0 流程处于严密控制之下。然而,也存在 BPMN XML 来自不受信任来源的情况,这可能导致安全攻击。

为避免上述链接中描述的攻击,可以在流程引擎配置上设置 enableSafeBpmnXml 属性。

<property name="enableSafeBpmnXml" value="true"/>

**默认情况下此特性是禁用的!**原因是它依赖 Woodstox StAX 解析器的可用性。Woodstox 是一个安全的 XML 解析器,可以防止 XXE 等攻击。

如果 Flowable 运行的平台确实支持它,请务必启用此特性。

事件日志​

引入了一种事件日志机制。该日志机制建立在通用的事件机制之上,默认将引擎事件写入数据库中的一张表,从而可以查询和分析引擎的执行情况。

要启用数据库日志器:

processEngineConfigurationImpl.setEnableDatabaseEventLogging(true);

或在运行时:

databaseEventLogger = new EventLogger(processEngineConfiguration.getClock(),
processEngineConfiguration.getObjectMapper());
runtimeService.addEventListener(databaseEventLogger);

EventLogger 类可以被扩展。特别是,createEventFlusher() 方法需要返回一个实现 EventFlusher 接口的实例,该接口负责实际发送事件数据。

很容易看出这张表的数据现在可以用来把 JSON 喂给大型 NoSQL 存储(如 MongoDB),从而对流程执行做大数据分析。

请注意,这种事件日志机制是对 Flowable'传统'历史管理器的补充。虽然它会记录所有事件,
但它并未针对查询或便捷检索进行优化。真正的用例是审计追踪和把数据喂给外部系统。

禁用批量插入​

默认情况下,引擎会把同一数据库表的多条 insert 语句组合成一次批量插入。

然而,某个受支持且经过测试的特定数据库版本可能不允许批量插入(我们已经见过一些此类情况)。此时可以禁用批量插入:

<property name="bulkInsertEnabled" value="false" />

安全脚本​

默认情况下,使用脚本任务时,执行的脚本在 JVM 中拥有全部权限。这对开发很方便,但在生产环境中可能有安全风险。

另一方面,脚本是流程定义的一部分,其生命周期与流程定义相同。脚本任务不像 JavaDelegate 那样有安全管理器。恶意脚本可以访问 JVM 的所有类和资源。

为解决这个问题,可以启用安全脚本特性。目前该特性仅对 JavaScript(Rhino)实现。需要添加以下依赖:

<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-secure-javascript</artifactId>
<version>${flowable.version}</version>
</dependency>

添加此依赖会传递性地引入 Rhino 依赖(见 https://github.com/mozilla/rhino)。Rhino 是一个纯 Java 的 JavaScript 实现,与标准 Nashorn JavaScript 引擎有所不同。

这意味着脚本之间可能存在(通常较小的)差异,例如 importPackage 等 Rhino 特有的函数。

安全脚本通过一个专门的 Configurator 对象进行配置,该对象被传给流程引擎配置:

SecureJavascriptConfigurator configurator = new SecureJavascriptConfigurator()
.setWhiteListedClasses(new HashSet<String>(Arrays.asList("java.util.ArrayList")))
.setMaxStackDepth(10)
.setMaxScriptExecutionTime(3000L)
.setMaxMemoryUsed(3145728L)
.setNrOfInstructionsBeforeStateCheckCallback(10);

processEngineConfig.addConfigurator(configurator);

以下设置是可行的:

  • enableClassWhiteListing:为 true 时,所有类将被列入黑名单,所有想使用的类都必须显式加入白名单。
  • whiteListedClasses:一个字符串 Set,对应允许使用的类的全限定类名。
  • maxStackDepth:限制脚本中调用函数时的栈深度。这可以用来避免无限递归。
  • maxScriptExecutionTime:脚本允许运行的最长时间。默认为 -1(禁用)。
  • maxMemoryUsed:脚本允许使用的最大内存(字节)。请注意,脚本引擎本身的内存使用不受此限制。
  • nrOfInstructionsBeforeStateCheckCallback:最大脚本执行时间和内存用量是通过定期检查实现的。该属性设置两次状态检查回调之间的指令数。

注意: maxMemoryUsed 设置只能由支持 com.sun.management.ThreadMXBean 的 JVM 使用。

ScriptExecutionListener 和 ScriptTaskListener 也有安全变体:org.flowable.scripting.secure.listener.SecureJavascriptExecutionListener 和 SecureJavascriptTaskListener。

它的用法如下:

<flowable:executionListener event="start" class="org.flowable.scripting.secure.listener.SecureJavascriptExecutionListener">
<flowable:field name="script">
<flowable:string>
<![CDATA[
execution.setVariable('test');
]]>
</flowable:string>
</flowable:field>
<flowable:field name="language" stringValue="javascript" />
</flowable:executionListener>

要查看演示不安全脚本以及如何通过安全脚本特性使其安全的示例,参见相应的单元测试。

日志会话 [实验性]​

Logging sessions 在 6.5.0 中加入,即使发生异常也允许你收集关于流程执行的信息。其核心是 LoggingListener 接口,它会在每个流程引擎事件发生时被调用。

在这个简单实现中,每个 ObjectNode 被发送到日志器:

class MyLoggingListener implements LoggingListener{
static Logger logger = LoggerFactory.getLogger(MyLoggingListener.class);

@Override
public void loggingGenerated(List<ObjectNode> loggingNodes) {
loggingNodes.forEach(jsonNodes -> logger.info(jsonNodes.toString()));
}
}

在流程引擎配置期间,会传入 LoggingListener 的一个实例

ProcessEngine processEngine = ProcessEngineConfiguration.createStandaloneInMemProcessEngineConfiguration()
.setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_FALSE)
.setJdbcUrl("jdbc:h2:mem:my-own-db;DB_CLOSE_DELAY=1000")
.setLoggingListener(new MyLoggingListener())
.buildProcessEngine();

LoggingSession ObjectNode​

传给 loggingGenerated 方法的 ObjectNode 列表是 JSON 对象,至少具有以下属性:

  • message - 一条人类可读的消息
  • scopeId - 用于将同一事务的所有消息分组的关联 ID
  • scopeType - 作用域的类型

根据所描述事件的类型,还会出现附加字段:

2020-01-21 10:46:54.852  INFO 4985 --- [  restartedMain] c.e.f.MyLoggingListener                : {"message":"Variable 'initiator' created","scopeId":"a193efb3-3c6d-11ea-a01d-bed6c476b3ed","scopeType":"bpmn","variableName":"initiator","variableType":"null","variableRawValue":null,"variableValue":null,"scopeDefinitionId":"loggingSessionProcess:1:a18d38ef-3c6d-11ea-a01d-bed6c476b3ed","scopeDefinitionKey":"loggingSessionProcess","scopeDefinitionName":"Logging Session Process","__id":"a1948bf5-3c6d-11ea-a01d-bed6c476b3ed","__timeStamp":"2020-01-21T16:46:54.819Z","type":"variableCreate","__transactionId":"a1948bf5-3c6d-11ea-a01d-bed6c476b3ed","__logNumber":1}