1. 業務背景
項目開發過程中如果均是簡單的數據請求與返回,那麼方法調用和業務邏輯是最容易處理的,根據入參返回數據即可,數據的生命周期始於請求,終於數據返回,沒有其他。
倘若特定需求場景需要多個接口協作完成一件事,數據流轉存在多路由,業務邏輯處理將會呈現複雜化,樸素的數據流控制方式就是定義數據中間態通過硬編碼形式來影響數據流向,這種設計在複雜度不深的情況下總是很容易實現,開發成本和溝通成本較低,也不失為一種非常有效的開發設計方式。而隨著業務場景複雜化,流程變更頻繁,開發人員會在之前得益的簡單設計上發現維護和可拓展性極差,甚至陷入流程泥潭中難以自救,最直接的表現就是接口交互定製化,所有的交互看不到任何業務或故事主線,所有的服務交互都需要最原始的那些開發人員的文檔、
注釋甚至「言傳身教」的指導才可以洞察複雜業務的其中一二,這是軟體開發中的技術負債和不完善,我們急需一個可以引導完整業務流程的體系或者框架來引導服務交互,來驅動業務數據流轉,對數據的出生、中轉、停留及最終消亡進行有效控制和監管,讓服務有源可溯,有序可遵。關於以上概述都是為了引申出下面項目實踐的利器,工作流。
數據流轉依賴硬編碼,面向接口交互,沒有統一司令塔服務進行調度面向業務數據設計,複雜邏輯的業務中,數據流故事主線不清晰,無法監控也無從溯源
2.技術調研
JBPM vs Activiti選型對比
關於工作流開源框架,一般有JBPM和Activiti,簡單檢索了下兩者對比如下:
Activiti持久層通過MyBatis實現、與Spring融合支持事務,與當前項目技術背景較為符合,且上手較為便捷,參考資料廣泛,學習成本較低,加上之前個人項目運用過Activiti前身,對PVM設計模式有一定了解,最終決定採用Activiti作為工作流來進行開發。
Activiti工作流特點
關於Activiti工作流的具體內容這裡不做贅述,本文的核心放在Activiti工作流與業務結合的實踐,下面是Activiti工作流的一些特點:
數據持久化支持鏈式API編程風格,所有的編程參與對象都可通過ProcessEngine獲取到支持流程設計器。可以結合IDEA中的ActiBpm等插件進行可視化流程設計,它最終轉換的是bpm文件,是一個類xml的流程配置文件原生支持Spring分離運行時與歷史數據Activiti是基於單庫單表的持久化3.流程設計
目前負責的項目是一個關於用戶認證相關的業務,簡單描述認證業務流程如下:
工作流起始 工作流的開始和結束,是整個工作流程的起點和重點工作流節點 工作流的核心節點和銜接,每個節點是故事主線的主要構成部分,代表一個聚合的業務邏輯,每個節點根據預定義走向進行數據驅動工作流路由 工作流路由分發,根據前置數據來進行工作流走向的決策,從而影響後續節點的流轉數據中間態 除了開始、結束兩個節點,被虛線包括的部分都是數據的中間態,無論業務數據和工作流數據,此時呈現的最大特點就是數據的不穩定性,業務處理中隨時都可能根據外部業務驅動產生數據和業務流程的向下繼續、向上回溯、分叉決策等,也是工作流中最活躍的部分4.架構設計
Service業務邏輯層
Activiti Service 封裝了工作流基礎API,這裡主要用到Activiti的RepositoryService、RuntimeService、TaskService三個服務類,RepositoryService提供了服務流程部署功能,RuntimeService提供了運行時服務這裡主要涉及任務啟動,TaskService提供了任務完成、任務取消等流程驅動的方法支持。這裡是基於Activiti原生API進行了定製化任務支持,類似一個門面服務,主要是用來融合Activiti原生服務和實際項目業務需求的APIWorkFlowAspect 通過Aop切面將工作流邏輯進行抽離,保證與業務層具體邏輯方法的隔離實現解耦;調用底層Activiti Service是提供的服務接口完成工作流驅動@WorkFlowHandle 通過Spel解析Annotation註解形參,支持Map類型複雜數據結構傳參,結合Aop進行實參映射綁定,解決切面層與業務層參數傳遞問題;通過@Repeatable支持業務方法的重複註解,為工作流作用業務方法的靈活配置的可擴展提供支持Dao數據持久層
水平拆分 根據業務數據的庫表設計進行工作流持久化改造,按照用戶userId進行工作流數據水平拆分事務支持 由於切分鍵一致,使得工作流數據與業務數據分離的同時能夠支持資料庫事務,保證數據完整一致性,減少開發複雜度5.項目實戰
5.1 maven配置
目前Activiti已進入7.0.0+,翻閱了大量網上資料,該版本輸出的時間較少,大部分還集中在5或6,這裡基於可用可操作的思想選用了介紹參考資料詳實的5.22.0進行開發,每個大版本變更差異較大,其他版本根據實踐需要進行升級或取捨
<activiti.version>5.22.0</activiti.version>
<!-- activiti -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-engine</artifactId>
<version>${activiti.version}</version>
</dependency>
<artifactId>activiti-spring</artifactId>
5.2 持久化改造
分庫分表
Activiti默認是單庫單表的,而我們現有項目是分庫分表的,路由策略是根據用戶ID進行水平切分的。
對Activiti的持久化存儲進行分庫分表改造基於以下兩點考慮:
業務數據分庫分表,Activiti工作流數據的流轉肯定是要和業務數據做聯動的,按照同樣語義即用戶ID進行水平切換路由可以輕鬆實現資料庫事務,減少實現數據一致性帶來的設計問題。業務表做了分庫分表的原因就是用戶體量大,持久化數據龐大,儘管讀多寫少且寫入量單庫單表在一段時間內不會對業務性能產生絲毫影響,但是為了擴展性更好,也為了規避後續變更難度,決定對Activiti工作流數據持久化也進行水平拆分。欄位及索引長度適配
為了保證數據完整性,Activiti默認創建表存在大量的外鍵約束,在生產環境下可以根據自身開發需要對這些外鍵進行去除,從而提高庫表查詢和處理性能。關於數據完整性可以通過應用程式層面進行保證和處理。Activiti的庫表設計都是基於可用性和普遍適配性進行設計的,個別欄位如存在varchar(4000)來記錄異常信息,根據自身業務存儲及性能考慮進行了部分欄位基於可用性的縮減;還有部分表存在大量的聯合索引,由於欄位長度過長導致聯合索引存儲大於736位元組,這是MySQL官方推薦的索引最大值,超過該值可能會產生性能問題,項目中沒有對MySQL默認限制進行修改,而是根據項目情況縮減了聯合索引欄位的長度。5.3 部署bpmn更新問題
在實際項目環境下,沒有bpmn流程調整我們是不需要頻繁進行bpmn流程部署的,每次新流程部署都會更新刷下流程ID、實例ID等,而且數據也會產生變更調整,而我們的需求是在需要更新的時候更新,不需要更新的時候覆用即可。因此在服務啟動時,我這裡通過時間戳作為版本號,每次啟動Spring服務時進行bpmn部署檢查,如果應用中的版本號未進行變更則不刷新Activiti部署信息保持之前部署快照,否則創建並部署新bpmn流程進行部署創建,更新部署快照信息,下面放一段demo代碼,由於項目是分庫分表的,每次啟動服務會輪詢每個分庫的deploy信息情況。
/**
* @author: guanjian
* @date: 2020/11/24 19:48
* @description: Activiti部署
*/
@Component("activitiDeployServlet")
public class ActivitiDeployServlet {
private final static Logger LOGGER = LoggerFactory.getLogger(ActivitiDeployServlet.class);
@Autowired
private RepositoryService repositoryService;
private BaseDBRouter dbRouter;
* BPMN流程定義部署
*
* @desc <p>
* 1、根據分庫數量(dbRouter.getDbNumber)進行bpmn流程定義部署
* 2、根據WORK_FLOW_DEPLOY進行部署,每次部署進行防重判斷,變更WORK_FLOW_DEPLOY則重新部署即啟用新流程
* WORK_FLOW_DEPLOY一致則不進行重新部署,復用原流程
* <p>
@PostConstruct
public void deploy() {
Optional.ofNullable(dbRouter.getDbKeyArray())
.orElse(Lists.newArrayList())
.forEach(db->{
try {
DBContextHolder.setDBKeyIndex(db);
handleDeployBpmn();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("check deploy bpmn error.");
} finally {
DBContextHolder.clearDBKeyIndex();
}
});
private Deployment queryBpmn() {
return repositoryService.createDeploymentQuery()
.deploymentName(ActivitiConstants.Deployment.WORK_FLOW_DEPLOY)
.singleResult();
private void handleDeployBpmn() {
Deployment deployment = queryBpmn();
if (Optional.ofNullable(deployment).isPresent()) {
LOGGER.info("{} bpmn exists.", DBContextHolder.getDBKeyIndex());
} else {
createBpmn();
private void createBpmn() {
repositoryService.createDeployment()
.disableSchemaValidation()
.name(ActivitiConstants.Deployment.WORK_FLOW_DEPLOY)
.addClasspathResource(ActivitiConstants.Deployment.BPMN_RESOURCE_PATH)
.deploy();
LOGGER.info("{} bpmn created.", DBContextHolder.getDBKeyIndex());
5.4 Activiti核心服務
Activiti主要有以下幾個核心服務,我們這裡最重要的是使用到了RepositoryService、RuntimeService、TaskService,RepositoryService用來進行服務部署,RuntimeService用來啟動任務管理任務實例,TaskService用來進行任務的查詢、流轉(完成、取消)等操作。
5.5 整合Spring
與Spring整合後,可以通過Spring來操作配置Activiti
<!-- Activiti processEngineConfiguration -->
<bean id="processEngineConfiguration">
<property name="dataSource" ref="routerTargetDataSource"/>
<property name="transactionManager" ref="routerTransactionManager"/>
<!--
flase:默認值。activiti在啟動時,會對比資料庫表中保存的版本,如果沒有表或者版本不匹配,將拋出異常。(生產環境常用)
true:activiti會對資料庫中所有表進行更新操作。如果表不存在,則自動創建。(開發時常用)
create_drop:在activiti啟動時創建表,在關閉時刪除表(必須手動關閉引擎,才能刪除表)。(單元測試常用)
drop-create:在activiti啟動時刪除原來的舊錶,然後在創建新表(不需要手動關閉引擎)。
-->
<property name="databaseSchemaUpdate" value="true"/>
<property name="history" value="none"/>
</bean>
<!-- Activiti processEngine -->
<bean id="processEngine">
<property name="processEngineConfiguration" ref="processEngineConfiguration"/>
<!-- Activiti Service -->
<bean id="repositoryService" factory-bean="processEngine" factory-method="getRepositoryService"/>
<bean id="runtimeService" factory-bean="processEngine" factory-method="getRuntimeService"/>
<bean id="taskService" factory-bean="processEngine" factory-method="getTaskService"/>
<bean id="historyService" factory-bean="processEngine" factory-method="getHistoryService"/>
<bean id="managementService" factory-bean="processEngine" factory-method="getManagementService"/>
</beans>
5.6 工作流常量定義
private final static String BPMN_DEPLOY_PATH_TEMPLATE = "bpmn/*.bpmn";
* 部署資源
public static class Deployment {
//BPMN流程定義名稱
public final static String WORK_FLOW_KEY = "xxx_workflow";
//BPMN流程版本號(yyyyMMddHHmm)
public final static String WORK_FLOW_VERSION = "v202012031347";
//BPMN流程部署名稱(名稱_版本號)
//每次部署對該變量進行防重判斷,版本號變更則部署新流程進行新部署
public final static String WORK_FLOW_DEPLOY = WORK_FLOW_KEY + Constants.Symbol.LINE + WORK_FLOW_VERSION;
//BPMN文件讀取路徑
public final static String BPMN_RESOURCE_PATH = BPMN_DEPLOY_PATH_TEMPLATE.replace("*", WORK_FLOW_KEY);
* 工作流節點定義
public enum WorkFlowNodeEnum implements SingleItem<String> {
* 起始
//開始
START("START"),
//結束
END("END"),
* 普通節點
NODE_1("NODE_1"),
NODE_2("NODE_2"),
NODE_3("NODE_3"),
* 排他網關
//數據源
XOR_DATA_SOURCE("XOR_dataSource"),
//反饋結果
XOR_CHSI_RESULT("XOR_chsiResult"),
;
private String key;
WorkFlowNodeEnum(String key) {
this.key = key;
@Override
public String getKey() {
return key;
public void setKey(String key) {
* 流程變量
public static class Variables {
//用戶ID
public final static String USER_ID = "userId";
//業務ID
public final static String BIZ_ID = "bizId";
public final static String DATASOURCE = "dataSource";
public final static String RESULT= "result";
* 任務取消原因
public static class CancelReason {
//強制作廢流程
public final static FINISH_FORCE_INVALID = "FINISH_FORCE_INVALID";
* 任務操作枚舉
public enum TaskOperateEnum {
//任務啟動
START,
//任務取消
CANCEL,
//任務完成
COMPLETE,
5.7 工作流註解@WorkFlowHandle & @WorkFlowHandles
@WorkFlowHandle是支持工作流操作的註解,@WorkFlowHandles則是藉助JDK1.8的@Repeatable特性支持重複註解,使得@WorkFlowHandle可以重複作用在業務Service方法上進行工作流操作。
@WorkFlowHandle的定義如下:
這裡的註解欄位定義可以根據具體業務進行抽象和具體,以上僅供參考。
* @date: 2020/12/02 17:14
* @description: 工作流處理
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(WorkFlowHandles.class)
public @interface WorkFlowHandle {
* 任務委託人
String assignee() default "";
* 業務ID
String bizId() default "";
* 環境變量
String variables() default "";
* 任務操作
ActivitiConstants.TaskOperateEnum taskOperate();
* 取消原因
String cancelReason() default "";
* 任務節點
ActivitiConstants.WorkFlowNodeEnum node() default ActivitiConstants.WorkFlowNodeEnum.END;
public @interface WorkFlowHandles {
WorkFlowHandle[] value();
5.8 工作流切面 WorkFlowAspect
WorkFlowAspect是以切面形式存在的工作流業務,把涉及工作流相關的元素數據抽離到切面中進行集中處理,通過註冊與業務邏輯進行融合,代碼隔離減少了邏輯耦合和混雜,通過以下腳手架的拼接完成了該功能的實現,匯總如下:
5.9 工作流與業務Service融合
@WorkFlowHandle(taskOperate = ActivitiConstants.TaskOperateEnum.START,
assignee = "#req.userId",
bizId = "#req.uuid",
variables = "{userId:'#req.userId',dataSource:'#req.dataSource'}"
)
@WorkFlowHandle(taskOperate = ActivitiConstants.TaskOperateEnum.COMPLETE,
node = ActivitiConstants.WorkFlowNodeEnum.NODE_1
@WorkFlowHandle(taskOperate = ActivitiConstants.TaskOperateEnum.CANCEL,
assignee = "#req.userId"
public Result bizMethod(Req req) {
//業務操作
//do something
return Result.success();
6. 其他
表刪除順序
由於存在外鍵,表刪除需要按照以下順序進行,第一遍沒刪除掉,可以再來一遍就乾淨了,測試環境使用的備記下
DROP TABLE IF EXISTS `ACT_RU_VARIABLE`;
DROP TABLE IF EXISTS `ACT_RU_EXECUTION`;
DROP TABLE IF EXISTS `ACT_RE_PROCDEF`;
DROP TABLE IF EXISTS `ACT_ID_GROUP`;
DROP TABLE IF EXISTS `ACT_GE_BYTEARRAY`;
DROP TABLE IF EXISTS `ACT_RE_DEPLOYMENT`;
DROP TABLE IF EXISTS `ACT_EVT_LOG`;
DROP TABLE IF EXISTS `ACT_GE_PROPERTY`;
DROP TABLE IF EXISTS `ACT_HI_ACTINST`;
DROP TABLE IF EXISTS `ACT_HI_ATTACHMENT`;
DROP TABLE IF EXISTS `ACT_HI_COMMENT`;
DROP TABLE IF EXISTS `ACT_HI_DETAIL`;
DROP TABLE IF EXISTS `ACT_HI_IDENTITYLINK`;
DROP TABLE IF EXISTS `ACT_HI_PROCINST`;
DROP TABLE IF EXISTS `ACT_HI_TASKINST`;
DROP TABLE IF EXISTS `ACT_HI_VARINST`;
DROP TABLE IF EXISTS `ACT_ID_INFO`;
DROP TABLE IF EXISTS `ACT_ID_MEMBERSHIP`;
DROP TABLE IF EXISTS `ACT_ID_USER`;
DROP TABLE IF EXISTS `ACT_PROCDEF_INFO`;
DROP TABLE IF EXISTS `ACT_RE_MODEL`;
DROP TABLE IF EXISTS `ACT_RU_EVENT_SUBSCR`;
DROP TABLE IF EXISTS `ACT_RU_IDENTITYLINK`;
DROP TABLE IF EXISTS `ACT_RU_JOB`;
DROP TABLE IF EXISTS `ACT_RU_TASK`;