状态机——Cola-StateMachine

Cola-StateMachine

阿里开源的 Cola-StateMachine(COLA 状态机)在企业级电商和复杂业务系统中应用极广。相比于 Spring State Machine 的笨重、多线程不安全以及复杂的生命周期管理,Cola-StateMachine 的核心设计哲学是无状态(Stateless)。

它本身就像一张"静态的城市地图"(不保存当前车辆的位置,只保存道路的连通规则),所有的业务数据、当前状态都由业务系统传入,因此它是单例、线程安全、极其轻量且无损性能的。

1、核心概念

在写代码之前,必须先理解 Cola 状态机的 核心要素。通常我们在配置时,会使用3个泛型:<S, E, C>。

  • State(状态 - S):对应业务中的物理状态,通常用枚举表示。
  • Event(事件 - E):触发状态流转的动作/催化剂,同样用枚举表示。
  • Context(上下文 - C):状态流转时的"载体"。因为状态机无状态,你需要把业务参数(如订单ID、支付金额、当前操作人)打包进 Context 传给状态机。
  • Transition(流转关系)
    • External Transition(外部流转):状态 A 接收到事件,流转到状态 B(最常见)。
    • Internal Transition(内部流转):状态 A 接收到事件,状态不发生改变,但触发了一段业务逻辑(例如:在"待发货"状态下,修改收货地址)。
  • Condition(条件):转移执行的前置校验 如库存检查、权限验证等
  • Action(动作):状态转换时执行的业务逻辑 如发送通知、更新数据库等

2、核心语法结构(DSL 链式调用)

Cola 提供了非常优雅的 Fluent API(流式接口),它的核心配置公式如下:

java 复制代码
builder.externalTransition() // 1. 声明是外部流转
       .from(SourceState)    // 2. 源状态
       .to(TargetState)      // 3. 目标状态
       .on(Event)            // 4. 触发事件
       .when(Condition)      // 5. 准入条件校验(断言)
       .perform(Action);     // 6. 核心业务逻辑执行

3、完整实战工程搭建

我们以一个标准的"电商订单系统"为例,完整实现前文状态图的所有细节。

3.1、引入Maven依赖

xml 复制代码
<dependency>
    <groupId>com.alibaba.cola</groupId>
    <artifactId>cola-component-statemachine</artifactId>
    <version>4.2.1</version>
</dependency>

3.2、定义状态与事件枚举

java 复制代码
// 状态
public enum OrderState {
    PENDING_PAY("待付款"),
    PENDING_DELIVER("待发货"),
    PENDING_RECEIVE("待收货"),
    PENDING_COMMENT("待评价"),
    COMPLETED("已完成"),
    CANCELLED("已取消");

    private final String desc;
    OrderState(String desc) { this.desc = desc; }
}


// 事件
public enum OrderEvent {
    USER_PAY("用户支付"),
    MERCHANT_DELIVER("商家发货"),
    RECEIVE_PACKAGE("收到包裹"),
    COMMENT("评价"),
    COMMENT_TIMEOUT("超时未评"),
    MANUAL_CANCEL("手动取消"),
    PAY_TIMEOUT("超时未支付"),
    UPDATE_ADDRESS("修改地址"); // 用于演示内部流转

    private final String desc;

    OrderEvent(String desc) {
        this.desc = desc;
    }
}

3.3、定义业务上下文(Context)

Context 负责在状态机各个生命周期阶段透传数据。

java 复制代码
public class OrderContext {
    private final Long orderId;
    private final BigDecimal price;
    private String newAddress; // 用于修改地址

    public OrderContext(Long orderId, BigDecimal price) {
        this.orderId = orderId;
        this.price = price;
    }
    // Getter & Setter 省略...
}

3.4、通过 Spring 配置类组装状态机

在这一步,我们把状态图完全用代码勾勒出来。你可以将 Condition 和 Action 抽离为独立的 Lambda 表达式或组件。

java 复制代码
@Configuration
public class StateMachineConfig {

    public static final String ORDER_MACHINE_ID = "orderStateMachine";

    @Bean
    public StateMachine<OrderState, OrderEvent, OrderContext> orderStateMachine() {
        // 第一步:通过工厂创建 Builder
        StateMachineBuilder<OrderState, OrderEvent, OrderContext> builder = StateMachineBuilderFactory.create();

        // ==================== 1. 待付款状态的分支 ====================
        builder.externalTransition()
                .from(OrderState.PENDING_PAY).to(OrderState.PENDING_DELIVER)
                .on(OrderEvent.USER_PAY)
                .when(checkPayAmount()) // 校验金额
                .perform(doPayAction());

        builder.externalTransition()
                .from(OrderState.PENDING_PAY).to(OrderState.CANCELLED)
                .on(OrderEvent.MANUAL_CANCEL)
                .perform(doCancelAction());

        builder.externalTransition()
                .from(OrderState.PENDING_PAY).to(OrderState.CANCELLED)
                .on(OrderEvent.PAY_TIMEOUT)
                .perform(doCancelAction());

        // ==================== 2. 待发货状态的分支 ====================
        builder.externalTransition()
                .from(OrderState.PENDING_DELIVER).to(OrderState.PENDING_RECEIVE)
                .on(OrderEvent.MERCHANT_DELIVER)
                .perform(doDeliverAction());

        // 演示:内部流转(修改地址,状态不变)
        builder.internalTransition()
                .within(OrderState.PENDING_DELIVER)
                .on(OrderEvent.UPDATE_ADDRESS)
                .perform(doUpdateAddressAction());

        // ==================== 3. 待收货状态的分支 ====================
        builder.externalTransition()
                .from(OrderState.PENDING_RECEIVE).to(OrderState.PENDING_COMMENT)
                .on(OrderEvent.RECEIVE_PACKAGE)
                .perform(doReceiveAction());

        // ==================== 4. 待评价状态的分支 ====================
        builder.externalTransition()
                .from(OrderState.PENDING_COMMENT).to(OrderState.COMPLETED)
                .on(OrderEvent.COMMENT)
                .perform(doCommentAction());

        builder.externalTransition()
                .from(OrderState.PENDING_COMMENT).to(OrderState.COMPLETED)
                .on(OrderEvent.COMMENT_TIMEOUT)
                .perform(doCommentTimeoutAction());

        // 第五步:生成单例状态机实例
        return builder.build(ORDER_MACHINE_ID);
    }

    // ==================== 准入条件定义 (Condition) ====================
    private Condition<OrderContext> checkPayAmount() {
        return context -> {
            // 如果实付金额小于等于0,拒绝流转
            return context.getPrice() != null && context.getPrice().compareTo(BigDecimal.ZERO) > 0;
        };
    }

    // ==================== 业务逻辑定义 (Action) ====================
    private Action<OrderState, OrderEvent, OrderContext> doPayAction() {
        return (from, to, event, context) -> {
            System.out.printf("【Action】订单%d 支付成功. 状态从 %s -> %s\n", context.getOrderId(), from, to);
            // 真实的业务系统里,可以在这里调用库存微服务扣减真实库存、发送落地MQ消息等
        };
    }

    private Action<OrderState, OrderEvent, OrderContext> doCancelAction() {
        return (from, to, event, context) -> System.out.printf("【Action】订单%d 已取消, 释放预扣库存.\n", context.getOrderId());
    }

    private Action<OrderState, OrderEvent, OrderContext> doDeliverAction() {
        return (from, to, event, context) -> System.out.println("【Action】物流单号已绑定,扣减主仓库存...");
    }

    private Action<OrderState, OrderEvent, OrderContext> doUpdateAddressAction() {
        return (from, to, event, context) -> System.out.println("【内部流转】仅修改物理地址为:" + context.getNewAddress() + ",状态保持不变");
    }

    private Action<OrderState, OrderEvent, OrderContext> doReceiveAction() { 
        return (f,t,e,c) -> System.out.println("【Action】收货成功,资金进入商家待结算账户"); 
    }
    
    private Action<OrderState, OrderEvent, OrderContext> doCommentAction() { 
        return (f,t,e,c) -> System.out.println("【Action】评价成功,发放 50 积分"); 
    }
    
    private Action<OrderState, OrderEvent, OrderContext> doCommentTimeoutAction() { 
        return (f,t,e,c) -> System.out.println("【Action】超时未评,系统自动默认好评"); 
    }
}

3.5、在业务层(Service)如何优雅调用

由于配置完毕的状态机是无状态单例,所以在 Service 中,我们需要手动的:查库 -> 驱动状态机 -> 改库。

java 复制代码
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private StateMachine<OrderState, OrderEvent, OrderContext> orderStateMachine;

    @Autowired
    private OrderMapper orderMapper; // 假设这是你的 MyBatis Mapper

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void executeOrderEvent(Long orderId, OrderEvent event, BigDecimal price, String newAddress) {
        // 1. 【核心】从数据库读取当前订单的最真实状态
        OrderDO orderDO = orderMapper.selectById(orderId);
        if (orderDO == null) {
            throw new IllegalArgumentException("订单不存在");
        }
        OrderState currentState = OrderState.valueOf(orderDO.getStatus());

        // 2. 构造本次触发所需的上下文数据
        OrderContext context = new OrderContext(orderId, price);
        context.setNewAddress(newAddress);

        try {
            // 3. 【核心驱动】传入当前状态、发生的事件、上下文
            // fireEvent 方法内部会自动执行:校验Condition -> 执行Action -> 返回新状态
            OrderState nextState = orderStateMachine.fireEvent(currentState, event, context);

            // 4. 将状态机计算出来的"新状态"持久化回数据库
            orderMapper.updateStatus(orderId, nextState.name());

        } catch (RuntimeException e) {
            // 如果遇到非法流转(例如当前状态是已取消,前端却传了一个"发货"事件)
            // Cola-StateMachine 会抛出无对应路由的异常,这里捕获后进行业务拦截
            throw new BizException("当前订单状态非法,无法操作该功能!");
        }
    }
}

4、避坑指南与高级进阶特性

4.1、错误处理与未定义路由

如果用户传入了一个状态机未配置的路由组合(例如:from(PENDING_DELIVER).on(USER_PAY)),Cola 默认会抛出一个 TransitionNotFoundException。

避坑: 业务层一定要包揽 try-catch,否则不合法的请求会导致数据库事务非预期回滚或抛出不易读的系统错误。

4.2、多个相同事件的优先级(多前置条件分支)

有时候,同一个状态,同一个事件,根据不同的业务场景,会流转到不同的状态。

例如:在"待付款"状态下,点击"支付"。如果库存充足,去"待发货";如果库存不足,直接去"已取消"。

Cola 支持通过 Condition 区分:

java 复制代码
builder.externalTransition()
       .from(OrderState.PENDING_PAY).to(OrderState.PENDING_DELIVER)
       .on(OrderEvent.USER_PAY).when(context -> stockEnough())
       .perform(doPayAction());

builder.externalTransition()
       .from(OrderState.PENDING_PAY).to(OrderState.CANCELLED)
       .on(OrderEvent.USER_PAY).when(context -> !stockEnough())
       .perform(doRefundAction());

注意: 当同一个源状态和事件配置了多个 Transition 时,Cola 会按配置顺序依次校验 when(Condition),第一个返回 true 的分支会被执行。因此,互斥条件的设计必须严谨。

4.3、极强辅助功能:逆向生成 PlantUML 图

这是 Cola 状态机最惊艳的功能之一。当你在 Java 代码里写了上百行复杂的流转规则后,人脑很难直观复核。Cola 支持直接把你配置的 Java 状态机代码,逆向生成为 PlantUML 文本。

java 复制代码
@RestController
@RequestMapping("/debug")
public class StateMachineDebugController {

    @Autowired
    private StateMachine<OrderState, OrderEvent, OrderContext> orderStateMachine;

    @GetMapping("/uml")
    public String getUml() {
        // 直接打印或输出
        return orderStateMachine.generatePlantUML();
    }
}
复制代码
@startuml PENDING_DELIVER --> PENDING_RECEIVE : MERCHANT_DELIVER PENDING_DELIVER --> PENDING_DELIVER : UPDATE_ADDRESS PENDING_COMMENT --> COMPLETED : COMMENT_TIMEOUT PENDING_COMMENT --> COMPLETED : COMMENT PENDING_PAY --> CANCELLED : PAY_TIMEOUT PENDING_PAY --> CANCELLED : MANUAL_CANCEL PENDING_PAY --> PENDING_DELIVER : USER_PAY PENDING_RECEIVE --> PENDING_COMMENT : RECEIVE_PACKAGE @enduml

你只需将这串文本复制到任何 PlantUML 查看器中,它就能自动画出状态流转图,供你和产品经理的 PRD 交互图进行对齐检查。

相关推荐
大千AI助手9 个月前
COLA:大型语言模型高效微调的革命性框架
人工智能·语言模型·自然语言处理·lora·peft·cola·残差学习
k↑1 年前
COLA学习之DDD各种术语分析(一)
ddd·cola
s:1032 年前
【Alibaba Cola 状态机】重点解析以及实践案例
java·状态模式·状态机·cola