Spring Statemachine 简介

Spring Statemachine 简介

    Spring Statemachine是Spring官方提供的一个框架,供应用程序开发人员在Spring应用程序中使用状态机。支持状态的嵌套(substate),状态的并行(parallel,fork,join)、子状态机等等。

官网地址:https://projects.spring.io/spring-statemachine/

本文使用版本:2.2.0.RELEASE

Spring Statemachine 项目模块

image.png

 

使用 Spring Statemachine

     Spring Statemachine 的设计是有状态的,每个statemachine实例内部保存了当前状态机上下文相关的属性(比如当前状态、上一次状态等),因此是线程不安全的。因此,在实际场景中,基本上都是利用工厂创建状态机。

配置 Spring Statemachine 工厂

方式一 通过Adapter构建工厂

@Configuration
@EnableStateMachineFactory
public class Config6
        extends EnumStateMachineConfigurerAdapter<States, Events> {

    @Override
    public void configure(StateMachineStateConfigurer<States, Events> states)
            throws Exception {
        states
            .withStates()
                .initial(States.S1)
                .end(States.SF)
                .states(EnumSet.allOf(States.class));
    }

}

public class Bean3 {

    @Autowired
    StateMachineFactory<States, Events> factory;

    void method() {
        StateMachine<States,Events> stateMachine = factory.getStateMachine();
        stateMachine.start();
    }
}

缺点:依赖@Configuration注解和Spring应用上下文,即编译时就需要指定状态机配置。 

方式二 通过Builder构建工厂【推荐】

StateMachine<String, String> buildMachine1() throws Exception {
    Builder<String, String> builder = StateMachineBuilder.builder();
    builder.configureStates()
        .withStates()
            .initial("S1")
            .end("SF")
            .states(new HashSet<String>(Arrays.asList("S1","S2","S3","S4")));
    return builder.build();
}

监听状态机事件

方式一 Spring 上下文事件机制

    状态机中的事件类OnTransitionStartEvent、OnTransitionEvent、OnTransitionEndEvent、OnStateExiteEvent、OnStateEntryEvent、OnStateChangeEvent、OnStateMachineStart、OnStateMachineStop等都是 ApplicationEvent 的子类,因此可以用Spring 中的ApplicationListener。

public class StateMachineApplicationEventListener
        implements ApplicationListener<StateMachineEvent> {

    @Override
    public void onApplicationEvent(StateMachineEvent event) {
        //监听事件处理逻辑
    }
}

@Configuration
public class ListenerConfig {

    @Bean
    public StateMachineApplicationEventListener contextListener() {
        return new StateMachineApplicationEventListener();
    }
}

缺点:Spring Statemachine官方认为Spring ApplicationContext 并不是一个很快的事件总线。从提升性能方面考虑,推荐使用下面的StateMachineListener方式。

方式二 使用StateMachineListener【推荐】

     StateMachineListener 是一个接口,通常使用其实现类StateMachineListenerAdapter:

public class StateMachineListenerAdapter<S, E> implements StateMachineListener<S, E> {

    @Override
    public void stateChanged(State<S, E> from, State<S, E> to) {
    }

    @Override
    public void stateEntered(State<S, E> state) {
    }

    @Override
    public void stateExited(State<S, E> state) {
    }

    @Override
    public void eventNotAccepted(Message<E> event) {
    }

    @Override
    public void transition(Transition<S, E> transition) {
    }

    @Override
    public void transitionStarted(Transition<S, E> transition) {
    }

    @Override
    public void transitionEnded(Transition<S, E> transition) {
    }

    @Override
    public void stateMachineStarted(StateMachine<S, E> stateMachine) {
    }

    @Override
    public void stateMachineStopped(StateMachine<S, E> stateMachine) {
    }

    @Override
    public void stateMachineError(StateMachine<S, E> stateMachine, Exception exception) {
    }

    @Override
    public void extendedStateChanged(Object key, Object value) {
    }

    @Override
    public void stateContext(StateContext<S, E> stateContext) {
    }

}

使用方式:

1、状态机实例添加Listener:

public class Config7 {

    @Autowired
    StateMachine<States, Events> stateMachine;

    @Bean
    public StateMachineEventListener stateMachineEventListener() {
        StateMachineEventListener listener = new StateMachineEventListener();
        stateMachine.addStateListener(listener);
        return listener;
    }

}

2、构建工厂时指定Listener:

 public StateMachine<OrderStates, Events> build() throws Exception {
        StateMachineBuilder.Builder<OrderStates, Events> builder = StateMachineBuilder.builder();

        System.out.println("构建订单状态机");

        builder.configureConfiguration()
                .withConfiguration()
                .beanFactory(beanFactory)
                .machineId("orderMachineId").listener(listener());

        builder.configureStates()
                .withStates()
                .initial(OrderStates.WAIT_PAY)
                .states(EnumSet.allOf(OrderStates.class));

与应用程序交互

    通过上述监听状态机的事件或使用具有状态和转换的动作来与状态机进行交互有点受限制。 有时,这种方法过于局限和冗长,无法与状态机所使用的应用程序进行交互。 对于此特定用例,我们进行了Spring样式的上下文集成,可轻松将状态机功能插入到bean中。

使用:

@WithStateMachine
public class Bean5 {

    @OnTransition(source = "S1", target = "S2")
    public void fromS1ToS2() {
    }

    @OnTransition
    public void anyTransition() {
    }
    
    @StatesOnTransition(source = States.S1, target = States.S2)
    public void fromS1ToS2(@EventHeaders Map<String, Object> headers, ExtendedState extendedState) {
    }
    
    @OnTransition
    public void anyTransition(
            @EventHeaders Map<String, Object> headers,
            @EventHeader("myheader1") Object myheader1,
            @EventHeader(name = "myheader2", required = false) String myheader2,
            ExtendedState extendedState,
            StateMachine<String, String> stateMachine,
            Message<String> message,
            Exception e) {
    }
    
    @OnStateChanged
    public void anyStateChange() {
    }
    
    @OnEventNotAccepted
    public void anyEventNotAccepted() {
    }

    @OnEventNotAccepted(event = "E1")
    public void e1EventNotAccepted() {
    }
    
    //.......
}

发送事件时如何传递业务参数?

StateMachine中有个方法,发送事件时可以传递业务参数:

 /**
     * Send an event {@code E} wrapped with a {@link Message} to the region.
     *
     * @param event the wrapped event to send
     * @return true if event was accepted
     */
    boolean sendEvent(Message<E> event);

Message 是spring-messaging 模块中的,Spring Statemachine利用它承载事件和业务参数:

image.png

使用示例:

   @GetMapping("/order/pay")
    public String payOrder(@RequestParam String orderNo){
        OrderDO orderDO = orderMapper.selectByOrderNo(orderNo);
        try {
            StateMachine<OrderStates, Events> stateMachine = orderStateMachineBuilder.build();
            stateMachine.start();
            Message<Events> message = MessageBuilder.withPayload(Events.PAY)
                    .setHeader("orderDO", orderDO)
                    .setHeader("payChannel", "AliPay").build();
            stateMachine.sendEvent(message);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "pay success !";
    }
 
在 @WithStateMachine 注解的类中,可以接收 Message ,获取事件以及业务参数用于做业务处理逻辑:
@WithStateMachine(id = "orderMachineId")
public class StateMachineIntegration {

    @Autowired
    OrderMapper orderMapper;

    @OnTransition(target = "WAIT_PAY")
    public void create(){

    }

    @OnTransition(source = "WAIT_PAY", target= "PAID")
    @Transactional(rollbackFor = Exception.class)
    public void pay(Message<Events> message) {
        OrderDO orderDO = (OrderDO) message.getHeaders().get("orderDO");
        System.out.println("传递的orderDO:" + orderDO);
        String payChannel = (String) message.getHeaders().get("payChannel");
        System.out.println("传递的payChannel:" + payChannel);
        orderDO.setOrderStatus(OrderStates.PAID.getCode());
        orderMapper.update(orderDO);
    }
}

 

设置状态机当前状态

     上面的例子状态机都是从初始状态开始流转的,但是实际业务,有的订单在“WAIT_PAY”(待支付)状态、有的订单在“PAID”(待发货)状态,还有的订单在“WAIT_RECEIVE”(待收货)状态等等,因此需要根据订单当前状态设置状态机当前状态。

Spring Statemachine 持久化

     如果不设置状态机当前状态,可以选择持久化状态机实例。Spring Statemachine 支持把状态机实例存储到内存、NoSQL、关系型数据库中,这样在使用时把存储的状态机实例恢复即可使用。

缺点

1、存储在内存中,占用业务应用程序的内存显然不可取;

2、存储在Redis中,Redis 不一定可靠,需要考虑 Redis 宕机之类的问题;

3、存储在数据库中,这给开发工作带来很大不便,开发者应该只关心保存业务数据,为什么要保存状态机实例,而且这也会占用数据库空间。

如何设置状态机当前状态

      我们看到状态机的接口 StateMachinePersister<S,E,T>  支持根据持久化的状态机上下文 contextObj 重置状态机实例,下面的 restore 方法:

public interface StateMachinePersister<S, E, T> {

    /**
     * Persist a state machine with a given context object.
     *
     * @param stateMachine the state machine
     * @param contextObj the context ojb
     * @throws Exception the exception in case or any persist error
     */
    void persist(StateMachine<S, E> stateMachine, T contextObj) throws Exception;

    /**
     * Reset a state machine with a given context object.
     * Returned machine has been reseted and is ready to be used.
     *
     * @param stateMachine the state machine
     * @param contextObj the context ojb
     * @return the state machine
     * @throws Exception the exception in case or any persist error
     */
    StateMachine<S, E> restore(StateMachine<S, E> stateMachine, T contextObj) throws Exception;
}

我们看一下Spring StateMachine 中 AbstractStateMachinePersister 类中的实现:

@Override
public final StateMachine<S, E> restore(StateMachine<S, E> stateMachine, T contextObj) throws Exception {
		final StateMachineContext<S, E> context = stateMachinePersist.read(contextObj);
		stateMachine.stop();
		stateMachine.getStateMachineAccessor().doWithAllRegions(new StateMachineFunction<StateMachineAccess<S, E>>() {

			@Override
			public void apply(StateMachineAccess<S, E> function) {
				function.resetStateMachine(context);
			}
		});
		stateMachine.start();
		return stateMachine;
}

这里面有一个接口 StateMachineAccess<S,E>,看一下它的定义:

public interface StateMachineAccess<S, E> {


	/**
	 * Reset state machine.
	 * 
	 * @param stateMachineContext the state machine context
	 */
	void resetStateMachine(StateMachineContext<S, E> stateMachineContext);

    //......
}

它有一个方法 resetStateMachine(StateMachineContext<S,E> stateMachineContext),通过传入构造的状态机上下文 stateMachineContext,可以做到设置状态机的当前状态。于是,我们可以考虑根据订单当前状态构建一个 StateMachineContext 对象,根据 StateMachineContext 对象信息重置状态机的当前状态。

实际使用示例: 

 /**
     * 利用StateMachineAccess<S, E>接口resetStateMachine方法,重新设置状态机上下文的方法是可以设置当前状态的
     * 其实反持久化接口 orderPersister.restore 方法内部就是这样恢复状态当前上下文的
     */
    @GetMapping("/order/deliver3")
    public String deliverOrder3(@RequestParam String orderNo) throws Exception {
        OrderDO orderDO = orderMapper.selectByOrderNo(orderNo);
        StateMachine<OrderStates, Events> stateMachine = orderStateMachineBuilder.build();
        StateMachineContext<OrderStates, Events> stateMachineContext = new DefaultStateMachineContext<OrderStates, Events>(new ArrayList<>(), OrderStates.getByCode(orderDO.getOrderStatus()),
                null, null, null, null, stateMachine.getId());
        try {
            // 设置状态机实例当前状态
            stateMachine.getStateMachineAccessor().doWithAllRegions(function -> function.resetStateMachine(stateMachineContext));
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 别忘了 start 状态机,否则也不会触发状态流转
        stateMachine.start();
        System.out.println("根据订单状态设置后的状态机当前状态:" + stateMachine.getState().getId());
        Message<Events> message = MessageBuilder.withPayload(Events.DELIVER)
                .setHeader("orderDO", orderDO)
                .setHeader("expressNo", "SF12435678").build();
        stateMachine.sendEvent(message);
        OrderStates orderState = stateMachine.getState().getId();
        System.out.println("处理后订单状态:" + orderState);
        orderDO.setOrderStatus(orderState.getCode());
        orderMapper.update(orderDO);
        return "deliver success !";
    }

在上述的代码中,我们查到订单后,根据订单当前状态构建了 StateMachineContext 接口的实现类 DefaultStateMachineContext 对象,把这个对象作为入参调用StateMachineAccess<S,E> 接口的resetStateMachine(StateMachineContext<S,E> stateMachineContext)方法,状态机的当前状态就已经重置为 orderDO 中的订单状态了,这种方式就避免了状态机实例的持久化问题。

以上共同的缺点:

        在高并发系统中,上述无论哪种方式,每次请求都要先构造出一个状态机实例,如果构造状态机复杂,会影响系统的QPS,大量的状态机实例也可能会带来GC等问题。

总结:

     个人来看,Spring Statemachine 目前在生产中运用尤其是高并发场景下使用的案例不多,迭代版本也较少,社区的活跃度比较低,GitHub上 Star 数尚不及另一款状态机松鼠状态机,但是也在系统设计方面提供了一种思路。在调研各种状态机之后,我们采取了基于Cola-StateMachine改造开发的cmt-statemachine,二者都是无状态、单例的。

拓展:

借鉴Cola-StateMachine的设计,相关文章:https://blog.csdn.net/significantfrank/article/details/104996419

基于Cola-StateMachine我们开发的cmt-statemachine:https://github.com/dsc-cmt/cmt-statemachine

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值