返回介绍

18.高级功能

发布于 2019-10-28 05:33:29 字数 11133 浏览 1719 评论 0 收藏 0

下面内容将介绍使用 Activiti 的高级用例,它会超越 BPMN 2.0 流程的范畴。 因此,对于 Activiti 的明确目标和经验有利于理解这里的内容。

监听流程解析

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

对于每个流程, BpmnParser 类都会创建一个新的 BpmnParse 实例。 这个实例会作为解析过程中的容器来使用。解析过程很简单: 对于每个 BPMN 2.0 元素,引擎中都会有一个对应的 org.activiti.engine.parse.BpmnParseHandler 实例。 这样,解析器会保存一个 BPMN 2.0 元素与 BpmnParseHandler 实例的映射。 默认,Activiti 使用 BpmnParseHandler 来处理所有支持的元素, 也使用它来提供执行监听器,以支持流程历史。

可以向 Activiti 引擎中添加自定义的 org.activiti.engine.parse.BpmnParseHandler 实例。 经常看到的用例是把执行监听器添加到对应的环节,来处理一些事件队列。 Activiti 在内部就是这样进行历史处理的。 要想添加这种自定义处理器,需要为 Activiti 添加如下配置:

<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>
               

配置到 preBpmnParseHandlersBpmnParseHandler 实例 会添加在默认处理器的前面。与之类似, postBpmnParseHandlers 会加在后面。 当自定义处理器内部逻辑对处理顺序有要求时就很重要了。

org.activiti.engine.parse.BpmnParseHandler 是一个很简单的接口:

public interface BpmnParseHandler {

  Collection<Class>? extends BaseElement>> getHandledTypes();

  void parse(BpmnParse bpmnParse, BaseElement element);

}
               

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 id 生成器需要以下依赖:

 <dependency>
    <groupId>com.fasterxml.uuid</groupId>
    <artifactId>java-uuid-generator</artifactId>
    <version>3.1.3</version>
</dependency>

多租户

多租户通常是在软件需要为多个不同组织服务时产生的概念。 关键是数据分片,组织不能看到其他组织的数据。 这种场景下,组织(或部门,或小组,或。。。)就叫做 租户

注意它和安装多个实例是从基本上不同的,这是一个 activiti 流程引擎实例为每个组织分别运行 (对应不同的数据库表)。虽然 activiti 是轻量级的, 运行流程引擎不会消耗很多资源,但是它还是增加了复杂性,并需要更多维护工作。 但是,对一些场景,可能也是正确的解决方案。

activiti 的多租户主要围绕着数据分片来实现。很重要的一点是, Activiti 没有强行校验多租户的规则。 这意味着不会校验,查询和使用数据时用户是否使用了正确的租户。 这应该由 activiti 引擎的调用者一层负责完成。 activiti 只确认租户信息会被保存,并在查询流程数据时会被用到。

在向 activiti 流程引擎发布流程定义时,可能需要传递一个租户标识。 它是一个字符串(比如,UUID,部门 ID,等等),限制在 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 在集群环境下运行。

[试验项目] 可以通过调用repositoryServicechangeDeploymentTenantId(String deploymentId, String newTenantId)修改 tenantId。 它会修改之前继承的所有 tenantId。 当我们从非多租户环境向多租户环境下切换时,就会非常实用了。 参考方法的 javadoc 获得更多细节信息。

执行自定义 SQL

Activiti API 允许使用高级 API 操作数据库。比如,在查询数据方面,查询 API 和 Native Query API 是非常强大的。 然而,对于某些情况,它们还是不够轻便。 下面的章节描述了如何使用完全自定义的 SQL 语句(select, insert, update 和 delete) 可以执行在 Activiti 的数据存储之上,但是完全又是配置在流程引擎中的 (比如使用事务)。

为了使用自定义 SQL,activiti 引擎使用下层框架的功能,MyBatis。 使用自定义 SQL 的第一件事,要创建 MyBatis 映射类。可以阅读 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>
...

注意,这是一个接口。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 class="...SomeProcessEngineConfigurationClass">

    ...

    <property name="configurators">
        <list>
            <bean class="com.mycompany.MyConfigurator">
                ...
            </bean>
        </list>
    </property>

    ...

</bean>
               

实现这个接口需要实现两个方法。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 Configurators
INFO  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.0 xml

大多数情况下,BPMN 2.0 流程发布到 Activiti 引擎是在严格的控制下的,比如开发团队。 然后,一些情况下,可能需要把比较随意的 BPMN 2.0 xml 上传到引擎。 这种情况,要考虑恶意用户会攻击服务器, 参考这里。

为了避免上面链接描述的攻击, 可以在引擎配置中设置:enableSafeBpmnXml

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

默认这个功能没有开启!这样做的原因是它需要使用 StaxSource 类。 不幸的是,一些平台(比如,JDK 6,JBoss,等等)不能用这个类(因为老的 xml 解析实现) 所以不能启用安全 BPMN 2.0 xml。

如果 Activiti 运行的平台支持这项功能,请打开这个功能。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文