大数跨境
0
0

【摸鱼学 Java】状态机设计:用枚举和注解搞定复杂状态流转

【摸鱼学 Java】状态机设计:用枚举和注解搞定复杂状态流转 Jerry出海记
2025-10-23
5

在开发中,我们经常遇到状态流转的场景 —— 比如订单从 “待支付” 到 “已支付” 再到 “已发货”,或者流程审批从 “提交” 到 “审核中” 再到 “通过”。如果用一堆if-else来处理这些状态转换,代码会变得臃肿且难以维护。今天咱们用枚举 + 注解实现一个轻量级状态机,让状态流转逻辑清晰到 “一眼看穿”。

一、什么是状态机?

状态机(State Machine)是一种数学模型,用来描述对象在不同状态下的行为和状态转换规则。它有 4 个核心要素:

  • 状态(State):对象的当前状态(如订单的 “待支付”“已支付”)。
  • 事件(Event):触发状态转换的动作(如 “支付”“发货”)。
  • 转换(Transition):从一个状态通过某个事件转换到另一个状态(如 “待支付”+“支付事件”→“已支付”)。
  • 动作(Action):状态转换时执行的逻辑(如支付成功后扣库存)。

举个订单状态流转的例子:

 


   
    
   待支付 →[支付事件]→ 已支付 →[发货事件]→ 已发货 →[确认收货事件]→ 已完成
       ↘[取消事件]→ 已取消

 

二、为什么用枚举 + 注解实现?

实现状态机的方式有很多(比如设计模式、框架依赖),但枚举 + 注解的组合有三个明显优势:

  • 枚举天然适合表示状态和事件:状态和事件都是有限且固定的集合,枚举的特性完美匹配。
  • 注解能清晰标记转换规则:用注解标记 “从哪个状态,通过哪个事件,转换到哪个状态”,规则一目了然。
  • 轻量级无依赖:不需要引入额外框架,纯 JDK 语法就能实现,适合中小型项目。

三、手把手实现状态机

我们以 “订单状态流转” 为例,一步步用枚举 + 注解搭建状态机。

(一)定义状态和事件(枚举登场)

首先用枚举定义订单的状态(State) 和可能触发的事件(Event)

 


   
    
   // 订单状态枚举
enum
 OrderState {
    PENDING_PAYMENT("待支付"),    // 初始状态
    PAID("已支付"),
    SHIPPED("已发货"),
    COMPLETED("已完成"),
    CANCELLED("已取消");

    private
 final String desc;
    OrderState(String desc) {
        this
.desc = desc;
    }
    // getter方法

    public
 String getDesc() { return desc; }
}

// 订单事件枚举(触发状态转换的动作)

enum
 OrderEvent {
    PAY("支付"),         // 支付事件:待支付→已支付
    SHIP("发货"),        // 发货事件:已支付→已发货
    CONFIRM_RECEIPT("确认收货"), // 确认收货:已发货→已完成
    CANCEL("取消");      // 取消事件:待支付→已取消

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

 

(二)定义转换规则(注解登场)

用注解标记状态转换规则:从哪个源状态(source),通过哪个事件(event),转换到哪个目标状态(target),以及转换时执行的动作(action)。

 


   
    
   import java.lang.annotation.*;

// 标记状态转换规则的注解

@Target(ElementType.TYPE)
 // 作用在枚举上(状态枚举)
@Retention(RetentionPolicy.RUNTIME)
 // 运行时可见(反射读取)
@interface
 Transition {
    OrderState source(); // 源状态(从哪个状态开始)
    OrderEvent event();  // 触发事件
    OrderState target(); // 目标状态(转换到哪个状态)
    Class<? extends Action> action() default EmptyAction.class; // 转换时执行的动作
}

// 动作接口(状态转换时执行的逻辑)

interface
 Action {
    void
 execute(); // 执行动作
}

// 空动作(默认不执行任何逻辑)

class
 EmptyAction implements Action {
    @Override

    public
 void execute() {}
}

 

(三)绑定状态与转换规则(枚举 + 注解结合)

给状态枚举添加@Transition注解,明确每个状态能通过哪些事件转换到其他状态:

 


   
    
   // 给订单状态枚举绑定转换规则
enum
 OrderState {
    // 待支付状态:可通过支付事件→已支付,或取消事件→已取消

    @Transition(source = PENDING_PAYMENT, event = OrderEvent.PAY, target = PAID, action = PayAction.class)

    @Transition(source = PENDING_PAYMENT, event = OrderEvent.CANCEL, target = CANCELLED, action = CancelAction.class)

    PENDING_PAYMENT("待支付"),

    // 已支付状态:可通过发货事件→已发货

    @Transition(source = PAID, event = OrderEvent.SHIP, target = SHIPPED, action = ShipAction.class)

    PAID("已支付"),

    // 已发货状态:可通过确认收货事件→已完成

    @Transition(source = SHIPPED, event = OrderEvent.CONFIRM_RECEIPT, target = COMPLETED, action = CompleteAction.class)

    SHIPPED("已发货"),

    // 已完成/已取消:没有后续转换(终端状态)

    COMPLETED("已完成"),
    CANCELLED("已取消");

    // 省略构造方法和getter(同上)

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

// 具体动作实现(转换时执行的业务逻辑)

class
 PayAction implements Action {
    @Override

    public
 void execute() {
        System.out.println("执行支付动作:扣减库存、生成支付记录");
    }
}

class
 CancelAction implements Action {
    @Override

    public
 void execute() {
        System.out.println("执行取消动作:恢复库存、发送取消通知");
    }
}

// ShipAction、CompleteAction类似,省略...

 

(四)实现状态机核心逻辑

状态机的核心功能是:接收当前状态和事件,根据@Transition注解的规则,返回目标状态并执行动作。

 


   
    
   import java.lang.annotation.Annotation;

public
 class StateMachine {
    // 处理状态转换的核心方法

    public
 static OrderState transition(OrderState currentState, OrderEvent event) throws Exception {
        // 1. 获取当前状态上的所有@Transition注解

        Annotation[] annotations = currentState.getClass().getField(currentState.name()).getAnnotationsByType(Transition.class);

        // 2. 遍历注解,找到匹配当前事件的转换规则

        for
 (Annotation anno : annotations) {
            Transition
 transition = (Transition) anno;
            if
 (transition.event() == event) {
                // 3. 执行转换动作

                Action
 action = transition.action().newInstance();
                action.execute();
                // 4. 返回目标状态

                return
 transition.target();
            }
        }

        // 未找到匹配的转换规则(非法状态转换)

        throw
 new IllegalStateException("状态转换非法:" + currentState.getDesc() + " 不能执行 " + event.desc);
    }
}

 

四、实战:订单状态流转测试

用一个例子模拟订单从创建到完成的全流程:

 


   
    
   public class OrderDemo {
    public
 static void main(String[] args) {
        // 初始状态:待支付

        OrderState
 currentState = OrderState.PENDING_PAYMENT;
        System.out.println("初始状态:" + currentState.getDesc());

        try
 {
            // 1. 执行支付事件

            currentState = StateMachine.transition(currentState, OrderEvent.PAY);
            System.out.println("支付后状态:" + currentState.getDesc()); // 已支付

            // 2. 执行发货事件

            currentState = StateMachine.transition(currentState, OrderEvent.SHIP);
            System.out.println("发货后状态:" + currentState.getDesc()); // 已发货

            // 3. 执行确认收货事件

            currentState = StateMachine.transition(currentState, OrderEvent.CONFIRM_RECEIPT);
            System.out.println("确认收货后状态:" + currentState.getDesc()); // 已完成

        } catch (Exception e) {
            System.out.println("状态转换失败:" + e.getMessage());
        }
    }
}

 

输出结果:

 


   
    
   初始状态:待支付
执行支付动作:扣减库存、生成支付记录
支付后状态:已支付
执行发货动作:生成物流单、更新库存
发货后状态:已发货
执行完成动作:生成交易完成记录、发送好评提醒
确认收货后状态:已完成

 

如果尝试非法转换(比如 “已支付” 状态执行 “取消” 事件):

 


   
    
   // 错误示例:已支付状态不能执行取消事件
currentState = OrderState.PAID;
StateMachine.transition(currentState, OrderEvent.CANCEL); 
// 抛出异常:状态转换非法:已支付 不能执行 取消

 

五、状态机的扩展与优化

(一)支持带参数的动作

实际业务中,动作可能需要订单 ID、用户信息等参数。可以修改Action接口,让动作执行时能接收参数:

 


   
    
   // 带参数的动作接口
interface
 Action {
    void
 execute(Object... params); // 可变参数接收业务数据
}

// 支付动作示例(需要订单ID)

class
 PayAction implements Action {
    @Override

    public
 void execute(Object... params) {
        String
 orderId = (String) params[0];
        System.out.println("订单" + orderId + "执行支付动作:扣减库存");
    }
}

// 状态机转换方法同步修改(传递参数)

public
 static OrderState transition(OrderState currentState, OrderEvent event, Object... params) throws Exception {
    // ... 省略查找注解逻辑

    Action
 action = transition.action().newInstance();
    action.execute(params); // 传递参数
    // ...

}

 

(二)批量校验转换规则

启动时校验所有状态转换规则是否完整,避免运行时出错:

 


   
    
   // 校验所有状态是否有完整的转换规则
public
 static void validate() {
    for
 (OrderState state : OrderState.values()) {
        Annotation[] annotations = state.getClass().getField(state.name()).getAnnotationsByType(Transition.class);
        if
 (annotations.length == 0 && !isTerminal(state)) {
            System.out.println("警告:状态" + state.getDesc() + "没有定义任何转换规则");
        }
    }
}

// 判断是否为终端状态(无需转换)

private
 static boolean isTerminal(OrderState state) {
    return
 state == OrderState.COMPLETED || state == OrderState.CANCELLED;
}

 

六、避坑指南

  1. 终端状态无需转换规则像 “已完成”“已取消” 这类终端状态,不需要定义转换规则,避免画蛇添足。

  2. 避免状态转换闭环比如 “已支付→已发货→已支付” 这种闭环转换,可能导致业务逻辑混乱,需谨慎设计。

  3. 动作逻辑要轻量状态转换时的动作(如扣库存)应尽量简短,复杂逻辑建议异步处理,避免阻塞状态机。

  4. 线程安全问题如果状态机在多线程环境下使用(如并发修改订单状态),需给transition方法加锁或用原子类保证线程安全。

  5. 注解重复使用的坑同一个状态的多个@Transition注解,source必须相同(都等于当前状态),否则会出现逻辑混乱。

七、什么时候用这种实现?

这种枚举 + 注解的状态机适合:

  • 状态和事件数量较少(枚举不宜过多,否则维护成本上升)。
  • 中小型项目或独立模块(无需引入 Spring StateMachine 等重型框架)。
  • 状态流转规则相对固定(不会频繁变更)。

如果是大型项目或状态规则复杂(如包含条件判断、子状态机),建议使用成熟框架(如 Spring StateMachine)。

总结

用枚举定义状态和事件,用注解标记转换规则,再配合一个简单的状态机核心类,就能轻松实现清晰可控的状态流转逻辑。相比一堆if-else,这种方式让状态转换规则 “可视化”,既方便开发也便于后期维护。

下次遇到订单、审批、流程类的状态管理需求,不妨试试这个思路。你在项目中是如何处理状态流转的?欢迎在评论区分享你的方案~

【声明】内容源于网络
0
0
Jerry出海记
跨境分享社 | 长期分享行业动态
内容 44206
粉丝 0
Jerry出海记 跨境分享社 | 长期分享行业动态
总阅读247.7k
粉丝0
内容44.2k