状态机是状态模式的一种应用,相当于上下文角色的一个升级版。在工作流或游戏等各种系统中有大量使用,如各种工作流引擎,它几乎是状态机的子集和实现,封装状态的变化规则。状态机可以帮助开发者简化状态控制的开发过程,让状态机结构更加层次化。
Spring 提供了一个很好的解决方案,Spring Statemachine(状态机)是应用程序开发人员在 Spring 应用程序中使用状态机概念的框架。
状态模式适用于以下几种场景:
- 行为随状态改变而改变场景;
- 一个操作中含有庞大的多分支机构,并且这些分支取决于对象的状态。
状态模式主要包含三种角色:
- 环境类角色(Context):定义客户端需要的接囗,内部维护一个当前状态实例,并负责具体状态的切换;
- 抽象状态角色(State):定义该状态下的行为,可以有一个或多个行为;
- 具体状态角色(ConcreteState):具体实现该状态对应的行为并且在需要的肩况下进行状态切换。
状态模式的相关模式
3.1 状态模式与责任链模式
状态模式和责任链模式都能消除if分支过多的问题。但某些情况下,状态模式中的状态可以理解为责任,那么这种情况下,两种模式都可以使用。
从定义来看,状态模式强调的是一个对象内在状态的改变,而责任链模式强调的是外部节点对象间的改变。
从其代码实现上来看,他们间最大的区别就是状态模式各个状态对象知道自己下一个要进入的状态对象而责任链模式并不清楚其下一个节点处理对象,因为链式组装由客户端负责。
3.2 状态模式与策略模式
状态模式和策略模式的UML类图架构几乎完全一样,但他们的应用场景是不一样的。策略模式多种算法行为择其一都能满足,彼此之间是独立的用户可自行更换策略算法,而状态模式各个状态间是存在相互关系的,彼此之间在一定条件下存在自动切换状态效果,且用户无法指定状态,只能设置初始状态。
四、状态模式的优缺点
优点:
- 结构清晰:将状态独立为类,消除了冗余的if...else或switch...case语句,使代码更加简洁,提高系统可维护性;
- 将状态转换显示化通常的对象内部都是使用数值类型来定义状态,状态的切换是通过賦值进行表现,不够直观,而使用状态类,在切换状态时,是以不同的类进行表示,转换目的更加明确;
- 状态类职责明确且具备扩展性。
缺点:
- 类膨胀:如果一个事物具备很多状态,则会造成状态类太多;
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱;
- 状态模式对开闭原则的支持并不太好,对于可以切换状态的状态模式增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。
基于Spring statemachine的轻量级状态机实现
在电商平台中,一个订单会有多种状态,临时单、已下单、待支付、已支付、待发货、待收货、已完成等等。每一种状态都和变化前的状态以及执行的操作有关。比如,用户将商品加入购物车后,后台会生成一个所谓的“临时单”。因为用户还没有点击下单,所以这个订单实际上还没有生成。只有当用户下单后,这个“临时单”才会转化为一个“待支付的订单”。以上过程中只有将一个处于“临时单”状态的订单执行下单操作,才能得到一个状...
在电商平台中,一个订单会有多种状态,临时单、已下单、待支付、已支付、待发货、待收货、已完成等等。每一种状态都和变化前的状态以及执行的操作有关。
比如,用户将商品加入购物车后,后台会生成一个所谓的“临时单”。因为用户还没有点击下单,所以这个订单实际上还没有生成。只有当用户下单后,这个“临时单”才会转化为一个“待支付的订单”。
以上过程中只有将一个处于“临时单”状态的订单执行下单操作,才能得到一个状态为“待支付”的订单。 即一个前置状态+一个恰当的操作,才能流转订单的状态。在这个过程中如果使用硬编码,我们就需要一系列的 if-else 语句来检查订单的当前状态、可执行操作以及这两个组合得到的下一个应该被流转的状态值。如果订单的状态流转很复杂,代码逻辑就会很复杂,可读性低,后期维护困难。
处理以上问题,我们可以使用状态设计模式来处理。对应到实践,就是状态机。
有限状态机状态间流转要素
要实现状态与状态之间的流转,必须要具备以下几点要素,如下图所示:
1. 当前状态
状态流转的起始状态,如上图中的新建状态
2. 触发事件
引发状态与状态之间流转的事件,如上图中的创建订单这个动作
3. 响应函数
触发事件到下一个状态之间的规则
4. 目标状态
状态流转的终止状态,如上图中的待付款状态
添加依赖:
在pom文件里引入状态机statemachine
,SpringBoot的启动类spring-boot-starter
,测试类spring-boot-starter-test
以及开发中常用的工具lombok
4.0.0
org.example
spring-statemachine-demo
1.0-SNAPSHOT
org.springframework.boot
spring-boot-starter-parent
2.2.1.RELEASE
org.springframework.statemachine
spring-statemachine-core
2.1.3.RELEASE
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
org.projectlombok
lombok
true
创建状态和枚举类
2. 创建订单状态枚举类:
该枚举类用于表示上面状态机流转图里的每一个订单状态,很简单
public enum OrderState {
/*
创建订单
*/
CREATED,
/*
等待付款
*/
PENDING_PAYMENT,
/*
等待配送
*/
PENDING_DELIVERY,
/*
订单完结
*/
ORDER_COMPLETE
}
3. 创建事件枚举类:
该枚举类用于表示上面状态机流转图里的每一个触发流转的动作,很简单
public enum OrderEvent {
/*
下单
*/
PLACE_ORDER,
/*
付款完成
*/
PAID,
/*
配送成功
*/
DELIVERED
}
4. 创建事件监听器:
- 使用
@WithStateMachine
注解开启状态机功能 - 被
@OnTransition(target = "PENDING_PAYMENT")
注解的方法表示该操作会使状态流转至PENDING_PAYMENT
- 定义
pendingPayment
方法表示用户创建订单操作 - 定义
pendingDelivery
方法表示用户付款操作 - 定义
complete
方法表示用户签收操作,订单状态变为完结
@WithStateMachine
@Slf4j
@Data
@Transactional
public class OrderListener {
private String orderStatus = OrderState.CREATED.name();
/**
* 注解中传入的是目标状态
*
* @param message 可以从message中拿出一些属性
*/
@OnTransition(target = "PENDING_PAYMENT")
public void pendingPayment(Message message){
log.info("订单创建,等待付款, status={} header={}", OrderState.PENDING_PAYMENT.name(),
message.getHeaders().get("orderId"));
// TODO 模拟业务流程
setOrderStatus(OrderState.PENDING_PAYMENT.name());
}
@OnTransition(target = "PENDING_DELIVERY")
public void pendingDelivery() {
log.info("订单已付款,等待发货, status={} ",
OrderState.PENDING_DELIVERY.name());
// TODO 模拟业务流程
setOrderStatus(OrderState.PENDING_DELIVERY.name());
}
@OnTransition(target = "ORDER_COMPLETE")
public void complete() {
log.info("订单完成, status={}",
OrderState.ORDER_COMPLETE.name());
// TODO 模拟业务流程
setOrderStatus(OrderState.ORDER_COMPLETE.name());
}
}
5. 创建配置类: 定义状态机的初始状态和状态流转的规则:
@EnableStateMachine
表示开启状态机- 该类继承自
EnumStateMachineConfigurerAdapter
并重写configure
方法指定状态机的初始状态和状态机一共有哪些状态,以及状态之间是如何流转的
@Configuration
@EnableStateMachine
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer states) throws Exception {
states.withStates()
.initial(OrderState.CREATED) // 指定初始化状态
.states(EnumSet.allOf(OrderState.class));
}
/**
* 配置状态机如何做流转
*
* @param transitions
* @throws Exception
*/
@Override
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
transitions
.withExternal() // 配置两个不同状态之间的流转
.source(OrderState.CREATED) // 创建订单
.target(OrderState.PENDING_PAYMENT) // 待付款
.event(OrderEvent.PLACE_ORDER) // 下单
.and().withExternal()
.source(OrderState.PENDING_PAYMENT)
.target(OrderState.PENDING_DELIVERY)
.event(OrderEvent.PAID)
.and().withExternal()
.source(OrderState.PENDING_DELIVERY)
.target(OrderState.ORDER_COMPLETE)
.event(OrderEvent.DELIVERED);
}
}
6. 创建模拟发送一些事件的类:
手动触发一些事件
public class MyRunner implements CommandLineRunner {
@Resource
StateMachine stateMachine;
@Override
public void run(String... args) throws Exception {
stateMachine.start();
Message message = MessageBuilder
.withPayload(OrderEvent.PLACE_ORDER) // 下单事件
.setHeader("orderId", "998") // 消息头里包含订单id
.build();
// 发送几个事件
stateMachine.sendEvent(message);
stateMachine.sendEvent(OrderEvent.PAID); // 已付款
stateMachine.sendEvent(OrderEvent.DELIVERED); // 已发货
}
}
7. 创建启动类:
SpringBoot标准的启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class ApplicationStarter {
@Bean
public MyRunner chickenRun() {
return new MyRunner();
}
public static void main(String[] arg) {
SpringApplication.run(ApplicationStarter.class, arg);
}
}
8. 创建测试类把SpringBoot给跑起来:
@SpringBootTest
public class StateMachineTest {
@Test
public void runForrest() {
}
}
运行结果:
MyRunner将会发送三个事件,这三个事件会触发Listener里三次订单状态的转换
此外,Spring statemachine还有一些高级功能:
-
Guard:状态准入、判断当前业务是否可以进入下个状态
-
Action:状态转移之后执行的业务逻辑
参考:
使用JAVA状态机实现订单状态控制功能-云社区-华为云 (huaweicloud.com)
以电商下单场景为例,玩一玩有限状态机的流转 - 掘金 (juejin.cn)
订单状态机_x-working的博客-CSDN博客
架构师内功心法,参与电商订单业务开发的状态模式详解 - 掘金 (juejin.cn)
订单状态流转之状态模式编码 - 掘金 (juejin.cn)