一佳技术

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

数据流:

  1. 构建阶段:FsmBuilder 把业务枚举转成 entity.State/entity.Event,组装 ConditionWithMeta → COLA StateMachineBuilder → StateMachine.build(id) 注册到全局 Map
  2. 运行阶段:Fsm.fireEvent(sourceEnum, eventEnum, ctx) → 内部 enum→entity 转换 → machine.fireEvent() → 把 entity.State 转回业务枚举返回
  3. 查询阶段: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转换列表容器 + isReady4EventTransits.java
AttachmentMap<K1, Map<?, ?>> 双层 Map,支持状态/事件级元数据挂载Attachment.java
ConditionWithMetaPredicate<C> + Map<?, ?> → COLA Condition<C>ConditionWithMeta.java
entity.State / entity.EventCOLA 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 时计算 predicateCOLA Visitor SPI 是 void 签名,条件计算只能在 Visitor 内部完成
Attachment 双层 MapMap<K1, Map<?, ?>>,K1 是 NamedEntry业务层在状态/事件上挂自定义元数据(如审批流的角色、优先级)
PlantUMLVisitor 只输出字符串格式 source --> target : event\n渲染交给外部 PlantUML Server;零依赖前端
工厂单例不清理COLA StateMachineFactory 静态 Map 永不 remove单例模式;生产环境生命周期等价于 JVM

八、测试覆盖

当前共 43 例(全绿),全部为纯 JVM 测试(零 Spring 依赖):

测试类用例覆盖点
FsmBuilderTest12build 产出、external/internal 转换、条件谓词、metadata 透传、action 回调(外部 + 内部)
FsmTest16fireEvent 正常/条件/无匹配、readyEvent/checkReadyEvent、readyEvents 四种变体(条件过滤 + 无条件)、PlantUML 首尾 + 箭头
TransitsTest6isReady4Event 按 code 跨类型匹配、getFormStates/getToStates/getEvents、transform
AttachmentTest5同 key 返回同一 Map、get(key1, key2) null/after-set、State key 跨实例(@EqualsAndHashCode of=“code”)
ConditionWithMetaTest4predicate 满足/不满足、null predicate 恒真、metadata 透传

可测程度 5/5:所有类纯 POJO + Builder + COLA 委托,测试不需要 Spring 容器、不需要 DB、不需要消息队列。唯一踩坑是 COLA StateMachineFactory id 重复——通过每次 build 用唯一 name 解决。