审批流程上线后通常还会持续变化:增加复核节点、调整候选组、修改网关条件,或者为表单增加新的校验字段。直接部署新版 BPMN 文件看似简单,却容易产生三个问题:
- 新提交的单据究竟进入哪个版本,取决于调用方如何查询和启动流程;
- 已经运行到一半的实例不能被当作新版实例处理,否则节点结构和业务数据可能不匹配;
- 回滚应用代码时,如果流程定义没有同步控制,旧代码可能启动一个它无法正确处理的新流程。
可靠的升级方案需要把“定义发布”“新实例切流”和“在途实例处理”拆成三个独立动作。核心原则是:流程版本由引擎管理,业务选择由应用明确记录,在途实例默认留在原定义中运行。
本文示例采用 Flowable 的 Java API。不同发行版或项目依赖可能在自动配置属性、数据库脚本管理方式上存在差异,具体配置名应以项目实际依赖的官方文档和源码为准,但版本隔离与冻结定义 ID 的设计不依赖某个特定小版本。
先理解三个标识
Flowable 部署 BPMN 后,会形成流程定义。理解以下标识是避免版本混乱的前提:
processDefinitionKey:来自 BPMN 中<process id="...">的逻辑名称,例如expenseApproval。同一流程的多个版本共享这个 key。processDefinitionId:某一次已部署定义的唯一标识。它对应确定的 key、版本和租户。processInstanceId:一次实际运行的流程实例标识。
按 key 启动流程通常意味着由引擎选择符合条件的定义;按 definition ID 启动则明确绑定某个已部署版本。实例创建后会关联其流程定义,后续部署同 key 的新版本,并不会自动把已有实例改成新版本。
因此,发布 v2 后无需强制迁移 v1 的在途实例。除非业务确实要求迁移,而且已经验证了活动节点映射、变量兼容性、历史审计和补偿策略,否则迁移本身比并行运行风险更高。
设计一张业务绑定表
不要只在运行时临时查询“最新版本”。应在业务数据库中保存每张单据实际选择的定义和实例:
CREATE TABLE biz_workflow_binding (
id BIGINT PRIMARY KEY,
business_type VARCHAR(64) NOT NULL,
business_key VARCHAR(128) NOT NULL,
process_definition_id VARCHAR(128) NOT NULL,
process_instance_id VARCHAR(128) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (business_type, business_key),
UNIQUE (process_instance_id)
);
business_type 用于区分费用、采购等流程,business_key 对应业务单据号。唯一约束用于阻止重复提交造成多个流程实例。它不能替代引擎状态判断,但可以成为业务侧的幂等边界。
如果业务表和 Flowable 表使用同一数据源且由同一 Spring 本地事务管理,启动实例和写入绑定记录可以放在一个事务中。若两者位于不同数据库,则不能假设普通 @Transactional 可以提供跨库原子性,此时应采用本地状态机、事务消息或可重试的补偿流程。
步骤一:部署新定义,但暂不切流
将 v2 BPMN 使用新的资源名保存,例如 processes/expense-v2.bpmn20.xml。BPMN 中的 process key 保持不变,节点 ID 则应谨慎修改,因为业务监听器、表单和报表可能引用它们。
@Service
public class WorkflowReleaseService {
private final RepositoryService repositoryService;
public WorkflowReleaseService(RepositoryService repositoryService) {
this.repositoryService = repositoryService;
}
public ProcessDefinition deploy(String resource, String releaseName) {
Deployment deployment = repositoryService.createDeployment()
.name(releaseName)
.addClasspathResource(resource)
.deploy();
return repositoryService.createProcessDefinitionQuery()
.deploymentId(deployment.getId())
.singleResult();
}
}
部署后先记录返回的 definition ID,不要立即让所有业务请求按 key 启动。发布检查至少包括:定义是否可查询、候选用户或候选组是否存在、表达式引用的变量是否有默认处理、服务任务委托类是否已经随应用发布。
生产环境不宜仅依赖应用启动时自动扫描 BPMN,因为多个实例同时启动可能让发布时点难以审计。更稳妥的方式是把部署做成受权限保护的发布动作,并记录操作人、制品校验值、deployment ID 和 definition ID。
步骤二:建立可控的版本路由
应用配置中保存当前允许创建新实例的 definition ID:
workflow:
routes:
expense: expenseApproval:7:8f3a-example
这里的值只是格式示意,实际 ID 必须使用部署后查询到的结果,不能从版本号自行拼接。绑定配置类:
@ConfigurationProperties(prefix = "workflow")
public record WorkflowProperties(Map<String, String> routes) {
}
启动服务始终按 definition ID 创建实例:
@Service
public class ExpenseWorkflowService {
private final RuntimeService runtimeService;
private final RepositoryService repositoryService;
private final WorkflowProperties properties;
private final WorkflowBindingRepository bindingRepository;
public ExpenseWorkflowService(
RuntimeService runtimeService,
RepositoryService repositoryService,
WorkflowProperties properties,
WorkflowBindingRepository bindingRepository) {
this.runtimeService = runtimeService;
this.repositoryService = repositoryService;
this.properties = properties;
this.bindingRepository = bindingRepository;
}
@Transactional
public String submit(String orderNo, long amountFen) {
String definitionId = properties.routes().get("expense");
if (definitionId == null) {
throw new IllegalStateException("未配置费用流程版本");
}
ProcessDefinition definition = repositoryService
.createProcessDefinitionQuery()
.processDefinitionId(definitionId)
.active()
.singleResult();
if (definition == null) {
throw new IllegalStateException("流程定义不存在或已暂停: " + definitionId);
}
Map<String, Object> variables = Map.of("amountFen", amountFen);
ProcessInstance instance = runtimeService.startProcessInstanceById(
definition.getId(), orderNo, variables);
bindingRepository.insert(
"expense", orderNo, definition.getId(), instance.getId());
return instance.getId();
}
}
金额使用整数分保存,避免流程表达式直接比较浮点数。网关条件所需变量应在启动前完成类型校验,不能让缺失变量直到流程运行时才暴露。
若需要灰度,可根据稳定且可复现的业务维度选择 v1 或 v2,例如组织、租户或业务类型。不要使用每次请求都变化的随机选择,否则同一单据重试时可能进入不同版本。无论采用何种规则,最终选中的 definition ID 都要持久化。
步骤三:切换新版并限制旧版新建
验证 v2 后,将路由配置从 v1 definition ID 切换为 v2。配置更新与应用实例生效可能不是瞬时完成的,因此启动接口仍需查询定义是否处于 active 状态,并对配置变更建立审计记录。
确认所有应用实例都已切换后,可以暂停旧定义,阻止它继续创建新实例,同时不暂停其已有实例:
public void stopNewInstancesOnOldVersion(String oldDefinitionId) {
repositoryService.suspendProcessDefinitionById(
oldDefinitionId,
false,
null);
}
第二个参数为 false 表示不连带暂停已有流程实例。调用前仍应在当前项目使用的 Flowable 依赖上做集成验证,尤其要覆盖定时器、异步作业和外部任务等项目实际使用的能力。
操作顺序不能颠倒:如果先暂停 v1,而部分应用仍持有 v1 配置,这些请求会直接失败。推荐顺序是部署 v2、验证 v2、更新路由、确认配置收敛、最后暂停 v1 的新建能力。
在途实例如何保持兼容
旧实例可以继续使用 v1 定义,但应用代码也必须兼容它。常见做法包括:
- 读取绑定表中的 definition ID,再决定展示哪套表单字段;
- 任务处理根据
taskDefinitionKey分派,不把所有版本强行映射到新版节点; - 新增变量时提供缺省值,不假设旧实例已经拥有该变量;
- 服务任务委托类保持向后兼容,或用独立类名承载新版逻辑;
- 删除旧代码前,查询旧定义下是否仍有运行实例和未完成任务。
可以按定义 ID 查询在途数量:
long running = runtimeService.createProcessInstanceQuery()
.processDefinitionId(oldDefinitionId)
.count();
只有计数归零还不一定代表可以立刻清理。若系统依赖历史查询、审计报表或流程图展示,删除部署可能影响历史资料可用性。是否删除旧部署,应根据历史级别、归档策略和合规要求单独评估;通常保留已完成版本比立即删除更容易审计。
回滚方案
如果 v2 在切流后出现问题,可执行以下步骤:
- 停止继续扩大流量,将业务路由改回仍可用的 v1 definition ID;
- 如 v1 已暂停,先恢复该流程定义,再恢复业务入口;
- 暂停 v2 的新实例创建,但默认不要连带暂停已经创建的 v2 实例;
- 按绑定表找出切流期间创建的 v2 实例,逐单决定继续、撤销还是人工补偿;
- 修复后部署新的定义版本,不要直接篡改已经部署的流程资源或引擎表。
恢复定义可使用:
repositoryService.activateProcessDefinitionById(
oldDefinitionId,
false,
null);
回滚应用代码之前,还必须确认旧代码能否处理已经存在的 v2 任务。流程路由回滚只能影响后续新实例,不能自动把 v2 在途实例变回 v1。
常见问题
为什么不直接调用 startProcessInstanceByKey?
按 key 启动适合版本策略简单、部署流程严格受控的系统。但在多实例滚动发布、灰度或紧急回滚场景中,它会把版本选择隐含在引擎查询规则中。按 definition ID 启动并落库,版本决策更容易复现和审计。
暂停旧定义会不会让旧任务无法审批?
取决于是否连带暂停实例。本文方案暂停定义时传入 false,目标是禁止以旧定义创建新实例,同时让既有实例继续运行。上线前应通过集成测试验证项目使用的监听器、作业执行器和任务操作。
能否把 v1 在途实例全部迁到 v2?
技术上可以设计迁移,但不能仅凭节点名称相似就执行。至少需要明确活动节点映射、并行分支、子流程、边界事件、变量转换、定时器以及历史审计的处理方式。若没有强制业务需求,让旧实例自然结束通常更可控。
唯一约束冲突后可以直接重试启动吗?
不能盲目重试。应先按 business_type + business_key 查询绑定记录,确认实例是否已经创建。跨数据库场景还要处理“引擎已启动但业务绑定未写入”或相反状态,通过对账任务修复,而不是重复创建实例。
如何测试升级流程?
集成测试应至少覆盖:v1 实例启动后部署 v2,v1 任务仍可完成;切流后新单据使用 v2;暂停 v1 后按 v1 ID 启动失败但原实例可继续;路由回退后新实例重新进入 v1;同一业务单号并发提交时只产生一条有效绑定。
总结
流程改版的关键不是“最新版本能否部署”,而是版本选择是否显式、在途实例是否隔离、回滚是否只影响预期范围。将 definition ID 固化到业务配置和绑定表中,先部署再切流,最后暂停旧版的新建能力,可以把发布动作拆解为可观察、可验证、可回退的步骤。
对于普通节点调整,优先让不同版本并行并让旧实例自然结束;只有在法规、业务规则或严重缺陷要求下,才为在途实例设计专门迁移。流程引擎负责保存版本关系,应用则必须对“哪个业务进入哪个版本”承担明确责任。