SpringBoot集成Spring Statemachine(状态机)完整示例

背景:
本文将基于借款订单状态流转这个场景来实现。如果使用if-else或者switch语句来处理这些状态,代码会变得非常臃肿且难以维护。而状态机提供了一种更加结构化和可维护的方式来管理这些状态转换。
示例中涉及到:状态机的配置、数据持久化、状态恢复查询、同一事件由同一sourceStatus流转到不同targetStatus
SpringBoot集成状态机:

1、首先,需要添加Spring Statemachine的依赖到Spring Boot项目的pom.xml文件中:

<dependency>
    <groupId>org.springframework.statemachine</groupId>
    <artifactId>spring-statemachine-core</artifactId>
    <version>4.0.0</version>
</dependency>

2、定义系统中订单存在的状态

public enum OrderStatusEnum {

// 待审核
APPROVE_PENDING,
// 审核中
APPROVE_ING,
// 审核失败
APPROVE_FAILED,
// 审核成功
APPROVE_SUCCESS;

}

3、定义系统中触发状态变更的事件

public enum OrderEvent {
// 开始审核
APPROVE_START,
// 审核通过
APPROVE_SUCCESS,
// 审核失败
APPROVE_FAILED;

}

4、状态机-状态流转配置

@Configuration
@EnableStateMachine(name = "OrderStateMachine")
@Slf4j
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStatusEnum, OrderEvent> {

@Resource
private OrderMapper orderMapper;


/**
 * 配置状态
 *
 * @param states
 * @throws Exception
 */
@Override
public void configure(StateMachineStateConfigurer<OrderStatusEnum, OrderEvent> states) throws Exception {
    states.withStates()
            .initial(OrderStatusEnum.LOAN_PENDING) // 设置初始状态为[待审核]
            .states(EnumSet.allOf(OrderStatusEnum.class));
}


/**
 * 配置状态转换事件关系
 *
 * @param transitions
 * @throws Exception
 */
@Override
public void configure(StateMachineTransitionConfigurer<OrderStatusEnum, OrderEvent> transitions) throws Exception {
    transitions
           //当执行 【开始审核】操作时,将订单状态由待审核 -> 审核中
           .withExternal().source(OrderStatusEnum.APPROVE_PENDING).target(OrderStatusEnum.APPROVE_ING).event(OrderEvent.APPROVE_START)
            .and()
            //当执行 【审核失败】操作时,将订单状态由审核中 -> 审核失败
            .withExternal().source(OrderStatusEnum.APPROVE_ING).target(OrderStatusEnum.APPROVE_FAILED).event(OrderEvent.APPROVE_FAILED)
            .and()
            //当执行 【审核成功】操作时,将订单状态由审核中 -> 审核成功
            .withExternal().source(OrderStatusEnum.APPROVE_ING).target(OrderStatusEnum.APPROVE_SUCCESS).event(OrderEvent.APPROVE_SUCCESS);
}


/**
 * 持久化配置
 *
 * @return
 */
@Bean
public DefaultStateMachinePersister persister() {
    return new DefaultStateMachinePersister<>(new StateMachinePersist<OrderStatusEnum, OrderEvent, BizOrder>() {
        @Override
        public void write(StateMachineContext<OrderStatusEnum, OrderEvent> context, BizOrder order) throws Exception {
            OrderStatusEnum orderStatus = context.getState();
            log.info("订单状态持久化,订单ID:{},目标状态:{}", order.getId(), orderStatus);
            orderMapper.updateOrderStatus(order.getId(), orderStatus);
        }

        @Override
        public StateMachineContext<OrderStatusEnum, OrderEvent> read(BizOrder order) throws Exception {
            log.info("恢复订单状态机状态");
            return new DefaultStateMachineContext<>(order.getStatus(), null, null, null);
        }

    });
}

}

5、新建一个变更订单状态的服务

public interface BizOrderStatusService {

    /**
     *
     * 通用状态变更处理器
     * @param incomingId
     * @param event
     */
    void eventHandler(Long orderId, OrderEvent event);

}

@Service
public class BizOrderStatusServiceImpl implements BizOrderStatusService {

    @Resource
    private OrderMapper orderMapper;

    @Resource
    private StateMachine<OrderStatusEnum, OrderEvent> orderStateMachine;

    @Resource
    private StateMachinePersister<OrderStatusEnum, OrderEvent, BizOrder> persister;


/**
 * 
 * 
 * @param orderId 订单id
 * @param event   事件类型
 */
    @Override
    public void eventHandler(Long orderId, OrderEvent event) {
        BizOrder order = orderMapper.getOrderById(orderId);
        Assert.notNull(order, "订单不存在");
        // 自定义状态机参数对象(可以在此对象中定义后续需要用到的字段参数,状态配置那里如果需要做业务逻辑判断)
        StateMachineParam param = new StateMachineParam();
        param.setBizOrder(order);
        Message message = MessageBuilder.withPayload(event).build();
        if (!sendEvent(message, param)) {
            throw new ApplicationBizException("订单状态流转异常");
        }
    }


    /**
     * 发送订单状态转换事件 这里不要使用synchronized锁方法,效率比较低,
     * 分布式系统优先采用分布式锁,下单锁userId,订单状态流转锁orderId根据业务考虑使用什么。
     *
     * @param message
     * @param param
     * @return
     */
    private synchronized boolean sendEvent(Message<OrderEvent> message, StateMachineParam param) {
        boolean result = false;
        try {
            orderStateMachine.start();
            //尝试恢复状态机状态
            persister.restore(orderStateMachine, param.getBizOrder());
            orderStateMachine.getExtendedState().getVariables().put("param", param);
            result = orderStateMachine.sendEvent(message);
            //持久化状态机状态
            persister.persist(orderStateMachine, param.getBizOrder());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            orderStateMachine.stop();
        }
        return result;
    }

}

6、调用方法执行订单状态变更,并持久化到数据库

@RestController
@RequiredArgsConstructor
public class ApproveController {

 private final OrderStatusService orderStatusService;
 

/**
 * 前端调用start方法将订单状态改为审核中,并自动持久化到数据库
 * @param orderId  订单id
 * @return
 */
@PostMapping("/start")
public void start(Long orderId) {
    orderStatusService.eventHandler(orderId,OrderEvent.APPROVE_START);
}

/**
 * 前端调用start方法将订单状态改为审核成功,并自动持久化到数据库
 * @param orderId  订单id
 * @return
 */
@PostMapping("/approveSuccess")
public void approveSuccess(Long orderId) {
    orderStatusService.eventHandler(orderId,OrderEvent.APPROVE_SUCCESS);
}

/**
 * 前端调用start方法将订单状态改为审核失败,并自动持久化到数据库
 * @param orderId  订单id
 * @return
 */
@PostMapping("/approveFailed")
public void approveFailed(Long orderId) {
    orderStatusService.eventHandler(orderId,OrderEvent.APPROVE_FAILED);
}

}

现在,我们已经配置了状态机并创建了服务来操作它。接下来,你可以在应用的任何部分注入OrderStatusService,并传入相应的事件来改变订单的状态。

7、总结:以上就是状态机的基础用法,一个事件对应一种来源状态(sourceStatus)和目标状态(targetStatus)。在我自己使用到的场景中还包含一个事件需要根据不同的条件将同一来源状态流转到不同的目标状态。这时我们就需要在状态映射配置中增加业务逻辑判断。

8、扩展(新增一个放款事件,该事件会将订单状态由【审核成功】流转到【放款成功】或者【部分放款成功】,具体流流转哪一个状态是由订单的放款金额决定的,如果申请金额和放款金额一致就是【放款成功】,放款金额小于申请金额就是【部分放款成功】)

8.1 我们在订单状态枚举中新增(LOAN_SUCCESS, PARTIALLY_LOAN_SUCCESS)

// 待审核
APPROVE_PENDING,
// 审核中
APPROVE_ING,
// 审核失败
APPROVE_FAILED,
// 审核成功
APPROVE_SUCCESS,
// 放款成功
LOAN_SUCCESS,
// 部分放款成功
PARTIALLY_LOAN_SUCCESS;

8.2 我们在事件枚举中新增(LOAN)

// 开始审核
APPROVE_START,
// 审核通过
APPROVE_SUCCESS,
// 审核失败
APPROVE_FAILED,
// 操作放款
LOAN;

8.3 优化一下上面的【配置状态转换事件关系】,需要在事件后面增加条件判断(通过guard()实现)

/**
 * 配置状态转换事件关系
 *
 * @param transitions
 * @throws Exception
 */
@Override
public void configure(StateMachineTransitionConfigurer<OrderStatusEnum, OrderEvent> transitions) throws Exception {
    transitions
           //当执行 【开始审核】操作时,将订单状态由待审核 -> 审核中
           .withExternal().source(OrderStatusEnum.APPROVE_PENDING).target(OrderStatusEnum.APPROVE_ING).event(OrderEvent.APPROVE_START)
            .and()
            //当执行 【审核失败】操作时,将订单状态由审核中 -> 审核失败
            .withExternal().source(OrderStatusEnum.APPROVE_ING).target(OrderStatusEnum.APPROVE_FAILED).event(OrderEvent.APPROVE_FAILED)
            .and()
            //当执行 【审核成功】操作时,将订单状态由审核中 -> 审核成功
            .withExternal().source(OrderStatusEnum.APPROVE_ING).target(OrderStatusEnum.APPROVE_SUCCESS).event(OrderEvent.APPROVE_SUCCESS)
            .and()
            //当执行 【放款】操作时,将订单状态由审核成功 -> 放款成功
            .withExternal().source(OrderStatusEnum.APPROVE_SUCCESS).target(OrderStatusEnum.LOAN_SUCCESS).event(OrderEvent.LOAN).guard(guardForLoanSuccessByLoan())
            .and()
            //当执行 【放款】操作时,将订单状态由审核成功 -> 部分放款成功
            .withExternal().source(OrderStatusEnum.APPROVE_SUCCESS).target(OrderStatusEnum.PARTIALLY_LOAN_SUCCESS).event(OrderEvent.LOAN).guard(guardForPartiallyLoanSuccessByLoan());
}



/**
 * 订单状态由审核通过 -> 放款成功
 * 触发条件:订单申请金额=放款金额
 *
 * @return
 */
@Bean
public Guard<OrderStatusEnum, OrderEvent> guardForLoanSuccessByLoan() {
    return context -> {
        // 从扩展信息中获取参数
        StateMachineParam param = (StateMachineParam) context.getExtendedState().getVariables().get("param");
        BizOrder order = param.getBizOrder();
        // 如果申请金额=放款金额 ,返回true,状态机就会流转到调用此方法的目标状态
        if (order.getApplyAmount().compareTo(order.getLoanAmlunt) == 0) {
            return true;
        }
        return false;
    };
}

/**
 * 订单状态由审核通过 -> 部分放款成功
 * 触发条件:订单申请金额<放款金额
 *
 * @return
 */
@Bean
public Guard<OrderStatusEnum, OrderEvent> guardForPartiallyLoanSuccessByLoan() {
    return context -> {
        // 从扩展信息中获取参数
        StateMachineParam param = (StateMachineParam) context.getExtendedState().getVariables().get("param");
        BizOrder order = param.getBizOrder();
        // 如果申请金额<放款金额 ,返回true,状态机就会流转到调用此方法的目标状态
        if (order.getApplyAmount().compareTo(order.getLoanAmlunt) < 0) {
            return true;
        }
        return false;
    };
}

通过以上操作我们就可以实现业务中某些需要根据不同条件流转到不同状态的场景。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容