玩转 Spring 状态机:打造灵活高效的业务逻辑流,太优雅了!

科技   2024-11-29 15:08   安徽  

来源:https://blog.csdn.net/wenbin729392753

👉 欢迎加入小哈的星球,你将获得: 专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17..., 点击查看项目介绍
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2期已完结,演示链接:http://116.62.199.48/;

截止目前,累计输出 71w+ 字,讲解图 2776+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,Spring Cloud Alibaba 等等,戳我加入学习,解锁全部项目,已有2400+小伙伴加入

在现代电子商务系统中,订单的状态管理是一个非常重要的环节。订单从创建到最终完成或取消,通常会经历多个状态的转换。如何高效地管理这些状态流转,并在系统中灵活地扩展状态和行为,是我们在开发中需要解决的问题。

本文将详细介绍如何在Spring Boot项目中使用Spring Statemachine框架来实现订单状态流转控制。

一、Spring Statemachine概述

Spring Statemachine是由Spring团队提供的一个轻量级状态机框架。它为开发者提供了一种简便且强大的方式来管理复杂的状态流转逻辑,尤其适用于订单处理、工作流引擎等需要状态管理的场景。

Spring Statemachine具有以下特点:

  • 灵活的状态配置: 通过Java配置或外部配置文件定义状态和状态转换。
  • 支持并发状态和嵌套状态: 可以管理复杂的状态图。
  • 与Spring生态系统的良好集成: 易于与Spring Boot、Spring Security等集成。

二、订单状态流转场景分析

在一个典型的订单处理流程中,订单可能会经历以下几个状态:

  • 新建 (NEW): 订单刚刚创建,等待支付。
  • 已支付 (PAID): 用户完成支付,等待发货。
  • 已发货 (SHIPPED): 订单已经发货,等待收货。
  • 已完成 (COMPLETED): 用户确认收货,订单完成。
  • 已取消 (CANCELLED): 订单在任意状态下都可能被取消。

这些状态之间可能存在以下转换关系:

  • 新建 -> 已支付
  • 已支付 -> 已发货
  • 已发货 -> 已完成
  • 新建 -> 已取消
  • 已支付 -> 已取消

三、Spring Statemachine的原理与实现

Spring Statemachine中,状态和状态转换(Transitions)通过配置来定义。每个状态转换都由事件(Events)触发,从而使状态从一个状态流转到另一个状态。

1. 引入依赖

首先,在Spring Boot项目中引入Spring Statemachine的依赖。

<dependency>
 <groupId>org.springframework.statemachine</groupId>
 <artifactId>spring-statemachine-starter</artifactId>
</dependency>

2. 定义状态和事件

接下来,我们定义订单状态和事件。

public enum OrderStates {
    NEW, PAID, SHIPPED, COMPLETED, CANCELLED
}
 
public enum OrderEvents {
    PAY, SHIP, COMPLETE, CANCEL
}

3. 配置状态机

使用StateMachineConfigurerAdapter来配置状态机,包括状态、事件、监听器和持久化及其转换关系。

@Configuration
@EnableStateMachine
public class StateMachineConfig extends StateMachineConfigurerAdapter<OrderStates, OrderEvents> {
 
    private static final Logger log = LoggerFactory.getLogger(StateMachineConfig.class);
 
    @Override
    public void configure(StateMachineConfigurationConfigurer<OrderStates, OrderEvents> config)
            throws Exception {
        config
                .withConfiguration()
                .autoStartup(true)
                .listener(listener());
    }
 
    @Override
    public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states)
            throws Exception {
        states
                .withStates()
                .initial(OrderStates.NEW)
                .end(OrderStates.COMPLETED)
                .end(OrderStates.CANCELLED)
                .states(EnumSet.allOf(OrderStates.class));
    }
 
    @Override
    public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
        transitions
                .withExternal()
                .source(OrderStates.NEW).target(OrderStates.PAID).event(OrderEvents.PAY)
                .and()
                .withExternal()
                .source(OrderStates.PAID).target(OrderStates.SHIPPED).event(OrderEvents.SHIP)
                .and()
                .withExternal()
                .source(OrderStates.SHIPPED).target(OrderStates.COMPLETED).event(OrderEvents.COMPLETE)
                .and()
                .withExternal()
                .source(OrderStates.NEW).target(OrderStates.CANCELLED).event(OrderEvents.CANCEL)
                .and()
                .withExternal()
                .source(OrderStates.PAID).target(OrderStates.CANCELLED).event(OrderEvents.CANCEL);
    }
 
    @Bean
    public StateMachineListener<OrderStates, OrderEvents> listener() {
        return new StateMachineListenerAdapter<>() {
            @Override
            public void stateChanged(State<OrderStates, OrderEvents> from, State<OrderStates, OrderEvents> to) {
                log.info("State change to: {}", to.getId());
            }
 
            @Override
            public void stateMachineError(StateMachine<OrderStates, OrderEvents> stateMachine, Exception exception) {
                log.error("Exception caught: {}", exception.getMessage(), exception);
            }
 
            @Override
            public void eventNotAccepted(Message<OrderEvents> message) {
                Order order = (Order) message.getHeaders().get("order");
                log.error("Order state machine can't change state {} --> {}", Objects.requireNonNull(order).getStatus(), message.getPayload());
            }
        };
    }
 
    @Bean
    public StateMachinePersist<OrderStates, OrderEvents, String> inMemoryStateMachinePersist() {
        return new StateMachinePersist<>() {
            private final Map<String, StateMachineContext<OrderStates, OrderEvents>> contexts = new HashMap<>();
 
            @Override
            public void write(StateMachineContext<OrderStates, OrderEvents> context, String contextObj) {
                contexts.put(contextObj, context);
            }
 
            @Override
            public StateMachineContext<OrderStates, OrderEvents> read(String contextObj) {
                return contexts.get(contextObj);
            }
        };
    }
}

4. 配置状态改变后的处理器

使用@WithStateMachine来配置状态机流转后的后续逻辑,比如更新订单状态、发邮件等。

@Service
@WithStateMachine
public class OrderStateChangeHandler {
 
    private static final Logger log = LoggerFactory.getLogger(OrderStateChangeHandler.class);
 
    @OnTransition(source = "NEW", target = "PAID")
    public void payTransition(Message<OrderEvents> message) {
        Order order = (Order) message.getHeaders().get("order");
        log.info("Handle Pay Order:{}", order);
        // 其他业务 如保存订单状态
        Objects.requireNonNull(order).setStatus(OrderStates.PAID);
        //orderRepository.save(order);
 
    }
 
    @OnTransition(source = "PAID", target = "SHIPPED")
    public void shipTransition(Message<OrderEvents> message) {
        Order order = (Order) message.getHeaders().get("order");
        log.info("Handle Ship Order:{}", order);
        // 其他业务 如更新订单
        Objects.requireNonNull(order).setStatus(OrderStates.SHIPPED);
//        orderMapper.updateById(order);
    }
    @OnTransition(source = "SHIPPED", target = "COMPLETED")
    public void completeTransition(Message<OrderEvents> message) {
        Order order = (Order) message.getHeaders().get("order");
        log.info("Handle Complete Order:{}", order);
 
        // 其他业务 如更新订单
        Objects.requireNonNull(order).setStatus(OrderStates.COMPLETED);
//        orderMapper.updateById(order);
    }
 
}

5. 使用状态机控制订单状态流转

配置好状态机后,我们可以在业务逻辑中使用它来控制订单的状态流转。

@Service
public class OrderService {
 
    private static final Logger log = LoggerFactory.getLogger(OrderService.class);
 
    private final StateMachine<OrderStates, OrderEvents> stateMachine;
 
    public OrderService(StateMachine<OrderStates, OrderEvents> stateMachine) {
        this.stateMachine = stateMachine;
    }
 
    public void payOrder(Integer id) {
        log.info("Pay Order: {}", id);
        Order order = new Order("12345""Sample Order", OrderStates.NEW, new BigDecimal("99.99"));
        // 模拟支付
//        payService.payOrder(1);
        Message<OrderEvents> message = MessageBuilder.withPayload(OrderEvents.PAY).setHeader("order", order).build();
        stateMachine.sendEvent(Mono.just(message)).subscribe();
    }
 
    public void shipOrder(Integer id) {
        log.info("Ship Order: {}", id);
        Order order = new Order("12345""Sample Order", OrderStates.PAID, new BigDecimal("99.99"));
        // 模拟发货
//        tradeService.shipOrder(1);
        Message<OrderEvents> message = MessageBuilder.withPayload(OrderEvents.SHIP).setHeader("order", order).build();
        stateMachine.sendEvent(Mono.just(message)).subscribe();
    }
 
    public void completeOrder(Integer id) {
        log.info("Complete Order: {}", id);
        Order order = new Order("12345""Sample Order", OrderStates.SHIPPED, new BigDecimal("99.99"));
 
        Message<OrderEvents> message = MessageBuilder.withPayload(OrderEvents.COMPLETE).setHeader("order", order).build();
        stateMachine.sendEvent(Mono.just(message)).subscribe();
    }
 
}

6. 控制订单流转

在控制层中我们可以根据不同的请求来触发状态转换:

@GetMapping("/order/pay/{id}")
public String payOrder(@PathVariable("id") Integer id) {
     orderService.payOrder(id);
     return "Order paid ";
}
 
@GetMapping("/order/ship/{id}")
public String shipOrder(@PathVariable("id") Integer id) {
    orderService.shipOrder(id);
    return "Order shipped ";
}
 
@GetMapping("/order/complete/{id}")
 public String completeOrder(@PathVariable("id") Integer id) {
    orderService.completeOrder(id);
    return "Order completed ";
}

四、原理解析

Spring Statemachine 的核心是有限状态机模型,它使用状态、事件、和转换来控制业务流程。状态机会在不同的状态之间进行转换,每次转换都会触发相关的操作。状态机还支持嵌套状态、并发状态等复杂场景。

  • 状态(State): 表示业务流程中的不同阶段。
  • 事件(Event): 触发状态转换的操作。
  • 转换(Transition): 状态之间的变化,通常由事件驱动。

Spring Statemachine通过定义状态机的配置,允许开发者灵活地管理状态和转换逻辑。

五、测试

当我们按照上面的步骤,配置好了状态机之后,接下来我们测试一下,配置的状态机是否能达到预期的目的。

1.NEW --> SHIPPED

因为我们配置的订单流转规则中,NEW只能转换到PAIDCANCELLED,所以我们期望的是订单不能流转成功。

在浏览器中访问:http://localhost:8082/order/ship/1

图片

控制台中打印如下错误信息,并且处理器中业务未执行,和我们的预期一致。

2. NEW --> COMPLETE

同上面一样,期望是会出现错误,不能转换成功。访问:http://localhost:8082/order/complete/1

图片

3. NEW --> PAID

由于在状态机的配置,NEW是可以转换成PAID的。所以,期望能转换成功。访问:http://localhost:8082/order/pay/1

图片

我们从控制台中可以看到, 订单状态流转成功,并且进入到handler中进行订单流转后的业务处理。这时该订单的状态已经变成PAID。

如果我们在来支付一次,结果会怎么样?

图片

执行一次结果是报错,因为上一次请求,该订单的状态已经流转为了PAID,所以再次流转NEW --> PAY就会报错。

4. PAID --> SHIPPED

属于配置允许的状态流转,所以期望能够转换成功。访问:http://localhost:8082/order/ship/1

图片

从控制台中可以看到能够成功流转,因为符合我们配置的状态机流转规则。

同样如果我们再调用一次会怎么样?

图片

和上一步的测试结果一样,因为该订单的状态已经扭转为了SHIPPED,所以它不能再次转换为SHIPPED

5.  SHIPPED --> COMPLETED

我们之间访问:http://localhost:8082/order/complete/1

该订单当前状态为SHIPPED,根据配置的规则可以转换为COMPLETED

图片

结果和我们预期的一致。

六、总结

在Spring Boot中使用Spring Statemachine,可以帮助我们高效地管理订单等业务流程中的状态流转逻辑。通过简单的配置和灵活的状态转换定义,我们可以实现复杂的状态控制。Spring Statemachine不仅仅适用于订单处理,还可以应用于各种需要状态管理的场景。

通过本文的介绍和代码示例,希望大家能够掌握如何在Spring Boot项目中使用Spring Statemachine,实现订单状态流转控制,并且能够将其应用到更多的实际开发场景中。

👉 欢迎加入小哈的星球,你将获得: 专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17..., 点击查看项目介绍
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2期已完结,演示链接:http://116.62.199.48/;

截止目前,累计输出 71w+ 字,讲解图 2776+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,Spring Cloud Alibaba 等等,戳我加入学习,解锁全部项目,已有2400+小伙伴加入



1. 我的私密学习小圈子,从0到1手撸企业实战项目!

2. 让数据传输更优雅:SpringBoot 前后端加密技巧全攻略

3. 提高系统吞吐量的一把利器:DeferredResult 到底有多强?

4. 2024年,只有搞颜色的 P 站真正关心网站性能

最近面试BAT,整理一份面试资料Java面试BATJ通关手册,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。

获取方式:点“在看”,关注公众号并回复 Java 领取,更多内容陆续奉上。

PS:因公众号平台更改了推送规则,如果不想错过内容,记得读完点一下在看,加个星标,这样每次新文章推送才会第一时间出现在你的订阅列表里。

“在看”支持小哈呀,谢谢啦

Java架构师宝典
专注于 Java 面试题、干货文章分享,不限于算法,数据库,Spring Boot, 微服务,高并发,JVM,Docker 容器,ELK相关知识,期待与您一同进步。
 最新文章