欢迎关注我的公众号:Java开发者笔记
业务背景
最近在我们新改造的财务项目中,对于以往的财务凭证状态改变的编码和设计有了不同的见解;
在一张专业财务凭证中,订单的状态会经过如下状态:
内部复杂的状态流转,第一眼看到下意思的就会觉得,这个业务简单,几个状态枚举,加上几个不同的接口就完事儿了嘛,比如你会写这样的代码:
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 创建订单
*/
@PostMapping("create")
public Result createOrder(@RequestBody OrderDto order) {
return orderService.create(order);
}
/**
* 审核订单
*/
@PostMapping("auditOrder")
public Result auditOrder(@RequestBody OrderDto order) {
return orderService.auditOrder(order);
}
/**
* 审核驳回订单
*/
@PostMapping("auditBackOrder")
public Result auditBackOrder(@RequestBody OrderDto order) {
return orderService.auditBackOrder(order);
}
// ...等等接口
}
不是说通过接口不能实现这个业务,只是接口带来的是线程的开销,据我了解身边在大厂的朋友,他们每写一个接口都需要经过复杂的审核,这背后的思考就是服务器的资源压力问题;
那除了上述通过接口来控制每一个状态,还有没有什么其他的办法?
- 策略模式
- 状态模式
策略模式和状态模式均可用于这种状态变化的事件,只不过有一点内部的区别:
- 策略模式重点关注在于对象的不同策略之间的不同选择,重点是选择;
- 状态模式重点在于对象的状态不同在不同事件下的不同流转,重点是状态;
所以我们最终采用了状态模式来完成这次的业务改造;
经过阅读 Spring 官网得知,Spring 提供一种工具,Spring State Machine,意思就是Spring 状态机,能完美解决上述业务中的状态流转问题:
一整段英文介绍中,重点的描述就是:应用程序处于有限数量的状态,但是随着某些事件的发生,会将应用程序从一个状态切换到另一个状态;
Spring 状态机定义
状态机,不是一种框架,也不是一个机器,它更抽象于是一种数学模型;更简单的一点可以说是一种管理状态流转的图;
从状态变化这四个字可以看出,一个状态机会包含以下几种要素:
- 原始状态:指的是一个对象在未变化之前的状态,例如草稿状态
- 事件:触发这个对象由一个事件变到另一个状态的动作,称之为事件
- 响应回调:事件触发后,对应的回调函数用于执行后续业务
- 目标状态:对象需要转换到的状态
- 状态守卫:一个状态流转到另一个状态时的监控,可以让状态流转成功或者流转失败;
技术实现
环境准备
采用Spring boot 2.7.18 + jdk 1.8 + spring-statemachine-core 3.2.1,pom如下:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.20</version>
</dependency>
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>
</dependencies>
然后创建一个订单表和订单日志表:
-- 订单表
CREATE TABLE voucher_order
(
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
order_no VARCHAR(64) NOT NULL COMMENT '订单号',
tenant_id BIGINT NOT NULL COMMENT '租户ID',
order_amount DECIMAL(10, 2) NOT NULL COMMENT '订单金额',
order_state VARCHAR(32) NOT NULL COMMENT '订单状态(DRAFT/AUDITING等)',
order_date DATE NOT NULL COMMENT '订单日期',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_order_no (order_no)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='订单表';
-- 状态流转日志表
CREATE TABLE voucher_state_log
(
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
tenant_id BIGINT NOT NULL COMMENT '租户ID',
order_id BIGINT NOT NULL COMMENT '订单ID',
log_content VARCHAR(512) NOT NULL COMMENT '日志内容',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
KEY idx_order_id (order_id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='订单状态流转日志表';
-- 测试数据(草稿状态订单)
INSERT INTO voucher_order (order_no, tenant_id, order_amount, order_state, order_date)
VALUES ('TEST20251027001', 1, 999.99, 'DRAFT', '2025-10-27');
-- 状态机持久化表(存储每个订单的当前状态)
CREATE TABLE state_machine_persist
(
order_id BIGINT NOT NULL COMMENT '订单ID(外键)',
state VARCHAR(32) NOT NULL COMMENT '当前状态',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (order_id),
CONSTRAINT fk_sm_order FOREIGN KEY (order_id) REFERENCES voucher_order (id) ON DELETE CASCADE
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='状态机持久化表';
状态枚举和事件枚举
在准备好上述的pom和sql文件后,我们根据上述的状态流转图,来定义对应的状态枚举和事件枚举:
public enum VoucherState {
// 草稿
DRAFT,
// 审核中
AUDITING,
// 审核通过
AUDIT_PASSED,
// 核算
ACCOUNTING,
// 红冲
RED_FLUSH,
// 归档
ARCHIVED
}
public enum VoucherEvent {
// 提交审核(草稿->审核中)
SUBMIT_AUDIT,
// 审核通过(审核中->审核通过)
AUDIT_SUCCESS,
// 驳回审核(审核通过->草稿)
REJECT_AUDIT,
// 进入核算(审核通过->核算)
ENTER_ACCOUNTING,
// 执行归档(核算->归档)
ARCHIVE,
// 执行红冲(核算->红冲)
RED_FLUSH
}
状态机核心配置
上面说到,状态机一共需要五个部分组成,我们定义好了状态枚举和事件枚举后,接下来就是配置状态机的状态流转过程,只能由某个状态流转到某个状态:
@Configuration
@EnableStateMachineFactory
public class VoucherStateMachineConfig extends StateMachineConfigurerAdapter<VoucherState, VoucherEvent> {
@Autowired
private StateMachinePersistService persister;
@Autowired
private VoucherStateGuard guard;
@Autowired
private VoucherStateListener listener;
// 配置状态
@Override
public void configure(StateMachineStateConfigurer<VoucherState, VoucherEvent> states) throws Exception {
states
.withStates()
.initial(VoucherState.DRAFT) // 初始状态:草稿
.states(EnumSet.allOf(VoucherState.class));
}
// 配置状态流转
@Override
public void configure(StateMachineTransitionConfigurer<VoucherState, VoucherEvent> transitions) throws Exception {
transitions
// 草稿 -> 审核中(提交审核)
.withExternal()
.source(VoucherState.DRAFT).target(VoucherState.AUDITING)
.event(VoucherEvent.SUBMIT_AUDIT)
.guard(guard)
.and()
// 审核中 -> 审核通过(审核通过)
.withExternal()
.source(VoucherState.AUDITING).target(VoucherState.AUDIT_PASSED)
.event(VoucherEvent.AUDIT_SUCCESS)
.guard(guard)
.and()
// 审核通过 -> 草稿(驳回审核)
.withExternal()
.source(VoucherState.AUDIT_PASSED).target(VoucherState.DRAFT)
.event(VoucherEvent.REJECT_AUDIT)
.guard(guard)
.and()
// 审核通过 -> 核算(进入核算)
.withExternal()
.source(VoucherState.AUDIT_PASSED).target(VoucherState.ACCOUNTING)
.event(VoucherEvent.ENTER_ACCOUNTING)
.guard(guard)
.and()
// 核算 -> 归档(执行归档)
.withExternal()
.source(VoucherState.ACCOUNTING).target(VoucherState.ARCHIVED)
.event(VoucherEvent.ARCHIVE)
.guard(guard)
.and()
// 核算 -> 红冲(执行红冲)
.withExternal()
.source(VoucherState.ACCOUNTING).target(VoucherState.RED_FLUSH)
.event(VoucherEvent.RED_FLUSH)
.guard(guard);
}
// 配置监听器
@Override
public void configure(StateMachineConfigurationConfigurer<VoucherState, VoucherEvent> config) throws Exception {
config
.withConfiguration()
.listener(listener); // 全局事件监听器
}
// 持久化配置(使用内存存储,实际生产可替换为Redis等)
@Bean
public StateMachinePersister<VoucherState, VoucherEvent, Long> persister() {
return persister;
}
}
并且状态机给我们提供了事件监听器,用来监听从某个状态流转到某个状态后的事件处理:
@Component
public class VoucherStateListener implements StateMachineListener<VoucherState, VoucherEvent> {
@Autowired
private VoucherOrderMapper orderMapper;
@Autowired
private VoucherStateLogMapper logMapper;
// 用于临时存储状态上下文(替代原StateMachineListenerAdapter的方式)
private final ThreadLocal<StateContext<VoucherState, VoucherEvent>> contextHolder = new ThreadLocal<>();
@Override
public void stateContext(StateContext<VoucherState, VoucherEvent> stateContext) {
this.contextHolder.set(stateContext);
}
@Override
public void stateChanged(State<VoucherState, VoucherEvent> from, State<VoucherState, VoucherEvent> to) {
// 状态变更时执行(初始状态不记录)
if (from != null) {
// 获取上下文参数
StateContext<VoucherState, VoucherEvent> context = contextHolder.get();
Long orderId = context.getMessage().getHeaders().get("orderId", Long.class);
VoucherOrder order = orderMapper.selectById(orderId);
// 1. 更新订单状态
order.setOrderState(to.getId().name());
orderMapper.updateById(order);
// 2. 记录状态流转日志
VoucherStateLog log = new VoucherStateLog();
log.setOrderId(orderId);
log.setTenantId(order.getTenantId());
log.setLogContent(String.format("状态从[%s]流转至[%s]", from.getId(), to.getId()));
logMapper.insert(log);
}
}
// 其他默认方法(可根据需要实现)
@Override
public void stateEntered(State<VoucherState, VoucherEvent> state) {}
@Override
public void stateExited(State<VoucherState, VoucherEvent> state) {}
@Override
public void eventNotAccepted(Message<VoucherEvent> event) {}
@Override
public void transition(Transition<VoucherState, VoucherEvent> transition) {}
@Override
public void transitionStarted(Transition<VoucherState, VoucherEvent> transition) {}
@Override
public void transitionEnded(Transition<VoucherState, VoucherEvent> transition) {}
@Override
public void stateMachineStarted(StateMachine<VoucherState, VoucherEvent> stateMachine) {}
@Override
public void stateMachineStopped(StateMachine<VoucherState, VoucherEvent> stateMachine) {}
@Override
public void stateMachineError(StateMachine<VoucherState, VoucherEvent> stateMachine, Exception exception) {}
@Override
public void extendedStateChanged(Object key, Object value) {}
}
并且在配置过程中,需要考虑服务宕机的情况,所以需要一个持久化的操作,这里你可以选择使用一张持久化表,也可以选择使用订单本身字段来维护持久化状态:
@Service
public class StateMachinePersistService implements StateMachinePersister<VoucherState, VoucherEvent, Long> {
@Autowired
private StateMachineMapper stateMachineMapper;
@Override
public void persist(StateMachine<VoucherState, VoucherEvent> stateMachine, Long orderId) throws Exception {
// 保存当前状态到数据库
StateMachineEntity entity = new StateMachineEntity();
entity.setOrderId(orderId);
entity.setState(stateMachine.getState().getId().name());
// 存在则更新,不存在则插入
if (stateMachineMapper.selectById(orderId) != null) {
stateMachineMapper.updateById(entity);
} else {
stateMachineMapper.insert(entity);
}
}
@Override
public StateMachine<VoucherState, VoucherEvent> restore(StateMachine<VoucherState, VoucherEvent> stateMachine, Long orderId) throws Exception {
// 从数据库恢复状态
StateMachineEntity entity = stateMachineMapper.selectById(orderId);
if (entity != null && StringUtils.hasText(entity.getState())) {
VoucherState state = VoucherState.valueOf(entity.getState());
// 恢复状态机状态
stateMachine.getStateMachineAccessor()
.doWithAllRegions(accessor -> accessor.resetStateMachine(new DefaultStateMachineContext<>(
state, null, null, null)));
}
return stateMachine;
}
}
对应的状态守卫:
@Component
public class VoucherStateGuard implements Guard<VoucherState, VoucherEvent> {
@Autowired
private VoucherOrderMapper orderMapper;
@Override
public boolean evaluate(StateContext<VoucherState, VoucherEvent> context) {
// 获取订单ID(从事件参数中传递)
Long orderId = context.getMessage().getHeaders().get("orderId", Long.class);
if (orderId == null) {
context.getExtendedState().getVariables().put("error", "订单ID不能为空");
return false;
}
// 校验订单是否存在
VoucherOrder order = orderMapper.selectById(orderId);
if (order == null) {
context.getExtendedState().getVariables().put("error", "订单不存在");
return false;
}
// 校验当前状态是否与流转源状态匹配
VoucherState currentState = VoucherState.valueOf(order.getOrderState());
VoucherState sourceState = context.getTransition().getSource().getId();
if (!currentState.equals(sourceState)) {
context.getExtendedState().getVariables().put("error",
String.format("当前状态为[%s],不支持该操作", currentState));
return false;
}
return true;
}
}
接着,在service中只需要对外提供一个服务,启用代码如下:
@Service
public class VoucherStateMachineService {
@Autowired
private StateMachineFactory<VoucherState, VoucherEvent> stateMachineFactory;
@Autowired
private StateMachinePersister<VoucherState, VoucherEvent, Long> persister;
@Autowired
private VoucherOrderMapper orderMapper;
/**
* 执行状态流转
* @param orderId 订单ID
* @param event 触发事件
* @return 流转结果
*/
public Result<String> executeEvent(Long orderId, VoucherEvent event) {
try {
// 1. 获取订单信息
VoucherOrder order = orderMapper.selectById(orderId);
if (order == null) {
return Result.fail("订单不存在");
}
// 2. 创建状态机并恢复状态
StateMachine<VoucherState, VoucherEvent> stateMachine = stateMachineFactory.getStateMachine();
stateMachine.startReactively().block();
persister.restore(stateMachine, orderId); // 从持久化存储恢复状态
// 3. 发送事件(携带订单ID参数)
Message<VoucherEvent> message = MessageBuilder
.withPayload(event)
.setHeader("orderId", orderId)
.build();
boolean result = stateMachine.sendEvent(message);
// 4. 持久化状态机
persister.persist(stateMachine, orderId);
if (result) {
return Result.success("状态流转成功");
} else {
// 获取守卫中的错误信息
String error = stateMachine.getExtendedState().get("error", String.class);
return Result.fail(error != null ? error : "状态流转失败");
}
} catch (Exception e) {
return Result.fail("状态流转异常:" + e.getMessage());
}
}
}
效果测试
我们采用ApiFox插件测试接口:
带来的好处与问题是什么?
在面试中,如果你提出你使用过状态机,那么面试官就会问你,你使用状态机和使用普通接口流转带来的好处是什么?
好处如下:
- 使接口实现高内聚,无需引入额外其他接口带来额外线程开销
- 引入状态模式设计,代码清晰度更高,更易维护
- 状态事件更清晰,极大程度降低状态流转错乱带来的数据问题
好处说完了,说一下可能存在的问题:
- 分布式环境下,对同一订单操作可能存在状态越权的情况,例如多个实例操作同一订单,极有可能出现A将状态从草稿变成审核中,但是B将状态从审核中变成审核通过;并发操作带来的状态越权问题,解决方式:采用分布式锁或者乐观锁,在状态变更时加锁控制;
- 状态变成一个循环,比如从草稿变成待审核,又从待审核变成草稿;虽然业务上允许,但是这明显是两个事件触发,代码中需要注意这种循环状态不能由一个事件触发;