Skip to content

Java状态机案例Demo

约 3742 字大约 12 分钟

2026-04-09

「状态机 = 规则」 + 「Action = 业务」 + 「Service = 编排」

  • 限制状态的更新,制定好规则,例:只能从状态A变更为状态B和状态C,只能从状态B变更为状态D,如果直接从状态A变更为状态D,则报错。

  • 状态的变更是由每一个Event事件决定的,每个事件所对应的业务逻辑会由各自独立的Action去进行实现。

  • 流程编排:连接「状态机规则」与「事件业务实现」的枢纽,用于管理事件Event与业务Action的关联关系。

private final Map<OrderEvent, OrderEventAction> actions;

用途:快速回忆“订单状态机 + 业务动作编排”的设计思路,写真实代码时可直接套用。


一、业务场景与目标

  • 业务场景:订单从「创建 → 支付 → 发货 → 完成 / 取消」的生命周期。
  • 目标
    • 把“状态是否合法、能不能迁移”从业务代码里抽出来,放到一个纯规则的状态机中。
    • 每个事件(支付、发货、完成、取消)的业务逻辑(扣款、库存、物流、积分、退款等)由独立的 Action 实现
    • 通过一个编排层(OrderProcessService)把“状态规则”和“业务副作用”解耦

二、整体结构总览

1. 核心类一览

  • 状态 & 事件:
    • OrderStatus:订单状态枚举(CREATED/PAID/SHIPPED/FINISHED/CANCELED)。
    • OrderEvent:事件枚举(PAY/SHIP/FINISH/CANCEL)。
  • 状态机与上下文:
    • OrderStateMachine:只维护「当前状态 + 事件 → 下一状态」的二维表。
    • OrderContext:订单上下文,包含 orderId、当前 status 以及一些演示用的业务字段(流水号、运单号、积分)。
  • 事件业务动作:
    • 接口:OrderEventAction(函数式接口)。
    • 实现:
      • PayOrderAction:模拟支付成功、生成支付流水号。
      • ShipOrderAction:模拟发货、生成运单号。
      • FinishOrderAction:模拟订单完成发放积分。
      • CancelOrderAction:模拟取消订单、释放库存 / 退款说明。
  • 编排层:
    • OrderProcessService:连接状态机与 Action 的“中枢”,对外暴露 fire(OrderContext, OrderEvent)
  • Demo 入口:
    • OrderStateMachineDemo:控制台演示两种使用方式。

2. 推荐分层思路(重要)

  1. 状态机层:OrderStateMachine

    • 只做 1 件事:给你答案——“当前状态 + 事件 → 合法吗?下一状态是什么?”
    • 完全不关心支付 / 物流 / 库存等业务细节。
  2. 业务动作层:OrderEventAction + 各实现

    • 每个事件各自负责业务副作用
    • 例如:PAY 负责调支付、对账、预占库存;SHIP 负责出库与物流;FINISH 负责积分;CANCEL 负责释放库存、退款。
  3. 编排层:OrderProcessService

    • 对外只提供一个统一方法:fire(order, event)
    • 内部执行顺序:
      1. 用状态机 transit 校验并计算 target 状态
      2. 找到该事件对应的 OrderEventAction,执行业务动作
      3. 业务成功后,将 OrderContext 的状态更新为 target 状态。

三、各类职责与关键代码

1. 订单状态:OrderStatus

  • 描述生命周期:已创建 → 已支付 → 已发货 → 已完成 / 已取消。
  • 在你的代码里已经有详细 Javadoc,主要是配合状态机使用。
/**
 * 演示用订单生命周期状态(与 {@link OrderStateMachine} 中允许的迁移一致)。
 * <p>
 * 典型路径:已创建  已支付  已发货  已完成;在「已创建 / 已支付」阶段可取消。
 *
 * @author yunze
 * @since 2026/04/08
 */
public enum OrderStatus {
    /** 订单已创建,待支付 */
    CREATED,
    /** 已支付,待发货 */
    PAID,
    /** 已发货,待确认完成 */
    SHIPPED,
    /** 订单正常结束 */
    FINISHED,
    /** 订单已取消 */
    CANCELED
}

2. 订单事件:OrderEvent

  • 表示“触发状态变化的业务动作”,如 PAYSHIPFINISHCANCEL
  • 实际系统中,这些事件可能来自:
    • 用户操作(点击支付 / 确认收货)。
    • 第三方回调(支付成功通知、物流签收回调)。
    • 定时任务(超时未支付自动取消)。
/**
 * 触发订单状态迁移的业务事件,由外部动作(支付成功、发货、确认收货、取消等)映射而来。
 *
 * @author yunze
 * @since 2026/04/08
 * @see OrderStateMachine
 */
public enum OrderEvent {
    /** 支付完成 */
    PAY,
    /** 商家发货 */
    SHIP,
    /** 确认收货 / 流程完结 */
    FINISH,
    /** 取消订单 */
    CANCEL
}

3. 状态机:OrderStateMachine

核心点:用一个嵌套 EnumMap 存所有合法的迁移规则。

/**
 * 轻量级订单状态机:用二维表(当前状态  事件  目标状态)描述合法迁移,不依赖 Spring。
 * <p>
 * 本示例未包含「已发货」后的取消等扩展路径,仅演示最小闭环与非法迁移校验。
 * <p>
 * <b>业务代码应写在哪里:</b>不要写在本类中。每个事件对应的支付、库存、物流等应实现
 * {@link OrderEventAction},由 {@link OrderProcessService} 在合法迁移确定后统一调用。
 *
 * @author yunze
 * @since 2026/04/08
 * @see OrderEventAction
 * @see OrderProcessService
 */
public class OrderStateMachine {
    /**
     * 状态转移表:外层 key 为当前 {@link OrderStatus},内层为「事件  下一状态」。
     */
    private final Map<OrderStatus, Map<OrderEvent, OrderStatus>> transitions = new EnumMap<>(OrderStatus.class);

    /**
     * 注册默认转移规则:
     * <ul>
     *   <li>CREATED + PAY  PAID;CREATED + CANCEL  CANCELED</li>
     *   <li>PAID + SHIP  SHIPPED;PAID + CANCEL  CANCELED</li>
     *   <li>SHIPPED + FINISH  FINISHED</li>
     * </ul>
     */
    public OrderStateMachine() {
        addTransition(OrderStatus.CREATED, OrderEvent.PAY, OrderStatus.PAID);
        addTransition(OrderStatus.CREATED, OrderEvent.CANCEL, OrderStatus.CANCELED);
        addTransition(OrderStatus.PAID, OrderEvent.SHIP, OrderStatus.SHIPPED);
        addTransition(OrderStatus.PAID, OrderEvent.CANCEL, OrderStatus.CANCELED);
        addTransition(OrderStatus.SHIPPED, OrderEvent.FINISH, OrderStatus.FINISHED);
    }

    /**
     * 根据当前状态与事件计算下一状态。
     *
     * @param currentStatus 当前订单状态
     * @param event         发生的事件
     * @return 迁移后的目标状态
     * @throws IllegalStateException 当当前状态下不允许该事件时(无定义转移)
     */
    public OrderStatus transit(OrderStatus currentStatus, OrderEvent event) {
        Map<OrderEvent, OrderStatus> statusMap = transitions.get(currentStatus);
        if (statusMap == null || !statusMap.containsKey(event)) {
            throw new IllegalStateException("不支持的状态流转: " + currentStatus + " --" + event + "--> ?");
        }
        return statusMap.get(event);
    }

    /**
     * 注册一条有向边:在状态 {@code from} 上发生 {@code event} 时进入 {@code to}。
     */
    private void addTransition(OrderStatus from, OrderEvent event, OrderStatus to) {
        transitions.computeIfAbsent(from, key -> new EnumMap<>(OrderEvent.class)).put(event, to);
    }
}
  • transit(currentStatus, event)
    • 查询 transitions
    • 若找不到映射,抛出 IllegalStateException(非法状态流转)。
    • 找得到就返回目标 OrderStatus

注意:这里没有任何支付 / 库存 / 物流的代码,只是一张规则表。

4. 订单上下文:OrderContext

  • 模拟现实中的“订单聚合根 + 衍生字段”,方便 Demo 中展示业务变化:

    • orderId:订单号(long)。
    • status:当前订单状态。
    • payTransactionId:支付后生成的外部流水号。
    • logisticsTrackingNo:发货后的运单号。
    • memberPointsDelta:完结时发放的积分。
  • 真实系统里,这个类可以对应:

    • 数据库中的订单主表 + 关联字段;
    • 或者一个包含订单与相关子对象的 Domain Model。
/**
 * 演示用订单聚合上下文:承载当前状态及本 demo 中各事件写入的「业务结果」字段。
 * <p>
 * 真实项目中对应:订单实体 + 支付单、物流单、库存流水等,通常与状态一起持久化。
 */
public class OrderContext {

    private final long orderId;
    private OrderStatus status;

    /** 支付成功后写入的第三方流水号(demo) */
    private String payTransactionId;
    /** 发货后写入的物流单号(demo) */
    private String logisticsTrackingNo;
    /** 完成后累计的积分(demo) */
    private int memberPointsDelta;

    public OrderContext(long orderId, OrderStatus initialStatus) {
        this.orderId = orderId;
        this.status = Objects.requireNonNull(initialStatus);
    }

    public long getOrderId() {
        return orderId;
    }

    public OrderStatus getStatus() {
        return status;
    }

    /**  {@link OrderProcessService} 在业务成功后调用,更新持久化状态 */
    public void setStatus(OrderStatus status) {
        this.status = Objects.requireNonNull(status);
    }

    public String getPayTransactionId() {
        return payTransactionId;
    }

    public void setPayTransactionId(String payTransactionId) {
        this.payTransactionId = payTransactionId;
    }

    public String getLogisticsTrackingNo() {
        return logisticsTrackingNo;
    }

    public void setLogisticsTrackingNo(String logisticsTrackingNo) {
        this.logisticsTrackingNo = logisticsTrackingNo;
    }

    public int getMemberPointsDelta() {
        return memberPointsDelta;
    }

    public void addMemberPoints(int delta) {
        this.memberPointsDelta += delta;
    }

    @Override
    public String toString() {
        return "OrderContext{"
                + "orderId=" + orderId
                + ", status=" + status
                + ", payTransactionId='" + payTransactionId + '\''
                + ", logisticsTrackingNo='" + logisticsTrackingNo + '\''
                + ", memberPointsDelta=" + memberPointsDelta
                + '}';
    }
}

5. 事件业务接口:OrderEventAction

  • 定位:某个 OrderEvent 对应的一整套业务操作。
  • 签名非常简单:
/**
 * 某一 {@link OrderEvent} 对应的业务副作用:支付、发货、完结、取消等应各自实现本接口。
 * <p>
 * <b>放置位置约定(与状态机的关系):</b>
 * <ul>
 *   <li>{@link OrderStateMachine} 只维护「状态 × 事件  下一状态」,不包含业务代码。</li>
 *   <li>本接口的实现类放在业务层(本 demo  {@code com.yz.statemachine.order.action} 包),
 *        {@link OrderProcessService} 在合法迁移确定后调用。</li>
 *   <li>若使用 Spring,可将各 Action 声明为 Bean,在 ProcessService 中按事件注入或从 Map 获取。</li>
 * </ul>
 *
 * @author yunze
 * @since 2026/04/09
 */
@FunctionalInterface
public interface OrderEventAction {

    /**
     * 执行该事件对应的业务逻辑(调支付、写库存、发物流等)。成功返回后,由编排层更新订单状态。
     *
     * @param order 当前订单上下文(只读/有限写入由实现约定;demo 中由 action 写入扩展字段)
     * @throws RuntimeException 业务失败时应抛出,编排层不应更新状态(见 {@link OrderProcessService})
     */
    void execute(OrderContext order);
}
  • 放置位置:
    • 状态机类放 state 包 / rule 包;
    • 业务动作实现放在类似 order.action 的业务包里;
    • 编排层(OrderProcessService)在 service 层。

6. 四个具体事件动作

都在 com.yz.statemachine.order.action 包下,每个类一个事件:

  • PayOrderAction

    • 生成模拟支付流水号 PAY-xxxxxx
    • 输出“预占库存、写支付流水”等说明。
    /**
     * 支付事件业务:模拟拉起支付、扣款成功、预占库存、写支付流水。
     */
    public class PayOrderAction implements OrderEventAction {
    
        @Override
        public void execute(OrderContext order) {
            // 真实场景:调支付网关、幂等校验、与订单金额对账等
            String txnId = "PAY-" + UUID.randomUUID().toString().substring(0, 8);
            order.setPayTransactionId(txnId);
            System.out.println("    [业务-PAY] 支付成功,流水号=" + txnId + ",订单=" + order.getOrderId()
                    + "(模拟:预占库存 SKU、写入 oms_payment 流水)");
        }
    }
  • ShipOrderAction

    • 生成模拟运单号 SFxxxxxxxxx
    • 输出“出库扣实物库存、写物流订阅”等说明。
    /**
     * 发货事件业务:模拟创建物流单、写运单号、通知承运商。
     */
    public class ShipOrderAction implements OrderEventAction {
    
        @Override
        public void execute(OrderContext order) {
            // 真实场景:WMS 出库、对接物流 API、回写运单与发货时间
            String tracking = "SF" + ThreadLocalRandom.current().nextInt(100000000, 999999999);
            order.setLogisticsTrackingNo(tracking);
            System.out.println("    [业务-SHIP] 已发货,运单号=" + tracking + ",订单=" + order.getOrderId()
                    + "(模拟:出库扣实物库存、写入物流订阅)");
        }
    }
  • FinishOrderAction

    • 给用户加 50 积分(addMemberPoints(50));
    • 输出“结算佣金、评价提醒”等说明。
    /**
     * 确认收货/完结事件业务:模拟积分、结算分账等收尾逻辑。
     */
    public class FinishOrderAction implements OrderEventAction {
    
        @Override
        public void execute(OrderContext order) {
            int points = 50;
            order.addMemberPoints(points);
            System.out.println("    [业务-FINISH] 订单完结,发放积分 +" + points + ",订单=" + order.getOrderId()
                    + "(模拟:解冻营销预算、结算佣金、触发评价提醒)");
        }
    }
  • CancelOrderAction

    • 打印“释放预占库存;已付则创建退款单”等说明。
    /**
     * 取消事件业务:模拟释放库存、未支付则关闭单、已支付则走退款流程。
     */
    public class CancelOrderAction implements OrderEventAction {
    
        @Override
        public void execute(OrderContext order) {
            // 真实场景:按当前是否已支付分支:退款 API、释放预占库存、关单原因落库
            System.out.println("    [业务-CANCEL] 取消订单,订单=" + order.getOrderId()
                    + "(模拟:释放预占库存;若已付则创建退款单并异步回调)");
        }
    }

在真实项目中,这些地方会调用:

  • 支付网关 SDK / RPC;
  • 库存/仓储系统;
  • 营销与积分系统;
  • 退款系统等。

7. 编排服务:OrderProcessService

关键方法 fire(OrderContext order, OrderEvent event)

  1. 拿当前状态:from = order.getStatus()
  2. 用状态机算出目标状态:to = stateMachine.transit(from, event)
  3. 找到对应 Action:OrderEventAction action = actions.get(event)
  4. 调用业务逻辑:action.execute(order)
  5. 业务成功后,最后更新状态:order.setStatus(to)

默认的 actions 映射在构造函数里直接用 EnumMap new 了四个 Action,你在 Spring 里可以改成注入。

/**
 * 订单流程编排:连接「状态机规则」与「事件业务实现」的枢纽。
 * <p>
 * <b>推荐执行顺序(与多数电商落地一致):</b>
 * <ol>
 *   <li>用 {@link OrderStateMachine#transit(OrderStatus, OrderEvent)} 校验并解析目标状态(只做规则,不写业务)。</li>
 *   <li>调用 {@link OrderEventAction#execute(OrderContext)} 完成副作用;失败则抛异常,<b>不修改</b>订单状态。</li>
 *   <li>业务成功后,将上下文中的状态更新为步骤 1 得到的目标状态(demo 直接改内存;生产环境应在同一事务中写库)。</li>
 * </ol>
 * 若需「先落库再调第三方」,可改为:乐观锁更新状态  调支付;失败则补偿回滚状态(需额外设计)。
 */
public class OrderProcessService {

    private final OrderStateMachine stateMachine;
    private final Map<OrderEvent, OrderEventAction> actions;

    public OrderProcessService() {
        this(new OrderStateMachine(), defaultActions());
    }

    public OrderProcessService(OrderStateMachine stateMachine, Map<OrderEvent, OrderEventAction> actions) {
        this.stateMachine = Objects.requireNonNull(stateMachine);
        this.actions = Objects.requireNonNull(actions);
    }

    private static Map<OrderEvent, OrderEventAction> defaultActions() {
        Map<OrderEvent, OrderEventAction> map = new EnumMap<>(OrderEvent.class);
        map.put(OrderEvent.PAY, new PayOrderAction());
        map.put(OrderEvent.SHIP, new ShipOrderAction());
        map.put(OrderEvent.FINISH, new FinishOrderAction());
        map.put(OrderEvent.CANCEL, new CancelOrderAction());
        return map;
    }

    /**
     * 触发一次状态迁移并执行业务:合法则先跑业务,成功后再改状态。
     *
     * @param order 订单上下文
     * @param event 业务事件
     * @throws IllegalStateException 非法迁移(与状态机一致)
     * @throws RuntimeException      业务动作失败
     */
    public void fire(OrderContext order, OrderEvent event) {
        OrderStatus from = order.getStatus();
        OrderStatus to = stateMachine.transit(from, event);
        OrderEventAction action = actions.get(event);
        if (action == null) {
            throw new IllegalStateException("未注册事件对应的业务实现: " + event);
        }
        action.execute(order);
        order.setStatus(to);
    }

    public OrderStateMachine getStateMachine() {
        return stateMachine;
    }
}

四、示例执行流程

OrderStateMachineDemo.main 中有两段 Demo,你可以随时跑来复习设计。

1. 纯状态机演示(不含业务)

  • 使用 OrderStateMachine 直接对 OrderStatus 做状态迁移。
  • 流程:
    • CREATED → PAY → PAID
    • PAID → SHIP → SHIPPED
    • SHIPPED → FINISH → FINISHED
    • FINISHED 上再尝试 CANCEL → 抛 IllegalStateException

用途:只验证规则正确性

2. 状态机 + 业务 Action 演示(推荐形态)

  • 使用 OrderProcessServiceOrderContext
  • 演示 2 条路径:
    1. 正常链路:CREATED + PAY → PAID → SHIP → SHIPPED → FINISH → FINISHED
      • 每步会输出:
        • 状态变化:from → to
        • 业务日志:支付流水号、运单号、积分等。
      • 最后打印完整 OrderContext,方便查看状态和字段变更。
    2. 取消链路:CREATED + CANCEL → CANCELED
      • 打印取消业务说明。

通过这两个 Demo,你可以很清晰地看到:

  • 状态机负责“能 / 不能 + 去哪儿”;
  • Action 负责“做什么事”;
  • 编排服务统一调度两者。

五、真实项目落地建议(速记)

  • 状态机层(纯规则)

    • 用类似本 Demo 的 EnumMap 或表驱动方式;
    • 或者使用 Spring Statemachine / Squirrel 等库时,也尽量把业务放在 Action / Listener 中,而不是写进状态机配置。
  • 事件粒度

    • 按“订单业务语义”设计事件:创建、支付成功、发货、签收、取消、退款成功等;
    • 避免过细如“更新字段 X”这种不具有业务意义的事件。
  • 幂等与事务

    • 业务 Action 内部要处理幂等(尤其是支付、退款、库存扣减)。
    • 状态机迁移 + 状态写库 + 业务动作,最好在同一事务模型中设计:是“先改库再调外部 + 补偿”,还是“先调外部成功再改库”——根据实际系统定。
  • 扩展新状态 / 新事件

    1. OrderStatus / OrderEvent 中加新枚举;
    2. OrderStateMachine 构造方法中 addTransition 对应规则;
    3. 新增对应的 OrderEventAction 实现;
    4. OrderProcessServiceactions Map 中注册。

六、总结参考

真实项目里做“订单状态机 + 业务编排”时,可以按下面的 checklist 来对照本 Demo:

  1. 先画出状态图(类似 OrderStatusOrderStateMachine 的 rules)。
  2. 定义事件枚举OrderEvent)。
  3. 写一个纯规则 StateMachine(不含业务)。
  4. 写业务动作接口 + 若干实现(参考 OrderEventAction + action 包)。
  5. 写编排服务(参考 OrderProcessService.fire 的顺序与错误处理)。
  6. 写一个 Demo / 单测(类似 OrderStateMachineDemo),跑一遍全链路是否符合预期。

你只要记得这张 mental map:
「状态机 = 规则」 + 「Action = 业务」 + 「Service = 编排」,再结合这个 Demo 的代码结构,就可以很快在任何项目里复用。