spring-statemachine入门 介绍 spring-statemachine
是spring
设计于处理状态的变化,以及定义状态之间的转换规则。
状态机的基本组成
状态机主要由4要素组成:
现态:当前所处的状态。
条件:或者称为“事件”,在状态转换图中使用箭头标识,当满足某个条件(触发某个事件)之后,由一个状态转移到另一个状态。比如下图中“支付”这个箭头。
动作:表示由现态转换到次态之后,需要执行的动作,不是必须的,可以是转换转换后不执行任何动作。比如下图中从[待收货]转换到[已收货]是否设置某个动作。
次态:下一环状态。
比如一个支付下单流程的状态转换图:
订单的开始状态为[待支付],当用户支付之后状态流转为[已支付],这个地方是个分支节点,当状态流转到[已支付]后,根据一些业务规则,将状态流转到[待收货]或者[待开票],若是[待收货],通过收货行为,使状态流转到[结束];若是[待开票]状态,通过投递行为,使状态流转到[结束]。
Spring stateMachine Spring StateMachine
是一个基于 Spring
框架的状态机框架,使用Spring StateMachine
可以方便地在 Java
应用中实现状态机功能,并可以与Spring
框架的其他功能结合使用。
基本用法 1.添加依赖
我这里使用的是springboot
工程,最好先引入官方提供的spring-statemachine-bom
,这样基本上不会出现与spring framework
版本不对应,出现的一些无法解释的坑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <dependencyManagement > <dependencies > <dependency > <groupId > org.springframework.statemachine</groupId > <artifactId > spring-statemachine-bom</artifactId > <version > 2.3.1</version > <type > pom</type > <scope > import</scope > </dependency > </dependencies > </dependencyManagement > <dependencies > <dependency > <groupId > org.springframework.statemachine</groupId > <artifactId > spring-statemachine-starter</artifactId > </dependency > </dependencies >
2.订单状态枚举类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Getter public enum StateEnum { UNPAID("unpaid" ,"待支付" ), PAID("paid" ,"已支付" ), WAITING_FOR_RECEIVE("waitingForReceive" ,"待收货" ), DONE("done" ,"结束" ), INVOICE("invoice" , "开票" ) ; private final String type; private final String value; StateEnum(String type, String value) { this .type = type; this .value = value; } public static StateEnum getByState (String state) { for (StateEnum stateEnum : StateEnum.values()) { if (stateEnum.getType().equals(state)) { return stateEnum; } } return null ; } }
3.事件枚举类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Getter public enum EventEnum { PAY("pay" ,"支付" ), RECEIVE("receive" ,"收货" ), DELIVERY("delivery" ,"发货" ) ; private final String type; private final String value; EventEnum(String type, String value) { this .type = type; this .value = value; } }
4.状态机配置
状态机配置类中包含状态配置、状态转换事件关系配置、监听器配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 @Configuration @EnableStateMachineFactory(name = "feeStateMachineFactory") @Slf4j public class FeeStateMachineConfig extends StateMachineConfigurerAdapter <StateEnum , EventEnum > { @Override public void configure (StateMachineStateConfigurer<StateEnum, EventEnum> states) throws Exception { states.withStates() .initial(StateEnum.UNPAID) .choice(StateEnum.PAID) .states(EnumSet.allOf(StateEnum.class)); } @Override public void configure (StateMachineConfigurationConfigurer<StateEnum, EventEnum> config) throws Exception { config.withConfiguration() .listener(listener()) .machineId("stateMachine" ) .autoStartup(false ); } @Override public void configure (StateMachineTransitionConfigurer<StateEnum, EventEnum> transitions) throws Exception { transitions .withExternal() .source(StateEnum.UNPAID) .target(StateEnum.PAID) .event(EventEnum.PAY) .guard(new Guard<StateEnum, EventEnum>() { @Override public boolean evaluate (StateContext<StateEnum, EventEnum> context) { log.info("进入订单守卫函数" ); return true ; } }) .action(ctx -> log.info("action状态变更:{} -> {}." , StateEnum.UNPAID.getValue(), StateEnum.PAID.getValue())) .and() .withExternal() .source(StateEnum.WAITING_FOR_RECEIVE) .target(StateEnum.DONE) .event(EventEnum.RECEIVE) .action(ctx -> log.info("action状态变更:{} -> {}." , StateEnum.WAITING_FOR_RECEIVE.getValue(), StateEnum.DONE.getValue())) .and() .withChoice() .source(StateEnum.PAID) .first(StateEnum.INVOICE, ctx -> (boolean ) Optional.ofNullable(ctx.getMessage().getHeaders().get("invoice" )).orElse(true ), ctx -> log.info("进入发票分支" )) .last(StateEnum.WAITING_FOR_RECEIVE, ctx -> log.info("进入收货分支" )) .and() .withExternal() .source(StateEnum.INVOICE) .target(StateEnum.WAITING_FOR_RECEIVE) .event(EventEnum.DELIVERY) .action(ctx -> log.info("action状态变更:{} -> {}." , StateEnum.INVOICE.getValue(), StateEnum.WAITING_FOR_RECEIVE.getValue())) ; } private StateMachineListener<StateEnum, EventEnum> listener () { return new StateMachineListenerAdapter<StateEnum, EventEnum>() { @Override public void transition (Transition<StateEnum, EventEnum> transition) { log.info("listener[{}]状态变更:{} -> {}" , transition.getKind().name(), transition.getSource() == null ? "NULL" : ofNullableState(transition.getSource().getId()), transition.getTarget() == null ? "NULL" : ofNullableState(transition.getTarget().getId())); } @Override public void eventNotAccepted (Message<EventEnum> event) { log.error("事件未收到: {}" , event); } private Object ofNullableState (StateEnum s) { return Optional.ofNullable(s) .map(StateEnum::getValue) .orElse(null ); } }; } }
4.测试调用
1 2 3 4 stateMachine = feeStateMachineFactory.getStateMachine("stateMachine" ); stateMachine.sendEvent(MessageBuilder.withPayload(EventEnum.PAY) .setHeader(JSON_STR, JSON.toJSONString(feeOrder)) .build())
使用注解方式配置监听器 前面说到,如果需要使用注解方式配置监听器,一定一定要注意machineId
的赋值。因为可以通过多种方式来处理状态转移之后的监听,所以并不一定所有人在使用时都会使用这种方式,但是使用这种方式就是一定要注意machineId
的赋值问题。(可以在定义配置时就设置action
也可以达到状态转移后进入某个函数进行相关处理。)
spring stateMachine
中设置的监听,这些监听都有设置对应的注解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public interface StateMachineListener <S ,E > { void stateChanged (State<S,E> from, State<S,E> to) ; void stateEntered (State<S,E> state) ; void stateExited (State<S,E> state) ; void eventNotAccepted (Message<E> event) ; void transition (Transition<S, E> transition) ; void transitionStarted (Transition<S, E> transition) ; void transitionEnded (Transition<S, E> transition) ; void stateMachineStarted (StateMachine<S, E> stateMachine) ; void stateMachineStopped (StateMachine<S, E> stateMachine) ; void stateMachineError (StateMachine<S, E> stateMachine, Exception exception) ; void extendedStateChanged (Object key, Object value) ; void stateContext (StateContext<S, E> stateContext) ; }
可以通过注解的方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @WithStateMachine public class StateMachineEventListener { @OnTransition(target = "UNPAID") public void create () { System.out.println("订单创建" ); } @OnTransition(source = "UNPAID", target = "PAID") public void pay (Message<EventEnum> obj) { System.out.println("用户支付完毕,状态机反馈信息:" ); } }
持久化 一般来说,我们的处理逻辑为从状态机工程中获取到一个状态机,但是不一定说每一台状态机都是从初始状态开始走,有可能订单目前处在某个状态,但是由于一些原因重启了服务,那么这是如果重新创建状态机那么状态需要从头开始走,这显然不符合逻辑,所以在获取到状态机之后,需要为这台状态机重新赋状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 stateMachine = feeStateMachineFactory.getStateMachine("stateMachine" ); stateMachine.stop(); stateMachine.getStateMachineAccessor().doWithAllRegions(sma -> { sma.addStateMachineInterceptor(new StateMachineInterceptorAdapter<StateEnum, EventEnum>() { @Override public void preStateChange (State<StateEnum, EventEnum> state, Message<EventEnum> message, Transition<StateEnum, EventEnum> transition, StateMachine<StateEnum, EventEnum> stateMachine, StateMachine<StateEnum, EventEnum> rootStateMachine) { log.info("preStateChange" ); FeeOrder result = JSON.parseObject(String.class.cast(message.getHeaders().get(JSON_STR)), FeeOrder.class); result.setOrderStatus(state.getId().getType()); } }); sma.resetStateMachine(new DefaultStateMachineContext<>(stateEnum, null , null , null , null , "stateMachine" )); }); stateMachine.start();
尽管可以手动对状态机赋状态,但是这种方式也解决不了根本问题,以订单为例子,如果用户在发起创建订单之后,订单处在待支付状态,然后用户一直没有去支付,这时出现需要重启服务器的情况;或者如果部署了多个实例,某一台实例中状态机处在待支付状态,而用户去支付时请求走到了另一台服务器,这时在另一台服务器上状态走到了待开票,如果开票的请求又走到了原本处在待支付状态的服务器上呢?所以这里就变成必须与数据库中的订单状态实时校验?
所以这里引入的持久化。主要针对于故障恢复、可恢复性、长时间运行等情况。
持久化有多种方式,基于内存、基于db
、基于redis
、基于MongoDB
等,这里以redis
为例。
1.引入依赖
1 2 3 4 5 <dependency > <groupId > org.springframework.statemachine</groupId > <artifactId > spring-statemachine-redis</artifactId > <version > 1.2.14.RELEASE</version > </dependency >
2.redis配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Configuration public class StateMachinePersisterConfig { @Resource private RedisConnectionFactory redisConnectionFactory; @Bean public RedisStateMachinePersister<StateEnum, EventEnum> persister () { RedisStateMachineContextRepository<StateEnum, EventEnum> repository = new RedisStateMachineContextRepository<StateEnum, EventEnum>(redisConnectionFactory); return new RedisStateMachinePersister<>(new RepositoryStateMachinePersist<>(repository)); } }
3.使用redis进行持久化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private synchronized boolean sendEvent (Events changeEvent, Order order) { boolean result = false ; try { stateMachine.start(); stateMachineMemPersister.restore(stateMachine, String.valueOf(order.getId())); Message message = MessageBuilder.withPayload(changeEvent) .setHeader("order" , order).build(); result = stateMachine.sendEvent(message); stateMachineMemPersister.persist(stateMachine, String.valueOf(order.getId())); } catch (Exception e) { System.out.println("操作失败:" + e.getMessage()); } finally { stateMachine.stop(); } return result; }
小结 spring stateMachine
在简单实用之后感觉虽然在一定层度上对状态装换过程的代码编写风格进行了解耦,但是感觉由于spring stateMachine
有点过于重量化,导致如果使用不当容易出现一些不可预判、出乎意料之外的问题。就比如之前提到的版本不对应监听注解失效的问题,在使用过程中它也不会报错,仅仅是始终无法进入到对应的注解中;还比如choice
分支的问题、状态机不持久化带来的问题、若使用了持久化,持久化带来的问题等。