玩转Spring状态机
说起Spring状态机,大家很容易联想到这个状态机和设计模式中状态模式的区别是啥呢?没错,Spring状态机就是状态模式的一种实现,在介绍Spring状态机之前,让我们来看看设计模式中的状态模式。
1. 状态模式
状态模式的定义如下:
状态模式(State Pattern)是一种行为型设计模式,它允许对象在内部状态发生变化时改变其行为。在状态模式中,一个对象的行为取决于其当前状态,而且可以随时改变这个状态。状态模式将对象的状态封装在不同的状态类中,从而使代码更加清晰和易于维护。当一个对象的状态改变时,状态模式会自动更新该对象的行为,而不需要在代码中手动进行判断和处理。
通常业务系统中会存在一些拥有状态的对象,而且这些状态之间可以进行转换,并且在不同的状态下会表现出不同的行为或者不同的功能,比如交通灯控制系统中会存在红灯、绿灯和黄灯,再比如订单系统中的订单会存在已下单、待支付、待发货、待收货等状态,这些状态会通过不同的行为进行相互转换,这时候在系统设计时就可以使用状态模式。
下面是状态模式的类图:

可以看到状态模式主要包含三种类型的角色:
1、上下文(**Context**)角色:封装了状态的实例,负责维护状态实例,并将请求委托给当前的状态对象。
2、抽象状态(**State**)角色:定义了表示不同状态的接口,并封装了该状态下的行为。所有具体状态都实现这个接口。
3、具体状态(**Concrete State**)角色:具体实现了抽象状态角色的接口,并封装了该状态下的行为。
下面是使用状态模式实现红绿灯状态变更的一个简单案例:
抽象状态类:
/**
* @description: 抽象状态类
*/
public abstract class MyState {
abstract void handler();
}
具体状态类A
/**
* @description: 具体状态A
*/
public class RedLightState extends MyState{
@Override
void handler() {
System.out.println("红灯停");
}
}
具体状态类B
/**
* @description: 具体状态B
*/
public class GreenLightState extends MyState{
@Override
void handler() {
System.out.println("绿灯行");
}
}
环境类:维护当前状态对象,并提供了切换状态的方法。
/**
* @description: 环境类
*/
public class MyContext {
private MyState state;
public void setState(MyState state) {
this.state = state;
}
public void handler() {
state.handler();
}
}
测试类
/**
* @description: 测试状态模式
*/
public class TestStateModel {
public static void main(String[] args) {
MyContext myContext = new MyContext();
RedLightState redLightState = new RedLightState();
GreenLightState greenLightState = new GreenLightState();
myContext.setState(redLightState);
myContext.handler(); //红灯停
myContext.setState(greenLightState);
myContext.handler(); //绿灯行
}
}
下面是对应的执行结果

可以发现,使用状态模式中的状态类在一定程度上也消除了if-else逻辑校验,看到这里, 有些人可能会有疑问:状态模式和策略模式的区别是什么呢?
状态模式更关注对象在不同状态的行为和状态之间的流转,而策略模式更关注对象不同策略的选择。
上面我们介绍了设计模式中的状态模式,接下来我们来看看Spring状态机。
2. Spring状态机
状态机,也就是 State Machine ,不是指一台实际机器,而是指一个数学模型 。说白了,就是指一张状态转换图。 状态机是状态模式的一种应用,相当于上下文角色的一个升级版。在工作流或游戏等各种系统中有大量使用,如各种工作流引擎,它几乎是状态机的子集和实现,封装状态的变化规则。Spring也提供了一个很好的解决方案。Spring中的组件名称就叫作状态机(StateMachine)。状态机帮助开发者简化状态控制的开发过程,让状态机结构更加层次化。
通过定义,我们很容易分析得到状态机应当具备一下几个要素:
当前状态:也就是状态流转的起始状态。
触发事件:引起状态之间流转的一些列动作。
响应函数:触发事件到下一个状态之间的规则。
目标状态:状态流转的目标状态。
对于组件化的状态机,当前使用较多的主要是两种:一种是Spring 状态机,一种是COLA状态机,这两种状态机的对比如下表所示:
| Spring 状态机 | COLA 状态机 | |
|---|---|---|
| API 调用 | 使用 Reactive 的 Mono、Flux 方式进行 API 调用 | 同步的 API 调用,如果有需要也可以将方法通过 消息队列、定时任务、多线程等方式进行异步调用 |
| 代码量 | core 包 284 个接口和类 | 36 个接口和类 |
| 生态 | 非常丰富 | 较为贫瘠 |
| 定制化难度 | 困难 | 简单 |
可以看到,Spring状态机锁提供的内容较为丰富,当然对于自定义的支持就不如COLA状态机好,如果对自定义的需求比较高,那建议使用COLA状态机。
本文以Spring状态机为例,展示如何在业务系统中使用状态机。
为了便于大家了解Spring状态机的实现原理和使用方式以及其提供的功能,下面列出了官方文档和源码,感兴趣的同学可以阅读阅读。
源代码: https://github.com/spring-projects/spring-statemachine
3. Spring状态机实现订单状态流转
对于状态模式,Spring封装好了一个组件,就叫状态机(StateMachine)。Spring状态机可以帮助我们开发者简化状态控制的开发过程,让状态机结构更加层次化。下面用Spring状态机模拟一个订单状态流转的过程。
3.1 环境准备
首先,如果要使用spring状态机,需要引入对应的jar包,这里我的springboot版本是:2.2.1.RELEASE
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
<version>${springboot.version}</version>
</dependency>
下面是简化的订单的定义,以及订单状态和订单转换行为的枚举
/**
* @description: 模拟订单类
*/
@Data
public class Order {
private Long orderId;
private OrderStatusEnum orderStatus;
}
/**
* @description: 订单状态
*/
public enum OrderStatusEnum {
// 待支付
WAIT_PAYMENT,
// 待发货
WAIT_DELIVER,
// 待收货
WAIT_RECEIVE,
// 完成
FINISH;
}
/**
* @description:订单状态转换行为
*/
public enum OrderStatusChangeEventEnum {
//支付
PAYED,
//发货
DELIVERY,
//收货
RECEIVED;
}
3.2 构造订单状态机
在引入jar包之后,需要构建一个针对订单状态流转的状态机
订单状态机配置类如下:
/**
* @description: 订单状态机
*/
@Configuration
@EnableStateMachine
public class OrderStatusMachineConfig extends StateMachineConfigurerAdapter<OrderStatusEnum, OrderStatusChangeEventEnum> {
/**
* 配置状态
*/
@Override
public void configure(StateMachineStateConfigurer<OrderStatusEnum, OrderStatusChangeEventEnum> states) throws Exception {
states.withStates()
.initial(OrderStatusEnum.WAIT_PAYMENT)
.end(OrderStatusEnum.FINISH)
.states(EnumSet.allOf(OrderStatusEnum.class));
}
/**
* 配置状态转换事件关系
*/
@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);
}
}
3.3 编写状态机监听器
监听状态变更事件,完成状态转换。
/**
* @description: 状态监听
*/
@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("支付,状态机反馈信息:" + 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("发货,状态机反馈信息:" + 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("收货,状态机反馈信息:" + message.getHeaders().toString());
return true;
}
}
3.4 编写订单服务类
模拟对订单的一些业务操作
/**
* @description: 订单服务
*/
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private StateMachine<OrderStatusEnum, OrderStatusChangeEventEnum> orderStateMachine;
private long id = 1L;
private Map<Long, Order> orders = Maps.newConcurrentMap();
@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.toString());
return order;
}
@Override
public Order pay(long id) {
Order order = orders.get(id);
System.out.println("尝试支付,订单号:" + id);
Message message = MessageBuilder.withPayload(OrderStatusChangeEventEnum.PAYED).
setHeader("order", order).build();
if (!sendEvent(message)) {
System.out.println(" 支付失败, 状态异常,订单号:" + id);
}
return orders.get(id);
}
@Override
public Order deliver(long id) {
Order order = orders.get(id);
System.out.println(" 尝试发货,订单号:" + id);
if (!sendEvent(MessageBuilder.withPayload(OrderStatusChangeEventEnum.DELIVERY)
.setHeader("order", order).build())) {
System.out.println(" 发货失败,状态异常,订单号:" + id);
}
return orders.get(id);
}
@Override
public Order receive(long id) {
Order order = orders.get(id);
System.out.println(" 尝试收货,订单号:" + id);
if (!sendEvent(MessageBuilder.withPayload(OrderStatusChangeEventEnum.RECEIVED)
.setHeader("order", order).build())) {
System.out.println(" 收货失败,状态异常,订单号:" + id);
}
return orders.get(id);
}
@Override
public Map<Long, Order> getOrders() {
return orders;
}
/**
* 发送状态转换事件
* @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;
}
}
3.5 测试入口
这里编写一个controller模拟c端用户请求,为了便于展示,这里使用一个测试方法完成所有的操作
@RestController
public class OrderController {
@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("全部订单状态:" + orderService.getOrders());
return "success";
}
}
下面是对应的执行结果

可以看到spring状态机很好的控制了订单在各个状态之间的流转。
4. 思考与总结
思考:针对状态机的特点,还有其他思路实现一个状态机吗?下面是一些常规思路,如果还有其他方法欢迎在评论区留言。
1. 消息队列方式
订单状态的流转可以通过MQ发布一个事件,消费者根据业务条件把订单状态进行流转,可以根据不同的事件发送到不同的Topic。
2. 定时任务驱动
每隔一段时间启动一下job,根据特定的状态从数据库中拿对应的订单记录,然后判断订单是否有条件到达下一个状态。
3. 规则引擎方式
业务团队可以在规则引擎里编写一系列的状态及其对应的转换规则,由规则引擎根据已经加载的规则对输入数据进行解析,根据解析的结果执行相应的动作,完成状态流转。
总结:
本文主要介绍了设计模式中的状态模式,并在此基础上介绍了Spring状态机相关的概念,并根据常见的订单流转场景,介绍了Spring状态机的使用方式。文中如有不当之处,欢迎在评论区批评指正。
5. 参考内容
https://cloud.tencent.com/developer/article/2198477?areaId=106001
https://cloud.tencent.com/developer/article/2360708?areaId=106001
https://juejin.cn/post/7087064901553750030
https://my.oschina.net/u/4090830/blog/10092135
https://juejin.cn/post/7267506576448929811
作者:京东科技 孙扬威
来源:京东云开发者社区 转载请注明来源
玩转Spring状态机的更多相关文章
- 玩转spring boot——快速开始
开发环境: IED环境:Eclipse JDK版本:1.8 maven版本:3.3.9 一.创建一个spring boot的mcv web应用程序 打开Eclipse,新建Maven项目 选择quic ...
- 玩转spring boot——开篇
很久没写博客了,而这一转眼就是7年.这段时间并不是我没学习东西,而是园友们的技术提高的非常快,这反而让我不知道该写些什么.我做程序已经有十几年之久了,可以说是彻彻底底的“程序老炮”,至于技术怎么样?我 ...
- 玩转spring boot——结合redis
一.准备工作 下载redis的windows版zip包:https://github.com/MSOpenTech/redis/releases 运行redis-server.exe程序 出现黑色窗口 ...
- 玩转spring boot——AOP与表单验证
AOP在大多数的情况下的应用场景是:日志和验证.至于AOP的理论知识我就不做赘述.而AOP的通知类型有好几种,今天的例子我只选一个有代表意义的“环绕通知”来演示. 一.AOP入门 修改“pom.xml ...
- 玩转spring boot——结合JPA入门
参考官方例子:https://spring.io/guides/gs/accessing-data-jpa/ 接着上篇内容 一.小试牛刀 创建maven项目后,修改pom.xml文件 <proj ...
- 玩转spring boot——结合JPA事务
接着上篇 一.准备工作 修改pom.xml文件 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi=&q ...
- 玩转spring boot——结合AngularJs和JDBC
参考官方例子:http://spring.io/guides/gs/relational-data-access/ 一.项目准备 在建立mysql数据库后新建表“t_order” ; -- ----- ...
- 玩转spring boot——结合jQuery和AngularJs
在上篇的基础上 准备工作: 修改pom.xml <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi=&q ...
- 玩转spring boot——MVC应用
如何快速搭建一个MCV程序? 参照spring官方例子:https://spring.io/guides/gs/serving-web-content/ 一.spring mvc结合thymeleaf ...
- 玩转spring boot——properties配置
前言 在以往的java开发中,程序员最怕大量的配置,是因为配置一多就不好统一管理,经常出现找不到配置的情况.而项目中,从开发测试环境到生产环境,往往需要切换不同的配置,如测试数据库连接换成生产数据库连 ...
随机推荐
- 提取protobuf定义文件结构
先安装protobuf的js支持包 npm install protobufjs test.proto文件如下所示 syntax = "proto3"; package Test; ...
- 监听数组Array变化或Obj属性变化
工作中经常会遇到监听数组发生变化时执行相应的回调触发逻辑,客户应用场景中需要实现对象变量的动态监听,当变量发生变化时触发回调函数,实现事件发送等应用场景. 通常由以下两种方式实现需求 一. ...
- numpy 索引,切片 ,转置,变值,多个数组的拼接
- python判断ip所属地区 python 判断ip 网段
IP地址是互联网中唯一标识一个设备的地址,有时候需要判断一个IP地址所属的地区,这就需要用到IP地址归属查询.本文将介绍Python如何通过IP地址查询所属地区并展示代码. 一. IP地址归属查询 I ...
- LSP 链路状态协议
转载请注明出处: 链路状态协议(Link State Protocol)是一种在计算机网络中用于动态计算路由的协议.它的主要作用是收集网络拓扑信息,为每个节点构建一个准确的网络图,并基于这些信息计算出 ...
- Python并发编程——paramiko远程控制的模块、病毒攻击原理、dll注入、
文章目录 paramiko模块 作业 攻击原理解析 一.什么是dll 二.为何要有dll 什么是dll注入: 什么时候需要dll注入 dll注入的方法 使用SetWindowsHookEx函数对应用程 ...
- linux常见命令(五)
用于文本内容处理的相关命令 sort uniq cut comm diff sort:对文件中数据进行排序,并将结果显示在标准输出上 命令语法:sort [选项] [文件] 选项 选项含义 -m 如果 ...
- 定时重启Nginx、MySql等服务
利用 Linux Crontab,每天定时重启 Nginx.MySQL等服务. 命令行格式说明 f1 f2 f3 f4 f5 program 其中 f1 是表示分钟,f2 表示小时,f3 表示一个月份 ...
- 整理unity资料
https://www.cnblogs.com/fly-100/p/3910515.html 协同的概念介绍
- 题解 SP15454
前言 数学符号约定 \(\operatorname{lowbit}(x)\):表示 \(x\) 的二进制最低位. \([a,b]\):表示区间 \(a\sim b\),其中包含 \(a,\,b\) 端 ...