一、事故現場

某年某月的一個凌晨,運維部打來電話:
“領導審批節點找不到人,流程卡住了!”

打開 BPMN 一看,差點原地去世——

<flowable:userCandidateGroups value="{SpecialtyResolver.getSpecialtyArgs('ZH','director')}"/>

少寫了一個 $,Flowable 把它當成普通字符串,結果解析不到任何候選人,任務孤零零地躺在 ACT_RU_TASK,無人可簽收。

更絕望的是:

  • 流程實例已經跑到一半;
  • 不能重新部署(歷史數據要對應舊定義);
  • 不能手動改數據庫(生產庫動刀需三級審批)。

於是,有了下面這段“熱補丁”代碼。


二、解決思路(不碰數據庫、不重啓流程)

Flowable 在「完成任務」與「創建下一任務」之間,會拋出 TASK_CREATE 事件;
我們只需要:

  1. 攔截下一節點;
  2. 判斷是不是“寫死表達式”;
  3. 現場補回 $ 重新計算;
  4. 把候選人寫回任務。

全程在 Java 代碼裏完成,零 SQL、零 downtime。


三、核心代碼(可直接複製)

/**
 * 生產熱修復:已發實例節點候選人表達式寫死
 * 適用 Flowable 6.x 任意版本
 */
private void fixFlow(Task task) {
    // 1. 拿到下一節點 execution
    DelegateExecution nextExec = (DelegateExecution) runtimeService
            .createExecutionQuery()
            .executionId(task.getExecutionId())
            .singleResult();
    if (nextExec == null) return;

    // 2. 取 BPMN 模型
    BpmnModel bpmnModel = repositoryService.getBpmnModel(task.getProcessDefinitionId());
    String activityId = nextExec.getCurrentActivityId();
    FlowElement element = bpmnModel.getMainProcess().getFlowElement(activityId);

    // 3. 只修 UserTask
    if (!(element instanceof UserTask)) return;
    UserTask userTask = (UserTask) element;

    // 4. 判斷是不是“寫死”的表達式
    String raw = userTask.getCandidateGroups();
    if (raw == null || !raw.startsWith("{SpecialtyResolver")) return;

    // 5. 補回 $ 重新計算
    String el = "${" + raw.substring(1);
    ExpressionManager mgr = CommandContextUtil
            .getProcessEngineConfiguration()
            .getExpressionManager();
    Object result = mgr.createExpression(el).getValue(nextExec);

    // 6. 寫回候選人(任務已創建,直接 add)
    Task nextTask = taskService.createTaskQuery()
            .executionId(nextExec.getId())
            .singleResult();
    if (nextTask == null) return;

    if (result instanceof Collection) {
        ((Collection<?>) result).forEach(u -> 
            taskService.addCandidateUser(nextTask.getId(), u.toString()));
    } else if (result instanceof String) {
        Arrays.stream(((String) result).split(","))
              .map(String::trim)
              .forEach(u -> taskService.addCandidateUser(nextTask.getId(), u));
    }
    log.info("熱修復完成,任務 {} 已寫入候選人", nextTask.getId());
}

調用時機:
completeTask() 末尾加一行 fixFlow(task); 即可,事務內執行,保證原子性。


四、效果演示

場景

修復前

修復後

舊實例

任務無候選人,流程卡住

候選人立即出現,可正常簽收

新實例

同樣生效,無需重新部署

零 downtime 上線


五、小結

  1. 表達式寫錯 ≠ 世界末日,事件驅動可以現場打補丁;
  2. 不碰數據庫、不重啓服務,生產庫也能安心改
  3. 代碼只改一次,歷史實例 + 未來實例一起受益。

下次再手滑,記得先把 $ 鍵焊死!