一佳技术

easy-fsm 使用文档

easy-fsm 是一个零 Spring 依赖的纯 Java 状态机组件。基于 COLA 4.3 StateMachine 做二次封装,业务只需定义实现 NamedEntry 的 State/Event 枚举,一行 Builder 链式 API 即可构建状态机,运行时支持 fireEvent / readyEvents / PlantUML 生成。


1. 引入依赖

<dependency>
    <groupId>io.matrix</groupId>
    <artifactId>easy-fsm</artifactId>
    <version>${revision}</version>
</dependency>

easy-fsm 零 Spring 依赖,可在任何 Java 项目中使用(包括非 Spring Boot 的单体应用、工具链、单元测试)。

2. 定义 State / Event 枚举

State 和 Event 枚举必须同时满足:

  1. extends Enum<E>(枚举)
  2. implements NamedEntry(kernel-client 提供)
public enum OrderState implements NamedEntry {
    CREATED("CREATED", "已创建"),
    PAID("PAID", "已支付"),
    SHIPPED("SHIPPED", "已发货"),
    COMPLETED("COMPLETED", "已完成"),
    CANCELLED("CANCELLED", "已取消");

    private final String code;
    private final String name;
    OrderState(String code, String name) { this.code = code; this.name = name; }
    @Override public String getCode() { return code; }
    @Override public String getName() { return name; }
}

public enum OrderEvent implements NamedEntry {
    PAY("PAY", "支付"),
    SHIP("SHIP", "发货"),
    CANCEL("CANCEL", "取消"),
    COMPLETE("COMPLETE", "完成");

    private final String code;
    private final String name;
    OrderEvent(String code, String name) { this.code = code; this.name = name; }
    @Override public String getCode() { return code; }
    @Override public String getName() { return name; }
}

// 业务上下文,携带条件判断所需的运行时数据
public class OrderCtx {
    private int amount;
    public int getAmount() { return amount; }
    public OrderCtx amount(int a) { this.amount = a; return this; }
}

约束:Fsm<S extends Enum<S> & NamedEntry, E extends Enum<E> & NamedEntry, C> 泛型三参数缺一不可。没有实现 NamedEntry 会在编译期报错。

3. 构建状态机

生产代码通常在 @PostConstruct 中构建一次,之后复用 Fsm 实例:

// Builder 第一个参数是唯一 machineId(COLA StateMachineFactory 全局 Map 的 key)
Fsm<OrderState, OrderEvent, OrderCtx> fsm = FsmBuilder
    .builder(OrderState.class, OrderEvent.class, OrderCtx.class, "order-machine")
    // 外部转换:from → to,事件
    .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)
    .build();

COLA StateMachineFactory 单例限制:build(name) 的 name 在 COLA 全局 Map 中唯一。重复 build 相同 name 会抛 StateMachineException: The state machine with id [xxx] is already built. 每个 Builder 实例只能调一次 build()。

4. 执行业务:fireEvent

OrderCtx ctx = new OrderCtx();
OrderState next = fsm.fireEvent(order.getState(), OrderEvent.PAY, ctx);
order.setState(next);  // next == OrderState.PAID

fireEvent 对无匹配转换的行为:返回 sourceState(COLA StateMachine 默认策略)。如需显式检查:

if (!fsm.readyEvent(order.getState(), OrderEvent.SHIP, ctx)) {
    throw new IllegalStateException("当前状态不可发货");
}

5. 查询可执行转换:readyEvents

查询某状态当前可执行的所有事件(带 ctx 会计算条件谓词,不带 ctx 条件视为满足):

// 不带 ctx:返回 PAID 状态下所有声明的转换
Transits<State, Event> all = fsm.readyEvents(OrderState.PAID);

// 带 ctx:只返回条件满足的转换(金额 ≥ 10000 才会包含 AUTO_SHIP)
Transits<State, Event> ready = fsm.readyEvents(OrderState.PAID, new OrderCtx().amount(50000));

// 判断某个事件是否在可执行列表里
boolean canShip = ready.isReady4Event(OrderEvent.SHIP);

// 获取所有可执行的目标状态
List<State> toStates = ready.getToStates();

6. 条件谓词

在 external()/internal() 调用中传入 Predicate<C> 作为条件:

.external(OrderState.PAID, OrderState.SHIPPED, OrderEvent.AUTO_SHIP,
          ctx -> ctx.getAmount() >= 10000)   // 只接受大订单

条件为 null 时视为恒真(满足)。

7. 动作回调 FsmAction

转换成功后自动触发回调,用于审计日志、发事件通知等:

import io.matrix.fsm.FsmAction;

.external(OrderState.CREATED, OrderState.PAID, OrderEvent.PAY,
          null, null, (from, to, evt, ctx) -> {
    auditLog(from, to, evt, ctx);  // 业务方法
})

FsmAction 签名:void execute(S from, S to, E event, C context)。异常会原样传播,框架不吞。

8. 元数据挂载

FsmBuilder 提供 stateAttachment / eventAttachment 把业务元数据挂到状态/事件上。ReadyEventVisitor 产出的 Transit 会携带 metadata。

9. PlantUML 可视化

一行生成状态图源码:

String puml = fsm.generatePlantUML();
// @startuml
// CREATED --> PAID : PAY
// PAID --> SHIPPED : SHIP
// ...
// @enduml

把字符串贴到 PlantUML Online Server 即可渲染成图片,挂到 wiki / 评审文档。

10. 注意事项

项说明
枚举必须实现 NamedEntry编译期泛型约束保证
每个 Builder 实例只能 build 一次重复 build 会抛 StateMachineException
COLA 全局 Map 不清理生产单例模式;测试需唯一 id
fireEvent 对无匹配转换返回 sourceState用 readyEvent 显式检查
内部转换 internal() 状态不变用于自环,如”已支付状态再次通知支付”
toState() / toEvent() 是 protected子类可覆盖做自定义 entity 包装

11. 常见问题

Q: fireEvent 返回的 state 怎么转回业务枚举? A: 直接赋值——Fsm<S, E, C> 泛型约束保证返回值是 S(你的业务枚举类型),COLA 内部的 entity.State 被自动转回。

Q: 测试里怎么避免 StateMachineException? A: 每个 @BeforeEach 用唯一的 machineId,例如:

String id = "order-" + testInfo.getTestMethod().get().getName() + "-" + UUID.randomUUID();
FsmBuilder.builder(..., id).build();

Q: 可以动态添加转换吗? A: 不行。Fsm 是构建时静态声明,构建后不可变。如需动态路由,用条件谓词 Predicate<C> 或两个独立 Fsm 实例按场景切换。

Q: 线程安全吗? A: Fsm 实例只读,内部 StateMachine 是 final,多线程 fireEvent 安全。

Q: 雪花算法的 allowClockBackward 配置不生效? A: 这是 easy-fsm 的已知设计(硬编码 5ms 阈值),生产代码的 clock backward 逻辑不读 maxClockBackwardMs 字段。详见 design.md §九。