Flowable CMMN API 全面解析:从引擎服务到表达式与单元测试的实战指南
2026/9/16 14:37:47 网站建设 项目流程

Flowable CMMN API 全面解析:从引擎服务到表达式与单元测试的实战指南

【免费下载链接】flowable-engineA compact and highly efficient workflow and Business Process Management (BPM) platform for developers, system admins and business users.项目地址: https://gitcode.com/GitHub_Trending/fl/flowable-engine

导读

本文以 Flowable 开源工作流引擎中的 CMMN(Case Management Model and Notation,案例管理模型与符号)引擎 API 为核心,系统讲解如何通过CmmnEngine及其六大服务与案例管理功能交互,涵盖仓储部署、运行时案例实例与计划项(Plan Item)操作、任务处理、历史数据查询与历史清理,以及异常策略、类型安全的 Query API、变量与瞬态变量、UEL 表达式与表达式函数等高级主题。读完本文,你将掌握 Flowable CMMN 引擎的完整 API 骨架,能够编写可运行的案例定义部署代码、使用表达式编写哨兵条件与监听器逻辑,并基于 JUnit Jupiter 为案例逻辑编写标准单元测试。

一、CMMN 引擎 API 与六大服务

1.1 入口:CmmnEngine

在 Flowable 中,与 CMMN 引擎交互最常见的方式就是通过引擎 API。整个 API 的起点是CmmnEngine,它可以通过多种方式创建(详见配置章节)。从CmmnEngine上可以获取包含案例/CMMN 方法的各个服务对象。

从仓库源码可以看到,CmmnEngine 接口 继承自 Flowable 的通用Engine接口,除声明VERSION常量(对应FlowableVersions.CURRENT_VERSION)外,核心就是暴露各服务的方法:

CmmnEngine cmmnEngine = CmmnEngineConfiguration.createStandaloneCmmnEngineConfiguration(); CmmnRuntimeService runtimeService = cmmnEngine.getCmmnRuntimeService(); CmmnRepositoryService repositoryService = cmmnEngine.getCmmnRepositoryService(); CmmnTaskService taskService = cmmnEngine.getCmmnTaskService(); CmmnManagementService managementService = cmmnEngine.getCmmnManagementService(); CmmnHistoryService historyService = cmmnEngine.getCmmnHistoryService();

CmmnEngineConfiguration.createStandaloneCmmnEngineConfiguration()会初始化并构建一个 CMMN 引擎,之后始终返回该引擎实例。这里的静态工厂方法在源码 CmmnEngineConfiguration.java 中定义。

配置文件的自动发现机制CmmnEngineConfiguration类会扫描类路径下所有的flowable.cmmn.cfg.xmlflowable-cmmn-context.xml文件:

  • 对于每个flowable.cmmn.cfg.xml,CMMN 引擎按照 Flowable 典型方式构建:CmmnEngineConfiguration.createCmmnEngineConfigurationFromInputStream(inputStream).buildCmmnEngine()
  • 对于每个flowable-cmmn-context.xml,CMMN 引擎按 Spring 方式构建:先创建 Spring 应用上下文,再从该上下文中获取 CMMN 引擎。

线程安全与集群友好:所有服务都是无状态的。这意味着可以轻松地在集群的多个节点上运行 Flowable,各节点访问同一个数据库,无需担心之前调用由哪台机器执行。对任意服务的任意调用都是幂等的,与执行位置无关。因此你可以为整个服务器持有CmmnEngine与各服务对象的一个引用长期复用。

1.2 CmmnRepositoryService:部署与案例定义

CmmnRepositoryService是使用 Flowable CMMN 引擎时最可能首先用到的服务,它提供对部署(Deployment)与案例定义(Case Definition)的管理和操作:

  • 案例定义是 CMMN 1.1 案例的 Java 对应物,是对案例每个步骤结构与行为的表示;
  • 部署是 Flowable CMMN 引擎内的打包单元,一个部署可以包含多个 CMMN 1.1 XML 文件以及任意其他资源。一个部署里放什么由开发者决定:可以只放一个 CMMN 1.1 XML 文件,也可以打包一整包案例和相关资源(例如部署hr-cases可以包含所有与 HR 案例相关的内容)。

CmmnRepositoryService可以部署上述包。部署意味着将包上传到引擎,所有案例被检查并解析后才存入数据库。从这一刻起,系统就认识了该部署,部署中包含的任何案例都可以被启动了。

此外,该服务还允许:

  • 查询引擎已知的部署和案例定义;
  • 获取各种资源,例如部署内的文件或引擎自动生成的案例图;
  • 获取案例定义的 POJO 版本,从而可以用 Java 而非 XML 来内省(introspect)案例。

1.3 CmmnRuntimeService:案例实例与运行时状态

与主要处理静态信息的CmmnRepositoryService相反,CmmnRuntimeService处理动态的运行时信息,包括:

  • 启动新案例实例:案例定义定义了案例中不同步骤的结构与行为,而案例实例是某次对该案例定义的执行。每个案例定义通常同时有多个实例在运行;
  • 存取案例变量:变量是与给定案例实例相关的数据,可被案例中的各种构造使用(例如计划项转换条件常用变量来决定案例继续走哪条路径);
  • 查询案例实例与计划项:计划项(Plan Item)是 CMMN 1.1 中已启用(enabled)计划项的表示;
  • 外部触发续跑:当案例实例处于等待外部触发器的等待状态时,使用该服务的各种操作向实例"发送信号",告知外部触发已收到、案例实例可以继续。

从源码 CmmnRuntimeService.java 可以看到更丰富的运行时能力,例如createCaseInstanceBuilder()triggerPlanItemInstanceenablePlanItemInstancestartPlanItemInstancedisablePlanItemInstancecompleteStagePlanItemInstancecompleteCaseInstanceterminateCaseInstance等,覆盖了计划项生命周期的各个状态转换。

1.4 CmmnTaskService:人工任务

人工任务的处理是 CMMN 引擎的核心之一,与任务相关的一切都集中在CmmnTaskService中:

  • 查询分配给用户或组的任务;
  • 创建新的独立任务(standalone task)——即与案例实例无关的任务;
  • 调整任务的分配人,或哪些用户以某种方式参与了任务;
  • 认领(claim)与完成(complete)任务:认领意味着某人决定成为该任务的负责人(assignee),即该用户将完成任务;完成意味着"执行任务的工作",通常是填写某种表单。

1.5 CmmnHistoryService 与 CmmnManagementService

  • CmmnHistoryService暴露 Flowable CMMN 引擎收集的所有历史数据。执行案例时引擎可以保留大量数据(可配置),例如案例实例的启动时间、谁执行了哪些任务、任务完成耗时、每个案例实例走了哪条路径等。该服务主要暴露查询能力来访问这些数据;
  • CmmnManagementService提供关于数据库表的底层信息,允许查询不同类型的作业(job)并执行它们。

关于各服务操作与引擎 API 更详细的信息,可查阅项目源码目录 modules/flowable-cmmn-api/src/main/java/org/flowable/cmmn/api 中各接口的 Javadoc。

二、异常策略

Flowable 的基础异常是org.flowable.engine.common.api.FlowableException(注:在现行代码结构中其实际包名为org.flowable.common.engine.api.FlowableException),属于非受检异常(unchecked exception)。API 在任何时候都可能抛出该异常,但特定方法中"预期"发生的异常会在 Javadoc 中说明。例如CmmnTaskService的片段:

/** * Called when the task is successfully executed. * @param taskId the id of the task to complete, cannot be null. * @throws FlowableObjectNotFoundException when no task exists with the given id. */ void complete(String taskId);

上面的例子中,传入一个不存在的 id 时会抛出异常。同时,由于 Javadoc明确说明 taskId 不能为 null,因此传入 null 时会抛出 FlowableIllegalArgumentException

虽然 Flowable 有意避免庞大的异常层级,但以下子类在特定情况下会被抛出。其余在流程执行或 API 调用中发生、不属于下述异常的错误,一律以普通的FlowableException抛出:

异常类触发场景
FlowableWrongDbExceptionFlowable 引擎发现数据库 Schema 版本与引擎版本不匹配时
FlowableOptimisticLockingException数据存储中因并发访问同一数据项发生乐观锁冲突时
FlowableClassLoadingException请求加载的类未找到或加载出错时(如 JavaDelegates、TaskListeners 等)
FlowableObjectNotFoundException请求或操作的对象不存在时
FlowableIllegalArgumentExceptionFlowable API 调用中提供了非法参数、引擎配置中配置了非法值,或流程定义中使用了非法值时
FlowableTaskAlreadyClaimedException调用taskService.claim(...)时任务已被认领

这些异常类在仓库中均有真实实现,集中在 modules/flowable-engine-common-api/src/main/java/org/flowable/common/engine/api 目录下,包括FlowableWrongDbExceptionFlowableOptimisticLockingExceptionFlowableClassLoadingExceptionFlowableObjectNotFoundExceptionFlowableIllegalArgumentExceptionFlowableTaskAlreadyClaimedException六个文件,与文档列举一一对应。

三、Query API:类型安全的流式查询

从引擎查询数据有两种方式:Query API原生查询(native queries)。Query API 允许用流式 API 编写完全类型安全的查询。你可以给查询添加各种条件(所有条件之间按逻辑 AND 组合),并且只能指定一个排序规则。示例:

List<Task> tasks = taskService.createTaskQuery() .taskAssignee("kermit") .orderByDueDate().asc() .list();

四、变量

每个案例实例在执行其组成步骤时都需要数据。在 Flowable 中,这些数据被称为变量(variables),存储在数据库中。变量可用于表达式(例如哨兵 sentry 的条件)、Java 服务任务中调用外部服务(例如提供服务调用的输入或存储其结果)等场景。

案例实例可以有变量(称为案例变量 case variables),此外计划项实例人工任务也可以有自己的变量。一个案例实例可以有任意数量的变量,每个变量作为ACT_RU_VARIABLE数据库表中的一行存储。

createCaseInstanceBuilder方法提供了可选方法,用于在通过CmmnRuntimeService创建并启动案例实例时提供变量:

CaseInstance caseInstance = runtimeService.createCaseInstanceBuilder().variable("var1", "test").start();

变量也可以在案例执行过程中添加,例如(CmmnRuntimeService):

void setVariables(String caseInstanceId, Map<String, ? extends Object> variables);

变量同样可以被读取,如下所示。注意CmmnTaskService上存在类似的方法:

Map<String, Object> getVariables(String caseInstanceId); Object getVariable(String caseInstanceId, String variableName);

变量经常被用于 Java 服务任务、表达式、脚本等场景。

五、瞬态变量(Transient Variables)

瞬态变量行为与普通变量类似,但不会被持久化。瞬态变量通常用于高级场景,拿不准时请使用普通案例变量。

瞬态变量的规则如下:

  • 完全不保存历史:瞬态变量不会存储任何历史;
  • 与普通变量一样存放在最高父级:在计划项上设置变量时,瞬态变量实际存储在案例实例执行(case instance execution)上。与普通变量一样,如果要在特定计划项或任务上设置变量,存在local变体方法;
  • 生命周期仅到下一个"等待状态"之前:瞬态变量只能在案例定义的下一个等待状态之前被访问,此后即消失。这里的等待状态指案例实例被持久化到数据存储的时间点;
  • 设置与读取的对称性:瞬态变量只能通过setTransientVariable(name, value)设置,但调用getVariable(name)时也会返回瞬态变量(同时也存在只检查瞬态变量的getTransientVariable(name))。这样设计的目的是让表达式书写更简单,让使用变量的既有逻辑对两类变量一视同仁;
  • 遮蔽同名持久变量:瞬态变量会**遮蔽(shadow)**同名的持久变量。即当案例实例上同时设置了同名的持久变量和瞬态变量时,调用getVariable("someVariable")返回的是瞬态变量的值。

可以在大多数暴露普通变量的地方设置和获取瞬态变量:

  • PlanItemJavaDelegate实现中的DelegatePlanItemInstance上;
  • 通过 runtime service 启动案例实例时;
  • 完成任务时。

方法遵循普通案例变量的命名约定:

CaseInstance caseInstance = runtimeService.createCaseInstanceBuilder().transientVariable("var1", "test").start();

六、表达式(Expressions)

6.1 UEL 与两种表达式类型

Flowable 使用UEL(Unified Expression Language,统一表达式语言)进行表达式解析,UEL 是 EE6 规范的一部分。表达式可用于例如 Java 服务任务、哨兵条件和计划项监听器。虽然表达式分为**值表达式(value-expression)方法表达式(method-expression)**两种,但 Flowable 做了抽象,在期望表达式的地方两者都可以使用。

  • 值表达式:解析为一个值。默认情况下所有案例变量都可用,所有 Spring Bean(如果使用 Spring)也可在表达式中使用。非 Spring 环境下,可通过CmmnEngineConfigurationsetBeans方法为表达式设置可用 Bean。例如:
${myVariable} ${myBean.myProperty}
  • 方法表达式:调用带参数或不带参数的方法。调用无参方法时,务必在方法名后加上空括号(这使表达式区别于值表达式)。传入的参数可以是字面值,也可以是会被自行解析的表达式。例如:
${printer.print()} ${myBean.addNewOrder('orderName')} ${myBean.doSomething(myVar, planItemInstance)}

这些表达式支持解析基本类型(包括基本类型之间的比较)、Bean、列表、数组和 Map。

6.2 表达式中可用的默认对象

除了所有案例实例变量,还有可在表达式中使用的默认对象:

关键字说明
caseInstance持有正在进行的案例实例的附加信息,在所有表达式中都可用
planItemInstanceDelegatePlanItemInstance持有当前计划项实例的附加信息,在所有计划项相关表达式(哨兵条件、计划项生命周期监听器、服务任务表达式等)中可用
planItemInstances暴露所有当前计划项实例的信息(用法见下文示例)
variableContainer表达式所解析的VariableContainer。变量容器是对案例实例、计划项实例、流程实例和执行(execution)的抽象,该关键字允许写出不绑定到特定实现的表达式
authenticatedUserId当前已认证用户的 id;若没有用户认证,则该变量不可用

示例:

${caseInstance.id} ${caseInstance.getVariable('myVariable') == 'test'} ${caseInstance.setVariable('myVariable', 'test')} ${planItemInstance.getPlanItem().getPlanItemDefinition().getName()} ${planItemInstance.getVariable('myVariable') == 123} ${planItemInstance.setVariable('myVariable', 123)} ${variableContainer.getVariable('myVariable')} ${variableContainer.setVariable('myVariable', 'true')}

6.3 planItemInstances 关键字:计划项实例过滤与统计

planItemInstances关键字值得特别说明。使用该关键字可以检索所有当前计划项实例,同时它类似于一个 API,可以检索当前计划项实例的更多信息。

例如,下面的表达式:

${planItemInstances.active().count()}

返回当前处于'active'(激活)状态的所有计划项实例的计数。如果只关心某个特定计划项,可以进一步过滤:

${planItemInstances.definitionId('a').active().count()}

如示例所示,planItemInstances关键字允许链式串联各种过滤方法,链式串联采用AND 语义。支持的方法如下。

按计划项实例状态过滤:

  • active()
  • available()
  • enabled()
  • disabled()
  • completed()
  • terminated()

以下状态过滤器也受支持,但注意它们反映的是非规范合规的内部状态

  • unavailable()
  • waitingForRepetition()
  • asyncActive()

按终态/非终态过滤(获取处于终态——即 terminated 或 completed——或非终态的计划项实例):

  • onlyTerminal()
  • onlyNonTerminal()

按 CMMN 模型中设置的标识符过滤:

  • definitionId('id1')
  • definitionIds('id1', 'id2')

按名称过滤:

  • name('name1')
  • names('name1', 'name2', 'name3')

使用 lambda 表达式过滤:

  • filter(planItemInstance -> planItemInstance.name.startsWith('Service'))

按当前阶段(stage)过滤:某些用例中,应只过滤属于当前阶段的计划项实例。"当前阶段"是计划项的父阶段;没有父阶段时则是案例实例本身。

  • currentStage()

返回结果的方法(这些方法不能继续链式串联,因为它们返回结果):

  • count():在应用所有过滤器后返回计划项实例的数量;
  • getDefinitionIds():返回匹配的计划项实例在 CMMN 模型中定义的所有 id 的字符串列表;
  • getDefinitionNames():返回匹配的计划项实例在 CMMN 模型中定义的所有名称列表;
  • getList():返回org.flowable.cmmn.api.runtime.PlanItemInstance实例的"原始"列表。

6.4 综合示例

统计案例实例中所有活跃计划项实例:

${planItemInstances.active().count()}

统计 id 为 'a' 或 'b' 的所有活跃计划项实例:

${planItemInstances.active().definitionIds('a', 'b').count()}

获取当前阶段中处于终态的所有计划项实例的 id:

${planItemInstances.currentStage().onlyTerminal().getDefinitionIds()()}

将上述表达式的结果存储到瞬态变量中:

${caseInstance.setTransientVariable('myVar', planItemInstances.currentStage().onlyTerminal().getDefinitionIds()}

七、表达式函数(Expression Functions)

7.1 variables 命名空间的内置函数

[实验性]表达式函数在 6.4.0 版本中加入。为了让案例变量操作更简单,在variables命名空间下提供了一组开箱即用的函数。

  • variables:get(varName):获取变量的值。与直接在表达式中写变量名的主要区别是:使用该函数时,变量不存在不会抛异常。例如${myVariable == "hello"}myVariable不存在时会抛异常,而${var:get(myVariable) == 'hello'}可以正常工作。
  • variables:getOrDefault(varName, defaultValue):类似get,但可提供默认值,当变量未设置或值为null时返回默认值。
  • variables:exists(varName):若变量有非 null 值则返回true
  • variables:isEmpty(varName)(别名:empty:检查变量值是否为空。依变量类型不同行为如下:
    • String 变量:为空字符串则视为空;
    • java.util.Collection变量:集合无元素则返回true
    • ArrayNode 变量:无元素则返回true
    • 变量为null时:始终返回true
  • variables:isNotEmpty(varName)(别名:notEmptyisEmpty的逆操作。
  • variables:equals(varName, value)(别名:eq:检查变量是否等于给定值。它是${execution.getVariable("varName") != null && execution.getVariable("varName") == value}这种写法的简写形式。若变量值为 null,返回false(除非与 null 比较)。
  • variables:notEquals(varName, value)(别名:neequals的反向比较。
  • variables:contains(varName, value1, value2, …):检查变量是否包含所有传入的值。依变量类型不同行为如下:
    • String 变量:传入的值作为必须成为变量一部分的子串;
    • java.util.Collection变量:所有传入值都必须是集合的元素(常规contains语义);
    • ArrayNode 变量:支持检查数组节点是否包含作为变量类型支持的 JsonNode;
    • 变量值为 null 时一律返回false;变量值非 null 且实例类型不属于上述类型时返回false
  • variables:containsAny(varName, value1, value2, …):类似contains,但只要传入值中任意一个(而非全部)包含在变量中即返回true
  • variables:base64(varName):将 Binary 或 String 变量转换为 Base64 字符串。
  • 比较函数
    • variables:lowerThan(varName, value)(别名:lessThan:lt):${execution.getVariable("varName") != null && execution.getVariable("varName") < value}的简写;
    • variables:lowerThanOrEquals(varName, value)(别名:lessThanOrEquals:lte):类似,但为<=
    • variables:greaterThan(varName, value)(别名:gt):类似,但为>
    • variables:greaterThanOrEquals(varName, value)(别名:gte):类似,但为>=

别名机制variables命名空间别名是varsvar,所以variables:get(varName)等价于vars:get(varName)var:get(varName)。注意变量名不需要加引号var:get(varName)等价于var:get('varName')var:get("varName")

作用域自动注入:上述所有函数都无需传入planItemInstancecaseInstance(不使用函数时则必须传)。引擎在调用函数时会自动注入适当的变量作用域。这也意味着这些函数在编写 BPMN 流程定义表达式时可以完全一样地使用。

7.2 在哨兵 if-part 条件中使用变量函数

这些变量函数在 CMMN 中尤其有用,例如编写哨兵(sentry)if-part 的条件时。考虑下面的 CMMN 案例定义:

假设哨兵除完成事件外还有一个 if-part。案例实例刚启动时(阶段变为 available),该 if-part 条件就会被求值。如果条件是${someVariable == someValue}这种形式,意味着变量必须在启动案例实例时就可用。但很多情况下这不可行,或者变量来得更晚(例如来自表单),这会导致底层抛出PropertyNotFoundException。考虑到变量的可空性,正确的表达式原本必须写成:

${planItemInstance.getVariable('someVariable') != null && planItemInstance.getVariable('someVariable') == someValue}

这相当冗长。但使用上述函数可以简化为:

${var:eq(someVariable, someValue)}

${var:get(someVariable) == someValue}

函数实现会考虑变量的可空性(变量为 null 时不抛异常)并正确处理相等性比较。

7.3 注册自定义表达式函数

此外,还可以注册可在表达式中使用的自定义函数。参考org.flowable.common.engine.api.delegate.FlowableFunctionDelegate接口。从源码 FlowableFunctionDelegate.java 可以看到,该接口的核心设计是:

  • prefix()/prefixes():表达式{prefix:method()}中冒号前的部分,用于将表达式文本匹配到对应的FlowableFunctionDelegate实例(prefixes()默认方法允许一个实现覆盖多个前缀);
  • localName()/localNames():冒号后的方法名部分(localNames()允许一个方法覆盖多个名称);
  • functionMethod():返回由 JUEL 调用的实际方法。

八、历史清理(History Cleaning)

默认情况下历史数据会永久存储,这会导致历史表无限增长、影响HistoryService的性能。历史清理功能在6.5.0版本引入,允许删除HistoricProcessInstances及其关联数据。一旦流程数据不再需要保留,就可以删除以减小历史数据库体积(对 CMMN 而言即HistoricCaseInstances及相关数据)。

8.1 自动历史清理配置

HistoricCaseInstances的自动清理默认禁用,但可以通过编程方式启用和配置。启用后,默认在凌晨 1 点运行一个清理作业,删除所有365 天前或更早结束HistoricCaseInstances及其关联数据。

历史流程的删除使用 FlowableBatch 机制完成:通过调度作业分批删除流程,并将所做工作的信息存储在 batch 表中。

CmmnEngine cmmnEngine = CmmnEngineConfiguration. .createProcessEngineConfigurationFromResourceDefault() .setEnableHistoryCleaning(true) .setHistoryCleaningTimeCycleConfig("0 0 1 * * ?") .setCleanInstancesEndedAfter(Duration.ofDays(365)) .buildCmmnEngine();

上述配置方法在源码 CmmnEngineConfiguration.java 中均有对应实现:setEnableHistoryCleaning(boolean)setHistoryCleaningTimeCycleConfig(String)setCleanInstancesEndedAfter(Duration)。其中setCleanInstancesEndedAfterNumberOfDays(int)已被标记为弃用(deprecated),建议改用setCleanInstancesEndedAfter(Duration)

Spring 环境也可以在application.properties或外部化配置中设置:

flowable.enable-history-cleaning=true flowable.history-cleaning-after=365d flowable.history-cleaning-cycle=0 0 1 * * ?

8.2 手动删除历史

手动清理历史可以通过执行CmmnHistoryService查询构建器上的方法来完成。

删除所有超过一年的HistoricCaseInstances及其相关数据:

int numberOfCasesInBatch = 10; Calendar cal = new GregorianCalendar(); cal.set(Calendar.YEAR, cal.get(Calendar.YEAR) - 1); cmmnHistoryService.createHistoricCaseInstanceQuery() .finishedBefore(cal.getTime()) .deleteSequentiallyUsingBatch(numberOfCasesInBatch, "Custom Delete Batch");

九、单元测试

案例是软件项目的组成部分,它们应当像普通应用逻辑一样被测试:使用单元测试。由于 Flowable 是可嵌入式(embeddable)Java 引擎,为业务案例编写单元测试就像编写普通单元测试一样简单。Flowable 支持JUnit Jupiter风格的单元测试。

9.1 测试注解与扩展机制

在 JUnit Jupiter 风格下,需要使用org.flowable.cmmn.engine.test.FlowableCmmnTest注解,或手动注册org.flowable.cmmn.engine.test.FlowableCmmnExtension。从源码 FlowableCmmnTest.java 可以看到:

  • FlowableCmmnTest是一个元注解(meta-annotation),它通过@ExtendWith(FlowableCmmnExtension.class)完成FlowableCmmnExtension的注册;
  • 该注解的@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)表明它作用于类型(测试类)且运行时保留。

关键行为:

  • 这会使CmmnEngine及各服务作为参数注入到测试方法和生命周期方法(@BeforeAll@BeforeEach@AfterEach@AfterAll)中;
  • 每个测试前,cmmnEngine默认使用类路径上的flowable.cmmn.cfg.xml资源初始化;
  • 若要指定不同的配置文件,需要使用org.flowable.cmmn.engine.test.CmmnConfigurationResource注解(见下面第二个示例);
  • 当配置资源相同时,CMMN 引擎会在多个单元测试间静态缓存

使用FlowableCmmnExtension,可以为测试方法标注org.flowable.cmmn.engine.test.CmmnDeployment

  • 当测试方法标注@CmmnDeployment后,每个测试前会部署CmmnDeployment#resources中定义的 cmmn 文件;
  • 若未定义资源,则会部署与测试类同包下、名为testClassName.testMethod.cmmn的资源文件;
  • 测试结束时,部署会被删除,包括所有相关的案例实例、任务等。

另外,使用 JUnit Jupiter 还可以将部署的 id(org.flowable.cmmn.engine.test.CmmnDeploymentId)注入到测试和生命周期方法中。

9.2 示例一:使用默认资源的 JUnit Jupiter 测试

@FlowableCmmnTest class MyTest { private CmmnEngine cmmnEngine; private CmmnRuntimeService cmmnRuntimeService; private CmmnTaskService cmmnTaskService; @BeforeEach void setUp(CmmnEngine cmmnEngine) { this.cmmnEngine = cmmnEngine; this.cmmnRuntimeService = cmmnEngine.getCmmnRuntimeService(); this.cmmnTaskService = cmmnEngine.getTaskRuntimeService(); } @Test @CmmnDeployment void testSingleHumanTask() { CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder() .caseDefinitionKey("myCase") .start(); assertNotNull(caseInstance); Task task = cmmnTaskService.createTaskQuery().caseInstanceId(caseInstance.getId()).singleResult(); assertEquals("Task 1", task.getName()); assertEquals("JohnDoe", task.getAssignee()); cmmnTaskService.complete(task.getId()); assertEquals(0, cmmnRuntimeService.createCaseInstanceQuery().count()); } }

9.3 示例二:使用自定义配置资源的 JUnit Jupiter 测试

@FlowableCmmnTest @CmmnConfigurationResource("flowable.custom.cmmn.cfg.xml") class MyTest { private CmmnEngine cmmnEngine; private CmmnRuntimeService cmmnRuntimeService; private CmmnTaskService cmmnTaskService; @BeforeEach void setUp(CmmnEngine cmmnEngine) { this.cmmnEngine = cmmnEngine; this.cmmnRuntimeService = cmmnEngine.getCmmnRuntimeService(); this.cmmnTaskService = cmmnEngine.getTaskRuntimeService(); } @Test @CmmnDeployment void testSingleHumanTask() { CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder() .caseDefinitionKey("myCase") .start(); assertNotNull(caseInstance); Task task = cmmnTaskService.createTaskQuery().caseInstanceId(caseInstance.getId()).singleResult(); assertEquals("Task 1", task.getName()); assertEquals("JohnDoe", task.getAssignee()); cmmnTaskService.complete(task.getId()); assertEquals(0, cmmnRuntimeService.createCaseInstanceQuery().count()); } }

延伸阅读

  • CMMN 配置章节:了解CmmnEngine的各种创建方式与数据库、事件监听等配置项;
  • CMMN API 接口定义:CmmnRuntimeServiceCmmnTaskServiceCmmnRepositoryServiceCmmnHistoryServiceCmmnManagementService等接口的完整 Javadoc;
  • CMMN 引擎实现:CmmnEngine接口与其服务获取方法;
  • CMMN 引擎测试支持:FlowableCmmnTestFlowableCmmnExtensionCmmnDeploymentCmmnConfigurationResourceCmmnDeploymentId等测试注解与扩展的实现。

【免费下载链接】flowable-engineA compact and highly efficient workflow and Business Process Management (BPM) platform for developers, system admins and business users.项目地址: https://gitcode.com/GitHub_Trending/fl/flowable-engine

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询