在开发中,我们经常遇到状态流转的场景 —— 比如订单从 “待支付” 到 “已支付” 再到 “已发货”,或者流程审批从 “提交” 到 “审核中” 再到 “通过”。如果用一堆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;
}
六、避坑指南
终端状态无需转换规则像 “已完成”“已取消” 这类终端状态,不需要定义转换规则,避免画蛇添足。
避免状态转换闭环比如 “已支付→已发货→已支付” 这种闭环转换,可能导致业务逻辑混乱,需谨慎设计。
动作逻辑要轻量状态转换时的动作(如扣库存)应尽量简短,复杂逻辑建议异步处理,避免阻塞状态机。
线程安全问题如果状态机在多线程环境下使用(如并发修改订单状态),需给
transition方法加锁或用原子类保证线程安全。注解重复使用的坑同一个状态的多个
@Transition注解,source必须相同(都等于当前状态),否则会出现逻辑混乱。
七、什么时候用这种实现?
这种枚举 + 注解的状态机适合:
-
状态和事件数量较少(枚举不宜过多,否则维护成本上升)。 -
中小型项目或独立模块(无需引入 Spring StateMachine 等重型框架)。 -
状态流转规则相对固定(不会频繁变更)。
如果是大型项目或状态规则复杂(如包含条件判断、子状态机),建议使用成熟框架(如 Spring StateMachine)。
总结
用枚举定义状态和事件,用注解标记转换规则,再配合一个简单的状态机核心类,就能轻松实现清晰可控的状态流转逻辑。相比一堆if-else,这种方式让状态转换规则 “可视化”,既方便开发也便于后期维护。
下次遇到订单、审批、流程类的状态管理需求,不妨试试这个思路。你在项目中是如何处理状态流转的?欢迎在评论区分享你的方案~

