工作流引擎Activiti使用進階!詳細解析工作流框架中高階功能的使用示例

攻城獅Chova發表於2021-06-13

Activiti高階功能簡介

  • Activit的高階用例,會超越BPMN 2.0流程的範疇,使用Activiti高階功能需要有Activiti開發的明確目標和足夠的Activiti開發經驗

監聽流程解析

  • bpmn 2.0 xml檔案需要被解析為Activiti內部模型,然後才能在Activiti引擎中執行.解析過程發生在釋出流程或在記憶體中找不到對應流程的時候,這時會從資料庫查詢對應的xml
  • 對於每個流程 ,BpmnParser類都會建立一個新的BpmnParse例項.這個例項會作為解析過程中的容器來使用
  • 解析過程:
    • 對於每個BPMN 2.0元素,引擎中都會有一個對應的org.activiti.engine.parse.BpmnParseHandler例項
    • 解析器會儲存一個BPMN 2.0元素與BpmnParseHandler例項的對映
    • 預設Activiti使用BpmnParseHandler來處理所有支援的元素
    • 同時也使用BpmnParseHandler來提供執行監聽器,以支援流程歷史
  • 可以向Activiti引擎中新增自定義的org.activiti.engine.parse.BpmnParseHandler例項
  • 經常使用的用例是把執行監聽器新增到對應的環節,來處理一些事件佇列.Activiti在內部就是這樣進行歷史處理的
  • 要想新增這樣的自定義處理器,需要為Activit增加配置:
<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>
  • 當自定義處理器內部邏輯對處理順序有要求時需要考慮:
    • 配置到preBpmnParseHandlers的BpmnParseHandler例項會新增在預設處理器的前面
    • 配置到postBpmnParseHandlers的BpmnParseHandler例項會新增在預設處理器的後面
  • 介面- org.activiti.engine.parse.BpmnParseHandler:
public interface BpmnParseHandler {

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

  void parse(BpmnParse bpmnParse, BaseElement element);

}
  • BpmnParseHandler介面中:
    • 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生成器需要新增依賴:
 <dependency>
    <groupId>com.fasterxml.uuid</groupId>
    <artifactId>java-uuid-generator</artifactId>
    <version>3.1.3</version>
</dependency>

多租戶

  • 多租戶:
    • 通常是在軟體需要作為多個不同組織服務時產生的概念
    • 關鍵是資料分片,組織不能看到其餘組織的資料
    • 在這種場景下,組織,部門,小組就叫做租戶
  • 多租戶和安裝多個例項是從基本上不同的:
    • 多租戶是一個Activiti流程引擎例項為每個組織分別執行,對應不同的資料表
    • 安裝多個Activiti流程引擎例項時,雖然Activiti是輕量級的,執行流程引擎不會消耗很多資源,但是增加了複雜性,並需要更多維護工作.然而對於一些場景,也是正確的解決方案
  • Activiti的多租戶主要圍繞著資料分片來實現:
    • Activiti沒有強行校驗多租戶的規則,即Activiti不會校驗查詢和使用資料時使用者是否使用了正確的租戶
    • 校驗由Activiti引擎的呼叫者層負責完成
    • Activiti只確認租戶資訊會被儲存,並在查詢流程資料時會被用到
  • 在向Activiti流程引擎釋出流程定義時,需要傳遞一個租戶標識.是一個字串,限制在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. 當需要從非多租戶環境向多租戶環境下切換時,會非常實用

執行自定義SQL

  • Activiti API允許使用高階API運算元據庫:
    • 在查詢資料方面,查詢API和Native Query API是非常強大的
    • 但是對於某些情況,不夠輕便
    • 使用完全自定義的SQL語句:select, insert, update和delete.可以執行在Activiti的資料儲存之上,但是完全又可以配置在流程引擎中:比如使用事務
  • 為了使用自定義SQL,Activiti引擎使用MyBatis框架的功能:
    • 因此使用自定義SQL的第一件事,要建立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>
...
  • 這個Mapper是一個介面:
    • 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>
  • 實現ProcessEngineConfigurator介面需要實現兩個方法:
    • configure: 將ProcessEngineConfiguration作為引數,可以通過這種方法新增自定義配置,這個方法在流程建立之前,在所有預設配置執行之前保證呼叫到
    • getPriority: 如果一些configurator存在依賴項的時候,允許對configurator進行排序
  • configurator例項:
    • LDAP整合:
      • 這個configurator用來替換預設的usergroup管理器類,使用處理LDAP使用者儲存的類
      • 基本上一個configurator允許很大程度上修改或增強流程引擎,對高階的場景非常有用
    • 使用自定義的版本替換流程定義快取, 如下:
public class ProcessDefinitionCacheConfigurator extends AbstractProcessEngineConfigurator {

    public void configure(ProcessEngineConfigurationImpl processEngineConfiguration) {
            MyCache myCache = new MyCache();
            processEngineConfiguration.setProcessDefinitionCache(enterpriseProcessDefinitionCache);
    }

}
  • 流程引擎配置器也可以通過ServiceLoader自動從classpath中載入:
    • 放在jar中的configurator實現必須放在classpath
    • 並在jarMETA-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的方式在某些環境下可能無法正常執行.使用ProcessEngineConfigurationenableConfiguratorServiceLoader屬性來禁用這個功能,這個屬性的預設值為true

啟動安全BPMN 2.0xml

  • 大多數情況下,BPMN 2.0流程釋出到Activiti引擎是在嚴格的控制下的
  • 然而在某些情況下,可能需要把比較隨意的BPMN 2.0 xml上傳到引擎,這時就要要考慮惡意使用者會攻擊伺服器
  • 為了避免BPMN 2.0xml引擎伺服器受到攻擊,可以在引擎中設定enableSafeBpmnXml:
<property name="enableSafeBpmnXml" value="true"/>
  • 預設這個功能沒有開啟.因為這個功能需要使用StaxSource
  • 由於JDK6,JBoss使用的是舊版的xml解析實現,無法使用StaxSource類,所以不能啟用安全的BPMN 2.0xml
  • 如果Activiti執行的平臺支援安全的BPMN 2.0xml功能,建議開啟

事件日誌

  • 在Activiti 5.16版本中,新增了事件日誌機制:
    • 這種日誌機制構建在通用目的下的Activiti引擎的事件機制,預設是禁用的
    • 目的是由引擎產生的事件會被捕獲,包含所有事件資料的map會被建立出來,並提供給org.activiti.engine.impl.event.logger.EventFlusher, 會把資料重新整理到別的地方
    • 預設會使用一個簡單地基於資料庫的事件處理器或者叫作重新整理器,會使用jacksonmap轉換為JSON, 並儲存到資料庫中的EventLogEntryEntity實體
    • 預設會建立資料庫日誌表ACT_EVT_LOG. 如果沒有使用事件日誌,可以刪除這個表
  • 啟用資料庫日誌:
processEngineConfiguration.setEnableDatabaseEventLogging(true);

或者在流程引擎執行階段:

databaseEventLogger = new EventLogger(processEngineConfiguration.getClock());
runtimeService.addEventListener(databaseEventLogger);
  • EventLogger類可以繼承:
    • 在需要使用自定義的資料日誌時:
      • createEventFlusher() 方法需要返回一個org.activiti.engine.impl.event.logger.EventFlusher介面的例項
      • managementService.getEventLogEntries(startLogNr, size) 可以獲取Actviti的EventLogEntryEntity例項
  • 可以使用大資料的NoSQL儲存: MongoDb,Elastic Search等等來儲存JSON。
  • 使用的類是可插拔的: org.activiti.engine.impl.event.logger.EventLogger/EventFlusher和很多EventHandler
  • 可以切換成自定義應用場景: 不在資料庫中儲存JSON,而是放到佇列或大資料儲存中
  • 注意:
    • 事件日誌機制是Activiti傳統歷史管理器的附加品
    • 雖然所有資料都在資料庫表中,但是並沒有為查詢優化,不容易獲取
    • 真實的使用場景:
      • 審計跟蹤
      • 將事件日誌資料放到大資料儲存中

相關文章