1. What is StateMachine?
Spring Statemachine is a framework for application developers to use the concept of state machine in Spring applications, from the design level: the purpose of state machine is to solve complex state management processes, to ensure the principle of program singularity and open and closed; Business perspective: The state machine should be driven by initializing the state, loading all existing states, events, transitions, actions, and triggering the next state, and solve the strong coupling between business logic and state management.
Spring Statemachine provides the following features:
- Easy-to-use planar (Tier 1) state machine for simple use cases.
- Hierarchical state machine structure to simplify complex state configurations.
- The state machine area provides more complex state configurations.
- Use of triggers, transitions, guards, and actions.
- Type-safe configuration adapters.
- State machine event listeners.
- Spring IoC integration associates beans with state machines.
Spring Statemachine principle
Spring state machines are built on the concept of finite state machines (FSMs) and provide a concise and flexible way to define, manage, and execute state machines. It defines states as Java objects and configures the rules for transitioning between states. State transitions are usually triggered by external events, and we can define different event types based on business logic and associate them with state transitions. Spring state machines also provide state listeners that execute specific logic when state changes. At the same time, the state of the state machine can be persisted into a database or other storage medium to maintain state consistency in the event of a system reboot or failure recovery. The Spring State Machine core consists of the following three key elements:
- State: Defines the various states that the system may be in, such as pending payment, paid, and so on in the order state.
- Transition: describes the conditions under which a system can move from one state to another when a particular event is received. For example, when a “Payment Successful” event is received, the order status changes from “Pending Payment” to “Paid”.
- Event: An action or message that triggers a state transition, which is the cause of the state machine’s migration from the current state to the new state.
Next, we convert the example of order status from the state pattern above into a state machine implementation.
2. Code engineering
Experiment Goal: An example of an order state is converted to a state machine implementation.
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springboot-demo</artifactId>
<groupId>com.et</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>Statemachine</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
Defines the state of the state machine and the type of event
Defining States: State is the core component of a state machine, representing the conditions or patterns that may exist in a system or object at a certain point in time. In a state machine, each state is a definite condition or stage that the system may be in. For example, in a simple coffee machine state machine, there may be states such as “Standby”, “Grind”, “Brew”, and “Done”. Each state is unique, and the system can only be in one of these states at any given time. Defining Transitions: Transitions refer to the transition process between states, which is the embodiment of the dynamic nature of the state machine model. When an external event is triggered, such as a user pressing a button, receiving a signal, meeting certain conditions, and so on, a state opportunity transitions from the current state to another. When defining a transition, you need to indicate the event that triggered the transition (Event
) and the response of the system at the time of the event, i.e. from which state (Source State
) to which state (Target State
)。
package com.et.statemachine.state;
/**
* @description:order status
*/
public enum OrderStatusChangeEventEnum {
PAYED,
DELIVERY,
RECEIVED;
}
package com.et.statemachine.state;
/**
* @description: order status
*/
public enum OrderStatusEnum {
WAIT_PAYMENT,
WAIT_DELIVER,
WAIT_RECEIVE,
FINISH;
}
Define state machines and state flow rules
The state machine configuration class is in useSpring State Machine
or other state machine frameworks, this class is primarily used to define the core structure of a state machine, including states (states
), event (events
), transition rules between states (transitions
), as well as possible state transition actions and decision logic. atSpring State Machine
, creating a state machine configuration class is typically through inheritanceStateMachineConfigurerAdapter
class. This adapter class provides several template methods that allow developers to override them to configure the various components of a state machine:
- Configuration status(
configureStates(StateMachineStateConfigurer)
): In this method, the developer defines all states in the state machine, including the initial state (initial state
) and end state (final/terminal states
)。 For example, define states A, B, and C, and specify state A as the initial state. - Configure transformations(
configureTransitions(StateMachineTransitionConfigurer)
: Here, the developer describes the transition rules between states, i.e. when an event (event
How the state machine should move from one state to another when it happens. For example, when event X occurs, the state machine is transferred from state A to state B. - Configure the initial state(
configureInitialState(ConfigurableStateMachineInitializer)
): If you need to explicitly specify the initial state when the state machine starts, you can set it in this method.
package com.et.statemachine.config;
import com.et.statemachine.state.OrderStatusChangeEventEnum;
import com.et.statemachine.state.OrderStatusEnum;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import java.util.EnumSet;
/**
* @description: order statemachine
*/
@Configuration
@EnableStateMachine
public class OrderStatusMachineConfig extends StateMachineConfigurerAdapter<OrderStatusEnum, OrderStatusChangeEventEnum> {
/**
* configure state
*/
@Override
public void configure(StateMachineStateConfigurer<OrderStatusEnum, OrderStatusChangeEventEnum> states) throws Exception {
states.withStates()
.initial(OrderStatusEnum.WAIT_PAYMENT)
.end(OrderStatusEnum.FINISH)
.states(EnumSet.allOf(OrderStatusEnum.class));
}
/**
* configure state transient with event
*/
@Override
public void configure(StateMachineTransitionConfigurer<OrderStatusEnum, OrderStatusChangeEventEnum> transitions) throws Exception {
transitions.withExternal().source(OrderStatusEnum.WAIT_PAYMENT).target(OrderStatusEnum.WAIT_DELIVER)
.event(OrderStatusChangeEventEnum.PAYED)
.and()
.withExternal().source(OrderStatusEnum.WAIT_DELIVER).target(OrderStatusEnum.WAIT_RECEIVE)
.event(OrderStatusChangeEventEnum.DELIVERY)
.and()
.withExternal().source(OrderStatusEnum.WAIT_RECEIVE).target(OrderStatusEnum.FINISH)
.event(OrderStatusChangeEventEnum.RECEIVED);
}
}
Define a state machine listener
State Machine Listener (State Machine Listener
) is a component that listens for and responds to various events during the running of a state machine, such as state transitions, entry or exit states, rejection of transitions, and so on. atSpring Statemachine
listeners can be implemented throughStateMachineListener
interface. The interface provides a series of callback methods, such as:transitionTriggered
、stateEntered
、stateExited
These methods are called when the state machine triggers a transition, enters a new state, or leaves an old state. At the same time, we can also implement listeners through annotations. Annotations can be used to directly declare the state in which the method should be called on the method of the class, simplifying the writing and configuration of listeners. For example@OnTransition
,@OnTransitionEnd
,@OnTransitionStart
wait
package com.et.statemachine.listener;
import com.et.statemachine.state.Order;
import com.et.statemachine.state.OrderStatusEnum;
import org.springframework.messaging.Message;
import org.springframework.statemachine.annotation.OnTransition;
import org.springframework.statemachine.annotation.WithStateMachine;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/**
* @description: state listener
*/
@Component
@WithStateMachine
@Transactional
public class OrderStatusListener {
@OnTransition(source = "WAIT_PAYMENT", target = "WAIT_DELIVER")
public boolean payTransition(Message message) {
Order order = (Order) message.getHeaders().get("order");
order.setOrderStatus(OrderStatusEnum.WAIT_DELIVER);
System.out.println("pay,feedback by statemachine:" + message.getHeaders().toString());
return true;
}
@OnTransition(source = "WAIT_DELIVER", target = "WAIT_RECEIVE")
public boolean deliverTransition(Message message) {
Order order = (Order) message.getHeaders().get("order");
order.setOrderStatus(OrderStatusEnum.WAIT_RECEIVE);
System.out.println("deliver,feedback by statemachine:" + message.getHeaders().toString());
return true;
}
@OnTransition(source = "WAIT_RECEIVE", target = "FINISH")
public boolean receiveTransition(Message message) {
Order order = (Order) message.getHeaders().get("order");
order.setOrderStatus(OrderStatusEnum.FINISH);
System.out.println("receive,feedback by statemachine:" + message.getHeaders().toString());
return true;
}
}
service
package com.et.statemachine.service;
import com.et.statemachine.state.Order;
import java.util.Map;
/**
* @author liuhaihua
* @version 1.0
* @ClassName OrderService
* @Description todo
* @date 2024年05月27日 15:15
*/
public interface OrderService {
Order create();
Order pay(long id);
Order deliver(long id);
Order receive(long id);
Map<Long, Order> getOrders();
}
package com.et.statemachine.service;
import com.et.statemachine.state.Order;
import com.et.statemachine.state.OrderStatusChangeEventEnum;
import com.et.statemachine.state.OrderStatusEnum;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.statemachine.StateMachine;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* @description: order service
*/
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private StateMachine<OrderStatusEnum, OrderStatusChangeEventEnum> orderStateMachine;
private long id = 1L;
private Map<Long, Order> orders = new ConcurrentHashMap<>();
@Override
public Order create() {
Order order = new Order();
order.setOrderStatus(OrderStatusEnum.WAIT_PAYMENT);
order.setOrderId(id++);
orders.put(order.getOrderId(), order);
System.out.println("order create success:" + order.toString());
return order;
}
@Override
public Order pay(long id) {
Order order = orders.get(id);
System.out.println("try to pay,order no:" + id);
Message message = MessageBuilder.withPayload(OrderStatusChangeEventEnum.PAYED).
setHeader("order", order).build();
if (!sendEvent(message)) {
System.out.println(" pay fail, error,order no:" + id);
}
return orders.get(id);
}
@Override
public Order deliver(long id) {
Order order = orders.get(id);
System.out.println(" try to deliver,order no:" + id);
if (!sendEvent(MessageBuilder.withPayload(OrderStatusChangeEventEnum.DELIVERY)
.setHeader("order", order).build())) {
System.out.println(" deliver fail,error,order no:" + id);
}
return orders.get(id);
}
@Override
public Order receive(long id) {
Order order = orders.get(id);
System.out.println(" try to receiver,order no:" + id);
if (!sendEvent(MessageBuilder.withPayload(OrderStatusChangeEventEnum.RECEIVED)
.setHeader("order", order).build())) {
System.out.println(" deliver fail,error,order no:" + id);
}
return orders.get(id);
}
@Override
public Map<Long, Order> getOrders() {
return orders;
}
/**
* send transient event
* @param message
* @return
*/
private synchronized boolean sendEvent(Message<OrderStatusChangeEventEnum> message) {
boolean result = false;
try {
orderStateMachine.start();
result = orderStateMachine.sendEvent(message);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (Objects.nonNull(message)) {
Order order = (Order) message.getHeaders().get("order");
if (Objects.nonNull(order) && Objects.equals(order.getOrderStatus(), OrderStatusEnum.FINISH)) {
orderStateMachine.stop();
}
}
}
return result;
}
}
controller
package com.et.statemachine.controller;
import com.et.statemachine.service.OrderService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
@RestController
public class HelloWorldController {
@RequestMapping("/hello")
public Map<String, Object> showHelloWorld(){
Map<String, Object> map = new HashMap<>();
map.put("msg", "HelloWorld");
return map;
}
@Resource
private OrderService orderService;
@RequestMapping("/testOrderStatusChange")
public String testOrderStatusChange(){
orderService.create();
orderService.create();
orderService.pay(1L);
orderService.deliver(1L);
orderService.receive(1L);
orderService.pay(2L);
orderService.deliver(2L);
orderService.receive(2L);
System.out.println("all orders:" + orderService.getOrders());
return "success";
}
}
The above are just some of the key codes, all of which can be found in the repositories below
Code repositories
3. Testing
Start the Spring Boot application
Test the state machine
Visit the http://127.0.0.1:8088/testOrderStatusChange to view the console output
order create success:Order(orderId=1, orderStatus=WAIT_PAYMENT)
order create success:Order(orderId=2, orderStatus=WAIT_PAYMENT)
try to pay,order no:1
2024-05-27 22:58:14.208 INFO 13306 --- [nio-8088-exec-2] o.s.s.support.LifecycleObjectSupport : started org.springframework.statemachine.support.DefaultStateMachineExecutor@13e24b22
2024-05-27 22:58:14.209 INFO 13306 --- [nio-8088-exec-2] o.s.s.support.LifecycleObjectSupport : started WAIT_RECEIVE WAIT_DELIVER FINISH WAIT_PAYMENT / WAIT_PAYMENT / uuid=7a254cb7-5a92-4f0b-b6c7-3edc4d9e2de2 / id=null
pay,feedback by statemachine:{order=Order(orderId=1, orderStatus=WAIT_DELIVER), id=fc2c0720-e6ba-9bf8-d359-72a6c61b4186, timestamp=1716821894196}
try to deliver,order no:1
deliver,feedback by statemachine:{order=Order(orderId=1, orderStatus=WAIT_RECEIVE), id=e743d376-22e1-bfc3-1c62-7131ff1bf7c1, timestamp=1716821894227}
try to receiver,order no:1
receive,feedback by statemachine:{order=Order(orderId=1, orderStatus=FINISH), id=652167b8-e74f-bde2-62f7-94bdbb5bad7e, timestamp=1716821894229}
2024-05-27 22:58:14.230 INFO 13306 --- [nio-8088-exec-2] o.s.s.support.LifecycleObjectSupport : stopped org.springframework.statemachine.support.DefaultStateMachineExecutor@13e24b22
2024-05-27 22:58:14.230 INFO 13306 --- [nio-8088-exec-2] o.s.s.support.LifecycleObjectSupport : stopped WAIT_RECEIVE WAIT_DELIVER FINISH WAIT_PAYMENT / / uuid=7a254cb7-5a92-4f0b-b6c7-3edc4d9e2de2 / id=null
try to pay,order no:2
2024-05-27 22:58:14.231 INFO 13306 --- [nio-8088-exec-2] o.s.s.support.LifecycleObjectSupport : started org.springframework.statemachine.support.DefaultStateMachineExecutor@13e24b22
2024-05-27 22:58:14.231 INFO 13306 --- [nio-8088-exec-2] o.s.s.support.LifecycleObjectSupport : started WAIT_RECEIVE WAIT_DELIVER FINISH WAIT_PAYMENT / WAIT_PAYMENT / uuid=7a254cb7-5a92-4f0b-b6c7-3edc4d9e2de2 / id=null
pay,feedback by statemachine:{order=Order(orderId=2, orderStatus=WAIT_DELIVER), id=d331fa76-8a28-aaa7-6257-a9404c2084d6, timestamp=1716821894230}
try to deliver,order no:2
deliver,feedback by statemachine:{order=Order(orderId=2, orderStatus=WAIT_RECEIVE), id=4e930443-6b04-fd86-6740-5631db2aea1d, timestamp=1716821894232}
try to receiver,order no:2
receive,feedback by statemachine:{order=Order(orderId=2, orderStatus=FINISH), id=6473cc9e-5cd9-0de5-12c8-7d51dd3f9da6, timestamp=1716821894233}
2024-05-27 22:58:14.234 INFO 13306 --- [nio-8088-exec-2] o.s.s.support.LifecycleObjectSupport : stopped org.springframework.statemachine.support.DefaultStateMachineExecutor@13e24b22
2024-05-27 22:58:14.234 INFO 13306 --- [nio-8088-exec-2] o.s.s.support.LifecycleObjectSupport : stopped WAIT_RECEIVE WAIT_DELIVER FINISH WAIT_PAYMENT / / uuid=7a254cb7-5a92-4f0b-b6c7-3edc4d9e2de2 / id=null
all orders:{1=Order(orderId=1, orderStatus=FINISH), 2=Order(orderId=2, orderStatus=FINISH)}