状态机这个词听起来偏理论,但放到业务系统里,其实就是在回答一个很具体的问题:一个对象当前处于什么状态、什么操作能触发变化、变化之后又该执行哪些动作。以订单、审批单、签章流程这类对象为例,只要生命周期里存在多个阶段,状态机思路几乎都会出现。
这篇文章就围绕 Java 中最常见的两种实现方式展开:一种是把状态判断写进业务代码,另一种是把流转规则抽出来集中管理。看完之后,你可以据此判断自己的场景是继续保持轻量实现,还是该尽早切到显式状态机,避免后期规则失控。
什么是状态机
状态机本质上是一个模型,用来描述对象在生命周期内如何从一个状态走到另一个状态。放到代码里理解,可以抓住三个核心元素:
- 状态(State):对象当前所处的阶段,比如订单是“待支付”还是“已发货”。
- 事件(Event):触发变化的动作或条件,比如用户点击“支付”。
- 转换(Transition):在某个状态下,遇到某个事件后,流转到目标状态。
可以把它理解成一句固定句式:对象在某个状态下,因为某个事件,转换到了另一个状态。
Java 中两种常见实现方式
显式状态机
显式状态机会把状态和转换规则单独抽出来管理,通常有清晰的转换表、转换图,或者直接使用框架,例如 Spring StateMachine。它的重点在于:规则集中、边界清楚、非法流转统一拦截。
这种做法通常有几个特点:
- 状态和转换规则集中配置
- 存在明确的状态转换表或流程图
- 非法转换可以统一校验和拦截
- 更适合状态多、规则复杂、变更频繁的业务
隐式状态机
隐式状态机则更常见于日常业务开发。状态值通常只是数据库中的一个字段,具体能不能流转,全靠各个业务方法里的 if/else 或 switch 去判断。
它的特点也很明显:
- 状态只是一个普通字段
- 流转逻辑分散在各个方法中
- 没有统一的规则中心
- 实现简单,但随着业务变复杂,维护成本会快速上升
两者的差别,不在于有没有“状态”这个概念,而在于状态规则是否被集中管理。
隐式状态机怎么写
这是很多业务系统最常见的形态。下面用订单状态举例说明。

数据库中的状态字段
CREATE TABLE orders (
id INT PRIMARY KEY,
status INT NOT NULL DEFAULT 0 COMMENT '0=待支付, 1=已支付, 2=已发货, 3=已完成, 4=已取消'
);这里的 status 就承担了状态机里的“状态”角色。
用枚举描述状态
public enum OrderStatus {
UNPAID(0, "待支付"),
PAID(1, "已支付"),
SHIPPED(2, "已发货"),
COMPLETED(3, "已完成"),
CANCELLED(4, "已取消");
private Integer code;
private String desc;
}即便是隐式状态机,状态枚举最好也要定义清楚。这样至少能避免到处写魔法值。
业务方法里直接做状态判断
public void payOrder(Integer orderId) {
Order order = orderRepository.findById(orderId);
// 隐式校验:只有待支付才能支付
if (!OrderStatus.UNPAID.getCode().equals(order.getStatus())) {
throw new BusinessException("当前状态不允许支付");
}
order.setStatus(OrderStatus.PAID.getCode());
orderRepository.save(order);
}
public void shipOrder(Integer orderId) {
Order order = orderRepository.findById(orderId);
// 隐式校验:只有已支付才能发货
if (!OrderStatus.PAID.getCode().equals(order.getStatus())) {
throw new BusinessException("当前状态不允许发货");
}
order.setStatus(OrderStatus.SHIPPED.getCode());
orderRepository.save(order);
}
public void cancelOrder(Integer orderId) {
Order order = orderRepository.findById(orderId);
// 隐式校验:已发货和已完成不能取消
if (OrderStatus.SHIPPED.getCode().equals(order.getStatus())
|| OrderStatus.COMPLETED.getCode().equals(order.getStatus())) {
throw new BusinessException("当前状态不允许取消");
}
order.setStatus(OrderStatus.CANCELLED.getCode());
orderRepository.save(order);
}对应的流转关系可以概括为:
UNPAID(0) ──[支付]──→ PAID(1) ──[发货]──→ SHIPPED(2) ──[确认收货]──→ COMPLETED(3)
│ │
└──[取消]──→ CANCELLED(4) ←──[取消]──┘
这种写法的优点是直接、上手快,尤其适合状态很少的场景。但它也有明显问题:规则散落在多个方法里,新增状态或修改流转时,很容易漏改某个分支。
显式状态机怎么实现
如果业务状态开始变多,或者流转规则经常调整,更稳妥的做法是把规则抽出来,形成统一入口。

方式一:状态转换表
最直接的方式,是用一张转换表集中声明“当前状态 + 事件 = 目标状态”。
public class OrderStateMachine {
// 转换规则表:Map<当前状态, Map<事件, 目标状态>>
private static final Map<Integer, Map<String, Integer>> TRANSITIONS = new HashMap<>();
static {
// 待支付状态下的合法转换
Map<String, Integer> unpaidTransitions = new HashMap<>();
unpaidTransitions.put("PAY", 1); // 支付 → 已支付
unpaidTransitions.put("CANCEL", 4); // 取消 → 已取消
TRANSITIONS.put(0, unpaidTransitions);
// 已支付状态下的合法转换
Map<String, Integer> paidTransitions = new HashMap<>();
paidTransitions.put("SHIP", 2); // 发货 → 已发货
paidTransitions.put("CANCEL", 4); // 取消 → 已取消
TRANSITIONS.put(1, paidTransitions);
// 已发货状态下的合法转换
Map<String, Integer> shippedTransitions = new HashMap<>();
shippedTransitions.put("CONFIRM", 3); // 确认 → 已完成
TRANSITIONS.put(2, shippedTransitions);
}
/**
* 执行状态转换.
*/
public static Integer transition(Integer currentState, String event) {
Map<String, Integer> allowed = TRANSITIONS.get(currentState);
if (allowed == null || !allowed.containsKey(event)) {
throw new IllegalStateException(
"非法状态转换: 状态=" + currentState + ", 事件=" + event);
}
return allowed.get(event);
}
}业务代码就可以简化成统一调用状态机:
public void payOrder(Integer orderId) {
Order order = orderRepository.findById(orderId);
// 由状态机统一校验和转换
Integer newStatus = OrderStateMachine.transition(order.getStatus(), "PAY");
order.setStatus(newStatus);
orderRepository.save(order);
}这种方式的价值在于,所有合法流转都能在一个地方看清楚,排查问题和扩展规则都会轻松很多。
方式二:枚举 + 方法
如果你希望状态本身就具备行为,也可以把转换逻辑直接写进枚举中。
public enum OrderStatus {
UNPAID(0) {
@Override
public OrderStatus onPay() { return PAID; }
@Override
public OrderStatus onCancel() { return CANCELLED; }
},
PAID(1) {
@Override
public OrderStatus onShip() { return SHIPPED; }
@Override
public OrderStatus onCancel() { return CANCELLED; }
},
SHIPPED(2) {
@Override
public OrderStatus onConfirm() { return COMPLETED; }
},
COMPLETED(3),
CANCELLED(4);
private Integer code;
// 默认实现:抛异常表示不允许该操作
public OrderStatus onPay() { throw new IllegalStateException("当前状态不允许支付"); }
public OrderStatus onShip() { throw new IllegalStateException("当前状态不允许发货"); }
public OrderStatus onConfirm() { throw new IllegalStateException("当前状态不允许确认"); }
public OrderStatus onCancel() { throw new IllegalStateException("当前状态不允许取消"); }
}这种写法的好处是每个状态自己知道允许哪些操作,调用方不必关心完整的判断过程。相比转换表,它更偏面向对象;相比隐式判断,它的规则仍然是集中可见的。
隐式与显式状态机怎么选
| 维度 | 隐式状态机 | 显式状态机 |
|---|---|---|
| 实现成本 | 低,直接写 if/else | 中等,需要定义转换规则 |
| 可读性 | 差,状态规则分散在各方法中 | 好,规则集中一目了然 |
| 维护成本 | 状态少时低,状态多时急剧上升 | 稳定,新增状态只需加规则 |
| 安全性 | 容易遗漏校验导致非法转换 | 统一拦截非法转换 |
| 适用场景 | 状态少(3-5个)且变化少 | 状态多或流转规则复杂 |
经验上看,如果只有两三个状态,且业务基本稳定,隐式状态机完全可以接受;但一旦状态超过 5 个,或者流程里开始出现取消、回退、补偿、外部系统联动,继续把规则散在业务方法里,后面大概率会越改越乱。
状态机落地时还要考虑什么
真正上线的业务流转,往往不只是“状态变一下”这么简单。下面几个概念经常会一起出现。

守卫条件(Guard)
事件发生后,不一定就能直接流转,往往还要做额外校验。比如订单发货前必须确认库存充足:
// 事件是"发货",但还需要守卫条件:库存充足
public Integer transition(Integer currentState, String event, Order order) {
if ("SHIP".equals(event) && order.getStock() <= 0) {
throw new BusinessException("库存不足,无法发货");
}
return TRANSITIONS.get(currentState).get(event);
}守卫条件的意义,是把“能不能转”这件事判断完整,而不是只看事件名。
转换动作(Action)
状态转换完成时,通常还会伴随业务动作,比如取消订单时自动退款、通知用户:
// 转换到"已取消"时,自动执行退款
public void cancelOrder(Order order) {
Integer newStatus = OrderStateMachine.transition(order.getStatus(), "CANCEL");
order.setStatus(newStatus);
// 转换动作
refundService.refund(order.getPaymentId());
notificationService.notifyUser(order.getUserId(), "订单已取消");
}这里要关注的是,状态变化和业务动作通常不能割裂处理,否则很容易出现“状态改了,但后续动作没执行完整”的问题。
入口动作 / 出口动作(Entry/Exit Action)
有些动作不是跟事件绑定,而是跟“进入某个状态”绑定。比如无论从哪个状态进入“已取消”,都要发通知并释放库存:
// 无论从哪里进入"已取消"状态,都发通知
private void onEnterCancelled(Order order) {
notificationService.notifyUser(order.getUserId(), "订单已取消");
inventoryService.releaseStock(order.getItems());
}这种场景适合把公共处理抽成状态入口动作,避免多处重复。
状态回退(Rollback)
有些业务还需要逆向流转。比如物流拦截成功后,订单可能要从“已发货”回退到“已支付”:
// 从已发货回退到已支付(例如物流拦截成功)
public void rollbackShipment(Order order) {
if (!OrderStatus.SHIPPED.getCode().equals(order.getStatus())) {
throw new BusinessException("只有已发货状态才能回退");
}
order.setStatus(OrderStatus.PAID.getCode());
// 撤销动作:取消物流单、恢复库存
logisticsService.cancelShipment(order.getLogisticsNo());
inventoryService.restoreStock(order.getItems());
}回退最容易被低估。因为它不只是把状态字段改回去,还要考虑之前的副作用是否也要补偿或撤销。
多字段联动时的复合状态问题
实际系统里,一个对象经常不止一个状态字段。比如订单既有签章状态,也有同步状态,这时就会形成复合状态:
// 两个独立的状态维度
private Integer signStatus; // 签章状态
private Integer syncYcStatus; // 同步状态
// 复合状态:只有签章完成且已同步才算"流程结束"
// 回退时两个维度可能都需要回退
这类场景需要额外看清楚三件事:
- 两个状态维度之间是否存在依赖关系,例如同步必须在签章完成后进行
- 回退是否需要联动处理,一个维度回退后另一个维度是否也要跟着回退
- 外部系统是否已经接收数据,如果涉及第三方不可逆操作,回退策略必须更谨慎
单字段状态机和复合状态机的复杂度差别很大。前者主要处理流转规则,后者还要处理维度间约束和补偿策略。
总结
状态机要解决的核心问题,其实就是对象生命周期管理。无论你最终选隐式还是显式实现,都绕不开下面几件事:
- 先把状态定义完整,最好用枚举统一表达
- 把事件和转换规则梳理清楚,避免规则散落在多个方法中
- 把转换时需要执行的动作一起设计进去
- 对非法转换建立统一的校验和拦截机制
- 提前考虑回退、补偿,以及多状态字段联动场景
如果你的业务状态少、变化也少,隐式状态机足够实用;但只要流程开始变长、规则开始变多,尽早收拢成显式状态机,通常会比后期补洞更省成本。









