Flowable CMMN 实战:从流程驱动到案例驱动的架构演进

去年我们团队接手了一个复杂的保险理赔系统,其中有一个"巨无霸"理赔流程------包含了37个审批节点、15条分支路径和23种异常处理。每次业务规则变更,都需要开发团队修改BPMN流程图,测试团队进行全流程回归,发布时间从1周延长到1个月。直到我们发现了CMMN这个"隐藏的宝石"。

一、噩梦般的保险理赔流程

最初的理赔系统采用传统的BPMN流程,长这样:

XML 复制代码
<!-- 传统的BPMN理赔流程 - 部分代码 -->
<process id="claimProcess">
    <!-- 37个用户任务节点 -->
    <userTask id="receiveClaim" name="接收理赔申请"/>
    <userTask id="validateDocuments" name="验证资料完整性"/>
    <userTask id="assessDamage" name="损失评估"/>
    <!-- ... 中间省略34个节点 ... -->
    <userTask id="finalApproval" name="最终审批"/>
    
    <!-- 15条分支路径 -->
    <exclusiveGateway id="damageAssessmentGateway">
        <sequenceFlow sourceRef="assessDamage" targetRef="smallClaim">
            <conditionExpression>x${amount &lt; 5000}</conditionExpression>
        </sequenceFlow>
        <sequenceFlow sourceRef="assessDamage" targetRef="mediumClaim">
            <conditionExpression>x${amount &gt;= 5000 &amp;&amp; amount &lt; 50000}</conditionExpression>
        </sequenceFlow>
        <sequenceFlow sourceRef="assessDamage" targetRef="largeClaim">
            <conditionExpression>x${amount &gt;= 50000}</conditionExpression>
        </sequenceFlow>
    </exclusiveGateway>
    
    <!-- 异常处理流程 -->
    <boundaryEvent id="timeoutEvent" attachedToRef="assessDamage">
        <timerEventDefinition>
            <timeDuration>PT24H</timeDuration>
        </timerEventDefinition>
    </boundaryEvent>
</process>

痛点分析​:

  1. 刚性流程:每个理赔案件必须按照预定路径执行
  2. 变更成本高:增加一个新的资料类型需要修改整个流程
  3. 无法应对异常:遇到特殊情况需要"走特殊流程"时系统无法支持
  4. 监控困难:案件卡在哪个环节?为什么卡住?很难追踪

业务总监的原话是:"我们的理赔处理像在走迷宫,明明有些案件很简单,却要跟复杂案件走一样的流程。"

二、技术选型:为什么是CMMN?

备选方案对比

我们考虑了三种方案:

方案A:继续优化BPMN流程

  • 优点:团队熟悉,开发速度快
  • 缺点:无法解决根本的流程刚性問題

方案B:自定义状态机

java 复制代码
// 自研状态机方案
public class ClaimStateMachine {
    private Map<ClaimState, List<ClaimEvent>> transitions = new HashMap<>();
    
    public void addTransition(ClaimState from, ClaimEvent event, ClaimState to) {
        // 自定义状态转移逻辑
    }
    
    // 问题:需要自己实现持久化、回退、监控等全套功能
}

方案C:Flowable CMMN

  • 基于案例管理模型,适合非结构化流程
  • 支持动态任务激活和完成
  • 内置持久化和监控能力

决策关键点​:

java 复制代码
// CMMN的核心概念验证
CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder()
    .caseDefinitionKey("insuranceClaim")
    .variable("claimType", "auto")  // 车辆理赔
    .variable("amount", 15000)     // 金额1.5万
    .start();

// 系统会根据案件情况动态激活不同的任务
// 而不是按照预定路径执行

三、架构重构:从流程驱动到案例驱动

第一阶段:CMMN案例模型设计

XML 复制代码
<!-- insurance-claim.cmmn -->
<case id="insuranceClaim" name="保险理赔案例">
    
    <!-- 案例参数 -->
    <casePlanModel id="casePlanModel">
        
        <!-- 第一阶段:必须完成的任务 -->
        <planItem id="receiveClaim" definitionRef="receiveClaimTask"/>
        <planItem id="validateDocuments" definitionRef="validateDocumentsTask"/>
        
        <!-- 条件性任务:只有金额大于1万才需要评估 -->
        <planItem id="assessDamage" definitionRef="assessDamageTask">
            <entryCriterion sentryRef="amountOver10kSentry"/>
        </PlanItem>
        
        <!-- 并行任务:调查和评估可以同时进行 -->
        <planItem id="investigate" definitionRef="investigateTask"/>
        <planItem id="evaluate" definitionRef="evaluateTask"/>
        
        <!-- 哨兵(Sentry):定义任务激活条件 -->
        <sentry id="amountOver10kSentry">
            <planItemOnPart sourceRef="validateDocuments">
                <standardEvent>complete</standardEvent>
            </planItemOnPart>
            <condition>${amount > 10000}</condition>
        </sentry>
        
    </casePlanModel>
    
    <!-- 任务定义 -->
    <humanTask id="receiveClaimTask" name="接收理赔申请"/>
    <humanTask id="validateDocumentsTask" name="验证资料完整性"/>
    <humanTask id="assessDamageTask" name="损失评估"/>
</case>

第二阶段:动态任务管理

java 复制代码
@Service
public class DynamicClaimService {
    
    @Autowired
    private CmmnRuntimeService cmmnRuntimeService;
    
    @Autowired
    private CmmnTaskService cmmnTaskService;
    
    // 根据案件情况动态添加任务
    public void addAdditionalTask(String caseInstanceId, String taskType) {
        // 动态创建计划项
        PlanItemInstance planItemInstance = cmmnRuntimeService
            .createPlanItemInstanceBuilder()
            .caseInstanceId(caseInstanceId)
            .planItemDefinitionId(taskType)
            .add();
        
        // 手动启动任务
        cmmnRuntimeService.startPlanItemInstance(planItemInstance.getId());
    }
    
    // 处理复杂理赔案件
    public void handleComplexClaim(String caseInstanceId, Claim claim) {
        // 根据案件复杂度动态添加任务
        if (claim.hasSuspiciousPattern()) {
            addAdditionalTask(caseInstanceId, "fraudInvestigation");
        }
        
        if (claim.requiresExpertOpinion()) {
            addAdditionalTask(caseInstanceId, "expertReview");
        }
        
        // 任务可以并行执行,不需要等待前序任务完成
    }
}

第三阶段:事件驱动的案件处理

java 复制代码
@Component
public class ClaimEventListener {
    
    @EventListener
    public void handleTaskCompleted(CmmnTaskCompletedEvent event) {
        String caseInstanceId = event.getCaseInstanceId();
        String taskId = event.getTaskId();
        
        // 任务完成时自动触发后续动作
        if ("validateDocuments".equals(taskId)) {
            // 根据验证结果决定下一步
            Boolean documentsValid = (Boolean) event.getVariable("documentsValid");
            if (Boolean.TRUE.equals(documentsValid)) {
                // 自动激活评估任务
                activateAssessmentTasks(caseInstanceId);
            } else {
                // 请求补充材料
                requestAdditionalDocuments(caseInstanceId);
            }
        }
    }
    
    private void activateAssessmentTasks(String caseInstanceId) {
        // 查询案件信息
        CaseInstance caseInstance = cmmnRuntimeService
            .createCaseInstanceQuery()
            .caseInstanceId(caseInstanceId)
            .singleResult();
        
        Double amount = (Double) caseInstance.getVariable("amount");
        
        if (amount > 50000) {
            // 大额案件需要额外审批
            cmmnRuntimeService.triggerPlanItemInstance("seniorApproval");
        }
    }
}

四、核心技术决策的深度思考

决策1:何时用BPMN,何时用CMMN?

我们制定了明确的选择标准:

java 复制代码
public class ProcessSelectionFramework {
    
    // 适合BPMN的场景
    public boolean isSuitableForBPMN(ProcessRequirement req) {
        return req.hasStrictSequence() ||           // 严格的顺序执行
               req.hasWellDefinedOutcomes() ||      // 明确的结果
               req.isRepeatableProcess();           // 可重复的流程
    }
    
    // 适合CMMN的场景  
    public boolean isSuitableForCMMN(CaseRequirement req) {
        return req.hasUnpredictablePath() ||        // 不可预测的路径
               req.requiresAdaptation() ||          // 需要适应性
               req.hasKnowledgeWorkNature();       // 知识工作性质
    }
    
    // 实际案例:保险理赔适合CMMN
    public void evaluateInsuranceClaim() {
        CaseRequirement claimReq = new CaseRequirement();
        claimReq.setUnpredictablePath(true);  // 每个案件路径不同
        claimReq.setAdaptationRequired(true); // 需要根据情况调整
        claimReq.setKnowledgeWork(true);       // 依赖核保员经验
        
        assert isSuitableForCMMN(claimReq); // 返回true
    }
}

决策2:任务粒度设计

错误示范​:任务粒度过细

java 复制代码
<!-- 反例:过度拆分的任务 -->
<humanTask id="validateDriverLicense" name="验证驾驶证"/>
<humanTask id="validateInsuranceCard" name="验证保险卡"/>
<humanTask id="validateAccidentReport" name="验证事故报告"/>
<!-- 用户需要点击十几次才能完成资料验证 -->

正确做法​:合理的任务聚合

java 复制代码
<!-- 正例:有意义的任务单元 -->
<humanTask id="validateDocuments" name="验证理赔资料">
    <extensionElements>
        <!-- 在表单中定义需要验证的所有资料 -->
        <flowable:formProperty id="documents" 
            name="资料验证" type="document-list"/>
    </extensionElements>
</humanTask>

决策3:哨兵条件的设计哲学

java 复制代码
<!-- 简单的条件哨兵 -->
<sentry id="basicCondition">
    <planItemOnPart sourceRef="previousTask">
        <standardEvent>complete</standardEvent>
    </planItemOnPart>
    <condition>${amount > 10000}</condition>
</sentry>

<!-- 复杂条件组合 -->
<sentry id="complexCondition">
    <planItemOnPart sourceRef="taskA">
        <standardEvent>complete</standardEvent>
    </planItemOnPart>
    <planItemOnPart sourceRef="taskB"> 
        <standardEvent>complete</standardEvent>
    </planItemOnPart>
    <condition>
        ${taskA.output == 'APPROVED' && taskB.output == 'APPROVED'}
    </condition>
</sentry>

五、性能优化与实战数据

性能对比数据

指标 BPMN方案 CMMN方案 提升幅度
平均处理时间 5.2天 3.1天 40%
流程变更发布时间 3周 3天 86%
异常案件处理效率 显著提升
用户满意度 6.5/10 8.7/10 34%

数据库优化策略

sql 复制代码
-- CMMN特有的大表索引优化
CREATE INDEX idx_cmmn_ru_planitem_case ON act_cmmn_ru_plan_item_inst(CASE_INST_ID_);
CREATE INDEX idx_cmmn_ru_sentry_case ON act_cmmn_ru_sentry_part_inst(CASE_INST_ID_);
CREATE INDEX idx_cmmn_ru_variable_case ON act_cmmn_ru_variable(CASE_INST_ID_);

-- 历史数据归档策略
CREATE EVENT archive_cmmn_histories
ON SCHEDULE EVERY 1 DAY
DO BEGIN
    -- 归档6个月前的已完成案例
    INSERT INTO act_cmmn_hi_case_inst_archive
    SELECT * FROM act_cmmn_hi_case_inst 
    WHERE END_TIME_ < DATE_SUB(NOW(), INTERVAL 180 DAY);
    
    DELETE FROM act_cmmn_hi_case_inst 
    WHERE END_TIME_ < DATE_SUB(NOW(), INTERVAL 180 DAY);
END;

六、踩坑记录与解决方案

坑1:哨兵条件的事务边界

java 复制代码
// 错误示例:在事务外修改案例变量
@Service
public class ProblematicCaseService {
    
    @Transactional
    public void completeTask(String taskId) {
        // 完成任务
        cmmnTaskService.complete(taskId);
        
        // 在事务外修改变量(危险!)
        new Thread(() -> {
            // 异步更新可能破坏哨兵条件的一致性
            updateExternalData(caseInstanceId);
        }).start();
    }
}

// 解决方案:使用案例事件监听器
@Component
public class SafeCaseService {
    
    @EventListener
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleAfterCommit(CaseEvent event) {
        // 事务提交后安全地执行后续操作
        updateExternalData(event.getCaseInstanceId());
    }
}

坑2:计划项的生命周期管理

java 复制代码
// 正确管理动态计划项
public class PlanItemLifecycleManager {
    
    public void manageDynamicPlanItem(String caseInstanceId) {
        // 1. 创建计划项
        PlanItemInstance planItem = cmmnRuntimeService
            .createPlanItemInstanceBuilder()
            .caseInstanceId(caseInstanceId)
            .planItemDefinitionId("dynamicTask")
            .add();
        
        // 2. 明确的状态管理
        cmmnRuntimeService.startPlanItemInstance(planItem.getId());
        
        // 3. 完成后清理
        cmmnRuntimeService.completePlanItemInstance(planItem.getId());
        
        // 避免计划项泄漏和状态不一致
    }
}

七、CMMN适用场景评估

适合CMMN的场景

java 复制代码
// 场景1:知识工作流程
public class KnowledgeWorkflow {
    // 保险理赔、医疗诊断、法律案件
    // 需要专业人员根据情况决定下一步行动
}

// 场景2:动态适应性流程  
public class AdaptiveProcess {
    // 客户投诉处理、突发事件响应
    // 路径无法预先完全定义
}

// 场景3:多维度协调工作
public class MultiDimensionalCoordination {
    // 项目管理系统、产品研发流程
    // 多个团队并行工作,需要动态协调
}

不适合CMMN的场景

java 复制代码
// 场景1:标准化操作流程
public class StandardizedProcess {
    // 订单处理、数据ETL流程
    // 步骤固定,不需要动态调整
}

// 场景2:简单线性流程
public class SimpleLinearProcess {
    // 用户注册、密码重置
    // 用CMMN是过度设计
}

八、架构师的经验总结

1. 案例设计原则

java 复制代码
// 原则1:案例阶段划分
public class CaseStageDesign {
    public void designCaseStages() {
        // 明确阶段划分:接收→评估→决策→结算
        // 每个阶段有明确的入口和出口标准
    }
}

// 原则2:任务自治性
public class TaskAutonomy {
    // 每个任务应该是自包含的
    // 尽量减少任务间的硬性依赖
}

// 原则3:异常处理设计
public class ExceptionHandlingDesign {
    // 为每个阶段设计异常处理机制
    // 支持案例的"优雅降级"
}

2. 团队协作模式

成功要素​:

  • 业务分析师:负责案例阶段和哨兵条件设计
  • 开发工程师:实现任务处理逻辑和集成点
  • 测试工程师:设计基于场景的测试用例

工具支持​:

  • CMMN模型设计器:可视化案例设计
  • 案例监控看板:实时跟踪案例状态
  • 哨兵条件验证工具:测试条件逻辑

九、写在最后

这次CMMN的实施让我们深刻理解了案例驱动流程驱动 的本质区别。最大的收获不是技术层面的,而是思维模式的转变------从"如何定义流程"到"如何支持知识工作"。

最重要的经验​:CMMN不是BPMN的替代品,而是解决另一类问题的工具。选择的关键在于识别业务的本质:是重复性的标准操作,还是需要灵活应对的知识工作。

对于考虑CMMN的团队,我的建议是:先从一个小而具体的案例开始,体验动态任务管理的威力,再逐步推广到更复杂的场景。CMMN的学习曲线比BPMN陡峭,但一旦掌握,它能解决传统工作流无法应对的复杂问题。

技术选型的真谛​:不是追求最新最热的技术,而是为具体问题找到最合适的解决方案。CMMN就是我们为非结构化知识工作找到的那个"合适方案"。

相关推荐
北友舰长5 分钟前
基于Springboot+thymeleaf图书管理系统的设计与实现【Java毕业设计·安装调试·代码讲解】
java·spring boot·mysql·课程设计·图书管理·b/s·图书
陈文锦丫7 小时前
MQ的学习
java·开发语言
乌暮7 小时前
JavaEE初阶---线程安全问题
java·java-ee
爱笑的眼睛117 小时前
GraphQL:从数据查询到应用架构的范式演进
java·人工智能·python·ai
Seven978 小时前
剑指offer-52、正则表达式匹配
java
代码or搬砖8 小时前
RBAC(权限认证)小例子
java·数据库·spring boot
青蛙大侠公主8 小时前
Thread及其相关类
java·开发语言
Coder_Boy_8 小时前
DDD从0到企业级:迭代式学习 (共17章)之 四
java·人工智能·驱动开发·学习
2301_768350238 小时前
MySQL为什么选择InnoDB作为存储引擎
java·数据库·mysql
派大鑫wink8 小时前
【Java 学习日记】开篇:以日记为舟,渡 Java 进阶之海
java·笔记·程序人生·学习方法