18.高级功能

优质
小牛编辑
120浏览
2023-12-01

下面内容将介绍使用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 id="processEngineConfiguration" 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运行的平台支持这项功能,请打开这个功能。