Comprehensive Guide to Spring State Machine: Concepts, Implementation, Persistence, and Troubleshooting
This article explains the fundamentals of finite‑state machines, introduces Spring Statemachine’s core features, walks through a complete Java implementation with database schema, persistence (memory and Redis), REST APIs, exception handling, and an AOP‑based solution for reliable state transitions.
The article begins by defining a state machine (finite‑state machine) as a mathematical model that represents a limited set of states and the transitions between them, using the simple example of an automatic door with open and closed states.
It then outlines the four essential concepts of a state machine: State , Event , Action , and Transition , each illustrated with the door example.
Formally, a finite‑state machine (FSM) consists of a set of states, an initial state, inputs, and a transition function that determines the next state, which is useful for describing an object's lifecycle and its reaction to external events.
The article shows how to model an order‑processing workflow as a state‑machine diagram, identifying elements such as start, end, current state, target state, action, and condition (e.g., payment triggers transition from WAIT_PAYMENT to WAIT_DELIVER ).
Spring Statemachine is introduced as a framework that brings state‑machine concepts into Spring applications. Its key capabilities include simple flat state machines, hierarchical structures, regions, triggers, guards, actions, type‑safe configuration adapters, Zookeeper‑based distributed state machines, event listeners, UML modeling, and persistence adapters.
For a quick start, the article provides a MySQL tb_order table definition: CREATE TABLE `tb_order` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', `order_code` varchar(128) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '订单编码', `status` smallint(3) DEFAULT NULL COMMENT '订单状态', `name` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '订单名称', `price` decimal(12,2) DEFAULT NULL COMMENT '价格', `delete_flag` tinyint(2) NOT NULL DEFAULT '0' COMMENT '删除标记,0未删除 1已删除', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '更新时间', `create_user_code` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '创建人', `update_user_code` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '更新人', `version` int(11) NOT NULL DEFAULT '0' COMMENT '版本号', `remark` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '备注', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='订单表';
Required Maven dependencies are added: <dependency> <groupId>org.springframework.statemachine</groupId> <artifactId>spring-statemachine-redis</artifactId> <version>1.2.9.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.statemachine</groupId> <artifactId>spring-statemachine-starter</artifactId> <version>2.0.1.RELEASE</version> </dependency>
Two enums are defined for the domain model: public enum OrderStatus { WAIT_PAYMENT(1, "待支付"), WAIT_DELIVER(2, "待发货"), WAIT_RECEIVE(3, "待收货"), FINISH(4, "已完成"); private Integer key; private String desc; // getters and utility methods omitted for brevity } public enum OrderStatusChangeEvent { PAYED, DELIVERY, RECEIVED; }
The state‑machine configuration class registers states and transitions: @Configuration @EnableStateMachine(name = "orderStateMachine") public class OrderStateMachineConfig extends StateMachineConfigurerAdapter { @Override public void configure(StateMachineStateConfigurer states) throws Exception { states.withStates() .initial(OrderStatus.WAIT_PAYMENT) .states(EnumSet.allOf(OrderStatus.class)); } @Override public void configure(StateMachineTransitionConfigurer transitions) throws Exception { transitions.withExternal().source(OrderStatus.WAIT_PAYMENT).target(OrderStatus.WAIT_DELIVER).event(OrderStatusChangeEvent.PAYED) .and() .withExternal().source(OrderStatus.WAIT_DELIVER).target(OrderStatus.WAIT_RECEIVE).event(OrderStatusChangeEvent.DELIVERY) .and() .withExternal().source(OrderStatus.WAIT_RECEIVE).target(OrderStatus.FINISH).event(OrderStatusChangeEvent.RECEIVED); } }
Persistence is demonstrated in two ways. An in‑memory persister stores the StateMachineContext in a simple Map , while a Redis persister uses RedisStateMachineContextRepository and RedisStateMachinePersister to survive across instances. The relevant beans are declared with @Bean methods.
A REST controller exposes endpoints for creating orders and driving the state machine: @RestController @RequestMapping("/order") public class OrderController { @Resource private OrderService orderService; @RequestMapping("/create") public String create(@RequestBody Order order) { orderService.create(order); return "success"; } @RequestMapping("/pay") public String pay(@RequestParam("id") Long id) { orderService.pay(id); return "success"; } @RequestMapping("/deliver") public String deliver(@RequestParam("id") Long id) { orderService.deliver(id); return "success"; } @RequestMapping("/receive") public String receive(@RequestParam("id") Long id) { orderService.receive(id); return "success"; } }
The service layer contains the business logic and a synchronized sendEvent method that starts the state machine, restores persisted state, sends the event, and persists the new state only when the transition succeeds: private synchronized boolean sendEvent(OrderStatusChangeEvent changeEvent, Order order) { boolean result = false; try { orderStateMachine.start(); stateMachineMemPersister.restore(orderStateMachine, String.valueOf(order.getId())); Message message = MessageBuilder.withPayload(changeEvent) .setHeader("order", order).build(); result = orderStateMachine.sendEvent(message); if (!result) return false; Integer flag = (Integer) orderStateMachine.getExtendedState().getVariables() .get(CommonConstants.payTransition + order.getId()); orderStateMachine.getExtendedState().getVariables() .remove(CommonConstants.payTransition + order.getId()); if (Objects.equals(1, flag)) { stateMachineMemPersister.persist(orderStateMachine, String.valueOf(order.getId())); } else { return false; } } catch (Exception e) { log.error("订单操作失败:{}", e); } finally { orderStateMachine.stop(); } return result; }
A listener class updates the order record on each transition and deliberately throws an exception for demonstration; the exception is caught by the surrounding transaction, causing a rollback. @Component("orderStateListener") @WithStateMachine(name = "orderStateMachine") public class OrderStateListenerImpl { @Resource private OrderMapper orderMapper; @OnTransition(source = "WAIT_PAYMENT", target = "WAIT_DELIVER") @Transactional(rollbackFor = Exception.class) public void payTransition(Message message) { Order order = (Order) message.getHeaders().get("order"); order.setStatus(OrderStatus.WAIT_DELIVER.getKey()); orderMapper.updateById(order); if (Objects.equals(order.getName(), "A")) { throw new RuntimeException("执行业务异常"); } } // similar methods for deliverTransition and receiveTransition omitted for brevity }
Testing steps show the normal flow (create → pay → deliver → receive) and the error that occurs when the pay endpoint is called twice, because the state machine still returns true even when the listener throws an exception.
The root cause is that StateMachine.sendEvent swallows exceptions from listeners, always returning true . To solve this, the article stores a success flag (1) or failure flag (0) in the state machine’s ExtendedState variables and persists the state only when the flag indicates success.
Further refactoring introduces a custom @LogResult annotation and an AOP aspect that automatically records the result of any transition method into ExtendedState , removing duplicated try‑catch code. The aspect extracts the Message argument, retrieves the order, executes the original method, and writes 1 or 0 to the state machine variables depending on success or exception.
Finally, the article ends with promotional remarks encouraging readers to follow the author’s public account and join the paid knowledge community for deeper projects.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.