一、业务问题
流程虚拟机(PVM)的职责是"驱动流程流转"——它需要读写执行路径、创建子路径、查找任务、删除路径。
这些数据存在数据库里,由引擎层负责持久化。于是 PVM 面临一个两难的境地:
方案 A:PVM 直接调引擎层的类
// 反面教材:PVM 直接依赖引擎实现
LbpmExecution entity = hibernateTemplate.get(LbpmExecution.class, id);
entity.setFdState("active");
hibernateTemplate.update(entity);
问题:
-
PVM 绑死在 Hibernate 上——想换 MongoDB?重写 PVM
-
PVM 认识持久化实体——
LbpmExecution是数据库表映射,PVM 不该知道"表结构" -
无法并行多套引擎——引擎层的注释说得很清楚:"需提供不同的流程引擎层并行的情形,即可以同时运行多套流程解析引擎"
-
测试困难——测 PVM 必须先起数据库
方案 B:PVM 定义自己需要的接口,引擎层来实现
这正是 EKP 的选择。PVM 说:"我需要这些能力,但我不关心你怎么实现。"引擎层说:"这些能力我有,我来适配给你。"
这就是适配器模式。
二、模式解法
2.1 标准适配器模式回顾
┌──────────────┐ ┌──────────────────┐ │ Client │────────▶│ Target │ ← 客户端期望的接口 │ (调用方) │ │ (目标接口) │ └──────────────┘ └────────▲─────────┘ │ 实现 ┌────────┴─────────┐ │ Adapter │ ← 适配器 │ (转换层) │ └────────┬─────────┘ │ 持有/委托 ┌────────▼─────────┐ │ Adaptee │ ← 被适配者(已有实现) │ (现有代码) │ └──────────────────┘
核心价值:让两个"接口不兼容"的部分能协作,而双方都不用改。
2.2 EKP 的形态:接口由"使用方"定义
标准适配器模式里,Target 接口通常是"已有的一套标准"。但 EKP 的情况更彻底:
接口由 PVM(使用方)定义,引擎层(提供方)来实现。
这叫依赖倒置——不是 PVM 依赖引擎,而是双方都依赖 EngineWire 这个抽象。
┌─────────────────────────────────────┐ │ PVM 层 │ │ (只认识 EngineWire 接口) │ └──────────────┬──────────────────────┘ │ 依赖 ▼ ┌─────────────────────────────────────┐ │ <<interface>> EngineWire │ ← PVM 定义的契约 │ │ │ + createProcessExecution(...) │ │ + createExecution(...) │ │ + findExecution(...) │ │ + deleteExecution(...) │ └──────────────▲──────────────────────┘ │ 实现(适配) ┌──────────────┴──────────────────────┐ │ 引擎层实现 │ │ (Hibernate / 未来可能的其他实现) │ └─────────────────────────────────────┘
PVM 不知道引擎层有什么类,引擎层不知道 PVM 内部怎么跑。 两边通过 EngineWire 对话。
三、源码证据
3.1 契约接口:EngineWire
接口的类注释把设计意图说得很直白:
/**
* 提供流程引擎必须的流程元素:流程定义和流程实例。
* PVM层不关注如何去创建流程定义,定义中的活动,流程实例和流程执行路径,
* 这些对象的创建都是由此接口提供。
* <p>
* PVM层是一个流程虚拟机,需提供不同的流程引擎层并行的情形,
* 即可以同时运行多套流程解析引擎。
* 流程运行时可以把当前接口实现提供给PVM层。
*
* @author 龚健
*/
public interface EngineWire {
三个关键信息:
| 原文 | 含义 |
|---|---|
| "PVM层不关注如何去创建…" | PVM 只管用,不管造 |
| "可以同时运行多套流程解析引擎" | 这是适配器模式的价值主张:可替换 |
| "把当前接口实现提供给PVM层" | 运行时注入实现(策略化的适配器) |
3.2 九个方法:PVM 的全部"外部需求"
接口只有九个方法,但覆盖了 PVM 的全部外部依赖:
public interface EngineWire {
/** ① 创建流程主执行路径 */
StateProcessExecution createProcessExecution(
OpenProcessDefinition definition, Parameters parameters);
/** ② 根据流程实例ID查找主执行路径 */
StateProcessExecution findProcessExecutionByInstance(
String processInstanceId, Parameters parameters);
/** ③ 创建子执行路径(并发分支) */
StateExecution createExecution(String name, Execution parent,
Parameters parameters);
/** ④ 获取指定id的执行路径 */
StateExecution getExecution(String executionId, Parameters parameters);
/** ⑤ 在指定范围内查找匹配任务ID和活动类型的执行路径 */
StateExecution findExecution(String taskId, String activityType,
Execution scope, Parameters parameters);
/** ⑥ 查找指定状态的执行路径集 */
List<StateExecution> findExecutions(String fdState, Execution scope,
Parameters parameters);
/** ⑦ 获取指定父路径下的直接子路径集 */
List<StateExecution> getChildExecutions(Execution parent,
Parameters parameters);
/** ⑧ 删除执行路径 */
void deleteExecution(Execution execution, Parameters parameters);
/** ⑨ 获取流程定义 */
OpenProcessDefinition getProcessDefinition(String processDefinitionId,
Parameters parameters);
}
逐一归类,PVM 的需求其实只有四类:
| 类别 | 方法 | 语义 |
|---|---|---|
| 创建 | ① ③ | 造执行路径 |
| 查找 | ② ④ ⑤ ⑥ ⑦ ⑨ | 找到已有的执行路径/定义 |
| 删除 | ⑧ | 销毁执行路径 |
| (隐含)更新 | 通过 StateExecution 接口 | 见 3.4 |
这是"最小完备接口"的典范——四个基本操作(增删查改),不多一个方法。
设计启示:契约接口要"刚好够用"。 接口方法越多,实现方的负担越重,可替换性越差。九个方法,就是 PVM 全部的外部依赖。
3.3 一个值得注意的细节:处处有 Parameters
九个方法,每一个都带 Parameters 参数。
createProcessExecution(OpenProcessDefinition definition, Parameters parameters) findProcessExecutionByInstance(String processInstanceId, Parameters parameters) createExecution(String name, Execution parent, Parameters parameters) // ... 全部如此
Parameters 是什么?它是跨越适配器边界的"上下文包"。
它携带:
-
流程实例(
LbpmProcess) -
执行参数(
ExecutionParameters) -
引擎层的服务容器(
ProcessServiceManager) -
访问管理器(
AccessManager)
为什么不把这些做成 EngineWire 的成员变量,而要每次传?
因为执行上下文是运行时的、每次调用都不同的。同一个 EngineWire 实例要服务多个并发执行,不能有实例状态。
设计启示:适配器接口要"无状态",状态通过参数传入。 这样适配器可以是单例、可复用的,天然线程安全。
3.4 双向适配:StateExecution
适配不是单向的。PVM 不仅要调用引擎层,还要修改引擎层的对象状态。
PVM 需要"把执行路径状态改成 active"这个能力。但 LbpmExecution 是引擎层的持久化实体,PVM 不认识它。
解法:PVM 定义 StateExecution 接口,引擎层的对象实现这个接口。
/**
* 提供能更新状态的执行路径,引擎层的执行路径需实现此接口,
* 以提供给PVM层能及时更新相应执行路径的状态
*/
public interface StateExecution extends Execution {
void setFdId(String id);
void setFdTaskId(String fdTaskId);
void setFdActivityType(String fdActivityType);
void setFdActivityId(String fdActivityId);
void setFdState(String fdState);
void setFdSuspendHistoryState(String fdSuspendHistoryState);
@Override
StateProcessExecution getProcessExecution();
@Override
StateExecution getFdParent();
}
类注释一句话点透:"引擎层的执行路径需实现此接口,以提供给PVM层能及时更新相应执行路径的状态"。
这是一个"反向适配器":
标准适配器:Adapter 实现 Target,内部委托 Adaptee 反向适配: Adaptee 直接实现 Target,无需额外适配层
为什么这里不需要独立的 Adapter 类?
因为 LbpmExecution 既是"持久化实体",也可以同时"实现 PVM 的接口"。一个类扮演两个角色,省掉了一层包装。
设计启示:适配器不一定非要"额外写一个类"。 如果被适配者能直接实现目标接口,就没有必要多加一层。适配器是手段,不是目的——目的是让两边能协作。
3.5 服务提供器:EngineProvider 三元组
EngineWire 不是孤立的。PVM 需要三样东西,打包在一个"提供器"里:
/**
* PVM主服务提供服务的提供器
*/
public interface EngineProvider {
/** @return 活动提供器 */
ActivityProvider getActivityProvider();
/** @return 引擎持久化服务 */
EngineWire getEngineWire();
/** @return 事件分发器 */
EngineEventMulticaster getEventMulticaster();
}
三样东西,对应 PVM 的三大组件:
| 提供的能力 | 对应接口 | PVM 拿它做什么 |
|---|---|---|
| 活动提供器 | ActivityProvider | 拿到节点行为、创建任务 |
| 持久化服务 | EngineWire | 读写执行路径 |
| 事件分发器 | EngineEventMulticaster | 广播事件 |
这是"打包适配"的思路:与其让 PVM 分别依赖三个接口,不如打包成一个 EngineProvider,PVM 只需认一个入口。
3.6 上下文:适配的"组装点"
ExecutionContextImpl 是两个世界的组装点:
public class ExecutionContextImpl implements ExecutionContext {
/** 被包裹执行路径 */
private StateExecution execution;
/** 流程主执行路径 */
private StateProcessExecution processExecution;
/** 流程引擎提供器 */
private EngineProvider provider;
/** 流程定义 */
private OpenProcessDefinition definition;
/** 流程参数 */
private Parameters parameters;
/** 执行路径是否记录到数据库中 */
private boolean persistent = true;
/** 构造函数一:启动新流程 */
public ExecutionContextImpl(OpenProcessDefinition definition,
Parameters parameters, EngineProvider provider) {
this.provider = provider;
this.parameters = parameters;
this.definition = definition;
// 通过 EngineWire 创建流程主执行路径
this.processExecution = provider.getEngineWire()
.createProcessExecution(definition, parameters);
this.execution = processExecution;
}
/** 构造函数二:操作已有流程 */
public ExecutionContextImpl(BehaviourParameters behaviour,
OpenProcessDefinition definition, Parameters parameters,
EngineProvider provider, boolean persistent) {
// ...
initExecution(provider.getEngineWire(), parameters, behaviour);
}
private void initExecution(EngineWire wire, Parameters parameters,
BehaviourParameters behaviour) {
this.processExecution = wire.findProcessExecutionByInstance(
behaviour.getFdProcessInstanceId(), parameters);
// 找到指定的执行路径
String taskId = behaviour.getFdTaskId();
if (StringUtil.isNull(taskId)) {
// 若任务id为空,则寻找任何一个激活的节点相对应的执行路径
List<StateExecution> executions = wire.findExecutions(
Execution.STATE_WAITING, null, parameters);
if (executions == null || executions.isEmpty()) {
throw new TaskRefExecutionIsNullException();
}
this.execution = executions.get(0);
} else {
this.execution = wire.findExecution(taskId,
behaviour.getFdActivityType(), processExecution, parameters);
if (execution == null) {
throw new TaskRefExecutionIsNullException();
}
}
}
}
两个构造函数,对应两个场景:
| 构造函数 | 场景 | 用的 EngineWire 方法 |
|---|---|---|
| 一 | 启动新流程 | createProcessExecution |
| 二 | 操作已有流程 | findProcessExecutionByInstance + findExecution |
注意第二个构造函数里的"兜底逻辑":
if (StringUtil.isNull(taskId)) {
// 任务id为空 → 找不到具体任务 → 退而求其次,找任何一个等待中的执行路径
List<StateExecution> executions = wire.findExecutions(
Execution.STATE_WAITING, null, parameters);
}
业务含义:有些操作(比如特权人跳转)可能没有明确的任务 ID,只知道"这个流程实例"。这时就找"第一个等待中的执行路径"。
设计启示:适配器接口的能力要能覆盖"模糊场景"。 如果
EngineWire只有"按 taskId 精确查找",这种模糊场景就没法处理。所以接口里既有精确查找(findExecution),也有条件查找(findExecutions)。
3.7 委托:OpenExecutionWraper 的适配职责
PVM 的包裹类 OpenExecutionWraper 把 EngineWire 的调用封装成便捷方法:
public abstract class OpenExecutionWraper extends ExecutionVisitor
implements OpenExecution {
/** 流程持久化服务 */
private EngineWire wire;
/** 流程定义 */
private OpenProcessDefinition definition;
/** 流程参数 */
private Parameters parameters;
public OpenExecutionWraper(ExecutionContext context) {
super(context);
this.parameters = context.getParameters();
this.wire = context.getProvider().getEngineWire(); // ← 从 provider 取 wire
this.definition = context.getDefinition();
}
/** 创建子执行路径 —— 委托给 EngineWire */
protected StateExecution createStateExecution(String name) {
return wire.createExecution(name, getSource(), parameters);
}
/** 取子执行路径集 —— 委托给 EngineWire */
protected List<StateExecution> getChildExecutions() {
return wire.getChildExecutions(getSource(), parameters);
}
/** 删除执行路径 —— 委托给 EngineWire */
protected void deleteExecution(Execution execution) {
wire.deleteExecution(execution, parameters);
}
}
这是标准的适配器"委托"结构:
PVM 内部代码 ↓ 调 createStateExecution(name) OpenExecutionWraper(PVM 侧的统一包装) ↓ 委托 wire.createExecution(name, source, parameters) EngineWire 实现(引擎层) ↓ 实际创建 + 持久化 数据库
PVM 内部代码不需要每次都写 wire.xxx(..., parameters)——parameters 和 wire 都在包裹类里存好了,调用方只管说"我要创建一个子路径"。
设计启示:适配器之上可以再封一层"便捷接口"。 EngineWire 是"能力契约"(面向引擎层),Wraper 的方法是"使用便利"(面向 PVM 内部代码)。两者分工明确。
四、设计启示
4.1 适配器 vs 依赖倒置:一对孪生兄弟
这两个模式经常一起出现,但关注点不同:
| 维度 | 适配器模式 | 依赖倒置原则 |
|---|---|---|
| 关注 | 接口不兼容,怎么协作 | 依赖方向,谁依赖谁 |
| 手段 | 加一层转换 | 定义抽象,双方都依赖抽象 |
| EKP 场景 | 引擎实体 → PVM 接口 | PVM 与引擎层都依赖 EngineWire |
| 关系 | 适配器实现依赖倒置的手段之一 | 依赖倒置指导适配器设计 |
EKP 里两者是融合的:EngineWire 既是"适配器接口"(让引擎层适配 PVM 的需求),也是"依赖倒置的抽象"(PVM 不依赖具体引擎)。
4.2 适用场景判断
什么时候需要适配器?
| 信号 | EKP 的情况 |
|---|---|
| 两个模块接口不兼容 | PVM 要 Execution,引擎层有 LbpmExecution |
| 一方可能被替换 | 引擎层可能换持久化实现 |
| 一方不该依赖另一方 | PVM 是"虚拟机",不该依赖具体引擎 |
| 需要隔离变化 | 数据库表结构可能变,PVM 不该受影响 |
反例(不该用适配器):
-
两个类接口本来就兼容 → 直接调
-
只有一个实现且永不变 → 直接依赖,别过度设计
4.3 三个实践要点
① 接口归"使用方"定义
EngineWire 定义在 PVM 侧(pvm/service 包),不定义在引擎侧。这个细节很重要:
接口在"要什么"的一侧 → 提供方必须迁就使用方 接口在"给什么"的一侧 → 使用方必须迁就提供方 ❌
接口归属决定了谁掌握主动权。 接口在 PVM 侧,意味着引擎层要适配 PVM;反之,PVM 就被引擎绑死了。
② 无状态 + 参数传递
前面讲过:EngineWire 的每个方法都带 Parameters。这让实现可以是单例、线程安全。
③ 提供"打包"入口
EngineProvider 把三个能力打包成一个入口。PVM 只需注入一个 provider,就能拿到全部所需服务。
五、业务价值
5.1 引擎可替换,PVM 不用改
这是最直接的价值:同一套 PVM,可以驱动不同的引擎实现。
业务上意味着什么?
-
从 Hibernate 迁到 MyBatis → 换
EngineWire实现,PVM 零改动 -
高并发场景改用缓存层 → 换实现,PVM 零改动
-
新客户用不同数据库 → 换实现,PVM 零改动
业务价值:技术选型不被锁死。 引擎核心的演进不会因为底层存储的变化而受阻。
5.2 测试可以脱离数据库
EngineWire 是接口,测试时可以注入一个内存实现:
// 测试用的内存实现(示意)
class InMemoryEngineWire implements EngineWire {
private Map<String, StateExecution> executions = new HashMap<>();
// 全部操作在内存 Map 上完成,不碰数据库
}
业务价值:PVM 的单测秒级完成,不用起数据库、不用造数据。测试跑得快,开发迭代就快。
5.3 分层解耦让"改一处"影响可控
PVM 和引擎层通过 EngineWire 隔离,意味着:
| 改动 | 影响范围 |
|---|---|
| 改 PVM 内部流转逻辑 | 引擎层不受影响 |
| 改引擎层持久化实现 | PVM 不受影响 |
| 改数据库表结构 | 只改 EngineWire 实现 |
业务价值:改动影响可控,回归测试范围小,上线风险低。
5.4 新人能分头理解
一个新人接手代码,可以分头理解两个世界:
-
想懂流程怎么流转 → 看 PVM 层(只依赖
EngineWire接口) -
想懂数据怎么存 → 看引擎层(实现
EngineWire)
不用同时理解两边。 这是分层带来的人力可扩展性——团队可以并行开发,而不是所有人都要懂全部。
一句话:适配器模式让 PVM 说"我需要什么",引擎层说"我来给"。接口由使用方定义,实现由提供方适配,双方都依赖抽象而非彼此。 这是流程引擎能"分层解耦、引擎可替换"的技术基础。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/wwwhongxia/article/details/166253341




