easy-fsm 状态机组件设计
本文档描述 easy-fsm 组件的设计与实现。该组件基于 COLA 4.3 StateMachine 做二次封装,提供 Builder 风格的状态机构建 API、条件谓词、元数据挂载和 PlantUML 生成能力。组件零 Spring 依赖,在业务层以泛型形式约束 State 必须是实现
NamedEntry的枚举、Event 同理,避免运行时装错类型。
一、背景与问题
1.1 为什么需要状态机
在 DDD 聚合根中,“订单状态迁移”、“工单审批流”、“任务生命周期”都是有限状态问题:
| 痛点 | 现状 | 目标 |
|---|---|---|
| 合法性散落各处 | if-else 分散在 Service / Validator / Controller 三层 | 状态转换规则集中声明,运行时可查可验证 |
| 可视化缺失 | 新人靠 trace 代码理解状态流 | 一键生成 PlantUML,挂到 wiki 即可 |
| 动作回调重复 | 每个 changeStatus() 都要写 audit log / 发事件 | Builder 里声明 FsmAction 回调,框架自动触发 |
| 条件谓词硬编码 | 大订单走一条路、小订单走另一条,散在代码 | Predicate<C> 作为条件,状态机自动路由 |
1.2 选型:为什么是 COLA 4.3 StateMachine
COLA StateMachine 解决了大部分问题,但业务用起来有两个不便:
| 不便 | 解决 |
|---|---|
| State / Event 必须是 COLA 的实例,不支持业务枚举直接当状态 | FsmBuilder 在内部做 enum → entity.State 转换 |
条件/元数据需要手动创建 Condition + 分开挂载 | ConditionWithMeta 把 Predicate 和 metadata 包成一个对象,Builder 一行调用 |
二、组件定位与边界
定位:一个”类型安全 + Builder 友好”的状态机门面。业务方声明枚举和规则后,Fsm 对外暴露:fireEvent(执行转换)、readyEvents(查询可执行转换)、generatePlantUML(可视化)。
边界:
| 做 | 不做 |
|---|---|
| 状态转换执行与查询 | 不负责持久化状态变化(业务层聚合根自己做) |
| 条件谓词 + metadata 透传 | 不做事务控制(调用方决定 TransactionTemplate) |
| PlantUML 生成 | 不做前端渲染(只输出字符串,渲染由 PlantUML Server 承担) |
| FsmAction 动作回调 | 不做重试/降级(回调里的异常原样传播) |
| 工厂单例缓存(COLA StateMachineFactory) | 不管理生命周期(构建一次复用终身) |
约束:每个 FsmBuilder.build() 的 machineId 在 COLA 全局 Map 中唯一;重复 build 相同 id 会抛 StateMachineException。生产代码不清理该 Map(单例模式)。
三、总体架构
flowchart TD
subgraph "业务层"
A1["OrderState(enum) implements NamedEntry"]
A2["OrderEvent(enum) implements NamedEntry"]
A3["OrderCtx(业务上下文)"]
end
subgraph "easy-fsm 门面层"
B1["FsmBuilder<br/>Builder 风格组装"]
B2["Fsm<br/>门面 API:fireEvent/readyEvents/generatePlantUML"]
B3["Transit / Transits<br/>转换快照与列表容器"]
B4["Attachment<br/>双层 Map 元数据挂载"]
B5["ConditionWithMeta<br/>Predicate + metadata 封装"]
end
subgraph "COLA 4.3 StateMachine"
C1["StateMachineBuilder<br/>COLA 原生产品"]
C2["StateMachine<State, Event, C>"]
C3["Visitor SPI"]
end
subgraph "entity & visitor"
D1["entity.State / entity.Event<br/>COLA 状态/事件实例"]
D2["PlantUMLVisitor → @startuml...@enduml"]
D3["ReadyEventVisitor → Transits 转换快照"]
end
A1 -->|"toState()"| D1
A2 -->|"toEvent()"| D1
B1 -->|"internal()/external()"| C1
C1 -->|"build(name)"| C2
B2 -->|"委托"| C2
C2 -->|"accept(Visitor)"| D2
C2 -->|"accept(Visitor)"| D3
B5 -->|"implements Condition"| C1
B4 -->|"挂载到 entity"| D1
D3 -->|"产出"| B3
数据流:
- 构建阶段:FsmBuilder 把业务枚举转成 entity.State/entity.Event,组装
ConditionWithMeta→ COLA StateMachineBuilder →StateMachine.build(id)注册到全局 Map - 运行阶段:Fsm.fireEvent(sourceEnum, eventEnum, ctx) → 内部 enum→entity 转换 →
machine.fireEvent()→ 把 entity.State 转回业务枚举返回 - 查询阶段:Fsm.readyEvents(source, ctx) →
ReadyEventVisitor遍历状态 → 计算条件 → 产出Transits<State, Event>
四、核心 API 草图
4.1 状态/事件枚举约束(业务层实现)
业务枚举必须同时是 enum + implements NamedEntry:
public enum OrderState implements NamedEntry {
CREATED("CREATED", "已创建"),
PAID("PAID", "已支付"),
SHIPPED("SHIPPED", "已发货"),
COMPLETED("COMPLETED", "已完成"),
CANCELLED("CANCELLED", "已取消");
private final String code;
private final String name;
// 构造器 + getName/getCode
}
约束来源:
Fsm<S extends Enum<S> & NamedEntry, E extends Enum<E> & NamedEntry, C>。枚举天然满足Enum<S>;NamedEntry由 kernel-client 提供。
4.2 Builder API
FsmBuilder.builder(OrderState.class, OrderEvent.class, OrderCtx.class, "order-machine")
.external(OrderState.CREATED, OrderState.PAID, OrderEvent.PAY)
.external(OrderState.PAID, OrderState.SHIPPED, OrderEvent.AUTO_SHIP,
ctx -> ctx.getAmount() >= 10000) // 带条件
.external(OrderState.PAID, OrderState.CANCELLED, OrderEvent.CANCEL,
null, null, (from, to, evt, ctx) -> auditLog(...)) // 带动作回调
.internal(OrderState.PAID, OrderEvent.PAY, OrderCtx::isValid) // 自环
.build();
四个 external() 重载 + 四个 internal() 重载,按”条件 → 元数据 → 动作”逐层增量。
4.3 Fsm 门面 API
// 执行转换:返回目标状态枚举(COLA 对无匹配转换返回 sourceState)
OrderState next = fsm.fireEvent(OrderState.CREATED, OrderEvent.PAY, new OrderCtx());
// 查询转换:带 ctx 则计算条件;不带则条件视为满足
Transits<State, Event> ready = fsm.readyEvents(OrderState.PAID, ctx);
boolean ready = fsm.readyEvent(OrderState.PAID, OrderEvent.AUTO_SHIP, ctx);
fsm.checkReadyEvent(OrderState.PAID, OrderEvent.PAY, ctx); // 不满足抛 IllegalArgumentException
// 可视化
String plantUML = fsm.generatePlantUML();
4.4 Transit / Transits
Transit<S, E> 是转换快照,持有 from/to/event 三个 NamedEntry + metadata。提供 getFromMata(Attachment) / getEventMata(Attachment) 从 Attachment 中取元数据的便捷方法。
Transits<S, E> 是列表容器,支持 isReady4Event(E) 按 code 匹配(跨类型:业务枚举 ↔ entity.Event 通过 code 比较)。
五、包结构与核心类
easy-fsm/
└── io/matrix/fsm/
├── Fsm.java # 门面
├── FsmBuilder.java # Builder
├── FsmAction.java # @FunctionalInterface 回调
├── Transit.java # 转换快照
├── Transits.java # 转换列表容器
├── Attachment.java # 双层 Map 元数据
├── condition/
│ └── ConditionWithMeta.java # Predicate + metadata → COLA Condition
├── entity/
│ ├── AbstractEntry.java # NamedEntry + metaMap + toEnum()
│ ├── State.java # COLA State 包装
│ └── Event.java # COLA Event 包装(含 parent 父事件支持)
└── visitor/
├── PlantUMLVisitor.java # → "@startuml ... @enduml"
└── ReadyEventVisitor.java # → Transits(可选条件计算)
| 类 | 职责 | 文件 |
|---|---|---|
| Fsm | 门面,委托 COLA StateMachine 执行/查询/可视化 | Fsm.java |
| FsmBuilder | 构建器,内部 enum → entity 转换 | FsmBuilder.java |
| Transit | 单次转换快照(from/to/event + metadata) | Transit.java |
| Transits | 转换列表容器 + isReady4Event | Transits.java |
| Attachment | Map<K1, Map<?, ?>> 双层 Map,支持状态/事件级元数据挂载 | Attachment.java |
| ConditionWithMeta | Predicate<C> + Map<?, ?> → COLA Condition<C> | ConditionWithMeta.java |
| entity.State / entity.Event | COLA State/Event 的包装实现 | State.java / Event.java |
| PlantUMLVisitor | 生成 @startuml...@enduml 状态图源码 | PlantUMLVisitor.java |
| ReadyEventVisitor | 遍历 StateMachine 收集可执行转换(支持条件计算) | ReadyEventVisitor.java |
依赖:
| 依赖 | 说明 |
|---|---|
com.alibaba.cola:cola-component-statemachine:4.3.2 | 核心实现 |
io.matrix:kernel-client | 提供 NamedEntry 接口约束 |
io.matrix:kernel-common | 通用工具 |
零 Spring 依赖 ✅
六、使用示例
6.1 完整订单状态机示例
// 1. 定义枚举
public enum OrderState implements NamedEntry { ... } // CREATED → PAID → SHIPPED → COMPLETED / CANCELLED
public enum OrderEvent implements NamedEntry { ... } // PAY / SHIP / CANCEL / COMPLETE / AUTO_SHIP
// 2. 构建(生产代码通常 @PostConstruct 执行一次,复用 Fsm 实例)
Fsm<OrderState, OrderEvent, OrderCtx> fsm = FsmBuilder
.builder(OrderState.class, OrderEvent.class, OrderCtx.class, "order-machine")
.external(OrderState.CREATED, OrderState.PAID, OrderEvent.PAY)
.external(OrderState.PAID, OrderState.SHIPPED, OrderEvent.SHIP)
.external(OrderState.SHIPPED, OrderState.COMPLETED, OrderEvent.COMPLETE)
.external(OrderState.CREATED, OrderState.CANCELLED, OrderEvent.CANCEL)
.external(OrderState.PAID, OrderState.CANCELLED, OrderEvent.CANCEL)
.external(OrderState.PAID, OrderState.SHIPPED, OrderEvent.AUTO_SHIP,
ctx -> ctx.getAmount() >= 10000)
.build();
// 3. 执行业务
OrderCtx ctx = new OrderCtx().amount(50000);
OrderState next = fsm.fireEvent(order.getState(), OrderEvent.AUTO_SHIP, ctx);
order.setState(next);
// 4. 启动时检查某状态可执行什么转换(可选,给前端/审批流用)
Transits<State, Event> ready = fsm.readyEvents(OrderState.PAID, ctx);
List<Event> events = ready.getEvents(); // [SHIP, AUTO_SHIP, CANCEL]
// 5. 生成 PlantUML 贴到 wiki
String puml = fsm.generatePlantUML();
6.2 COLA StateMachineFactory id 唯一性
COLA 把所有已构建的状态机存进 StateMachineFactory.stateMachineMap,key 是 build(name) 的 name。重复 name 会抛异常:
StateMachineException: The state machine with id [order-machine] is already built, no need to build again
生产环境:用 Spring @PostConstruct 初始化一次,注入给 Service 复用。
测试环境:每个 @BeforeEach 用唯一 id("order-" + testInfo.getTestMethod() + "-" + uuid)。
七、关键设计决策
| 决策 | 内容 | 理由 |
|---|---|---|
泛型 S/E 约束为 Enum & NamedEntry | 业务枚举既能当 COLA State 又有 code/name 元信息 | 避免在业务层写中间 DTO;code 天然唯一键 |
external() / internal() 各 4 个重载 | 从最简三参到完整五参,逐层递增 | Builder 风格渐进式 API,简单用例一行写完,复杂用例可控 |
ConditionWithMeta 封装 Predicate + metadata | 一个对象同时满足 COLA Condition 接口 + 挂载元数据 | ReadyEventVisitor 拿到 Condition 就能同时取 metadata,避免双参数传递 |
| Fsm 内部 enum → entity 转换 | 构造时把业务枚举转 COLA entity.State/Event | 业务层零感知 COLA 实现;entity.State 持有 metaMap 可挂 metadata |
| ReadyEventVisitor 支持可选 ctx | 无 ctx 时条件视为满足,有 ctx 时计算 predicate | COLA Visitor SPI 是 void 签名,条件计算只能在 Visitor 内部完成 |
| Attachment 双层 Map | Map<K1, Map<?, ?>>,K1 是 NamedEntry | 业务层在状态/事件上挂自定义元数据(如审批流的角色、优先级) |
| PlantUMLVisitor 只输出字符串 | 格式 source --> target : event\n | 渲染交给外部 PlantUML Server;零依赖前端 |
| 工厂单例不清理 | COLA StateMachineFactory 静态 Map 永不 remove | 单例模式;生产环境生命周期等价于 JVM |
八、测试覆盖
当前共 43 例(全绿),全部为纯 JVM 测试(零 Spring 依赖):
| 测试类 | 用例 | 覆盖点 |
|---|---|---|
| FsmBuilderTest | 12 | build 产出、external/internal 转换、条件谓词、metadata 透传、action 回调(外部 + 内部) |
| FsmTest | 16 | fireEvent 正常/条件/无匹配、readyEvent/checkReadyEvent、readyEvents 四种变体(条件过滤 + 无条件)、PlantUML 首尾 + 箭头 |
| TransitsTest | 6 | isReady4Event 按 code 跨类型匹配、getFormStates/getToStates/getEvents、transform |
| AttachmentTest | 5 | 同 key 返回同一 Map、get(key1, key2) null/after-set、State key 跨实例(@EqualsAndHashCode of=“code”) |
| ConditionWithMetaTest | 4 | predicate 满足/不满足、null predicate 恒真、metadata 透传 |
可测程度 5/5:所有类纯 POJO + Builder + COLA 委托,测试不需要 Spring 容器、不需要 DB、不需要消息队列。唯一踩坑是 COLA StateMachineFactory id 重复——通过每次 build 用唯一 name 解决。