Spring StateMachine 状态机入门(项目实战)

背景介绍

最近在公司做一个Spring Boot表单项目,表单涉及的状态如图所示:

Spring StateMachine 状态机入门(项目实战)_第1张图片

在设计表单状态转换模块时,想到状态机这个概念。在网上检索相关的实现框架,发现Spring StateMachine框架。网上大多数的教程都是非常简单的Demo,只有一个状态机连续切换的示例,很难作为一个实战入门的Demo。幸运的是在网上找到了一个Spring系列的视频,其中涉及到了状态机的实战项目。这篇文章也是基于该视频教程,结合自己的项目实践做一个完整的状态机实战笔记。

StateMachine实战

Spring StatsMachine主要涉及到两个重要的概念,一个是State(状态)、一个是Event(事件)。

State

在我的表单项目中,我的状态有:草稿、收集、统计、领取、关闭。

状态一般通过枚举类型进行定义,代码如下:

public enum PaperStates {
    DRAFT(0,"草稿"),
    OPENING(1,"收集"),
    ACCOUNT(2,"统计"),
    CLAIMING(3,"领取"),
    CLOSED(-1,"关闭");
    private int code;
    private String desc;

    PaperStates(int code, String desc){
        this.code = code;
        this.desc = desc;
    }
}

Event

状态之间通过事件完成切换。

事件的定义一般也是通过枚举类型,代码如下:

public enum PaperEvents {
    PUBLISH,//从草稿变成发布状态
    EXPIRE,//从发布变成统计状态
    CLAIM,//从统计状态变成可领取状态
    CLOSE,//从发布状态变成下架状态,从可领取状态变成下架状态,从下架状态变成草稿状态
    REOPEN;//重新发布
}

StateMachineConfig

定义好State和Event以后,还需要配置状态机,设置状态之间的流转关系。代码如下:

public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<PaperStates, PaperEvents> {

    @Override
    public void configure(StateMachineStateConfigurer<PaperStates, PaperEvents> states) throws Exception {
        states.withStates().initial(PaperStates.DRAFT).states(EnumSet.allOf(PaperStates.class));
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<PaperStates, PaperEvents> transitions) throws Exception {
        transitions.withExternal()
                //草稿状态到发布状态
                .source(PaperStates.DRAFT).target(PaperStates.OPENING)
                .event(PaperEvents.PUBLISH)
                .and()
                .withExternal()
                //发布状态到统计状态
                .source(PaperStates.OPENING).target(PaperStates.ACCOUNT)
                .event(PaperEvents.EXPIRE)
                .and()
                .withExternal()
                //统计状态到领取状态
                .source(PaperStates.ACCOUNT).target(PaperStates.CLAIMING)
                .event(PaperEvents.CLAIM)
                .and()
                .withExternal()
                //领取状态到下架状态
                .source(PaperStates.CLAIMING).target(PaperStates.CLOSED)
                .event(PaperEvents.CLOSE)
                .and()
                .withExternal()
                //发布状态到下架状态
                .source(PaperStates.OPENING).target(PaperStates.CLOSED)
                .event(PaperEvents.CLOSE)
                .and()
                .withExternal()
                //统计状态到下架状态
                .source(PaperStates.ACCOUNT).target(PaperStates.CLOSED)
                .event(PaperEvents.CLOSE)
                .and()
                .withExternal()
                //统计状态到发布状态
                .source(PaperStates.ACCOUNT).target(PaperStates.OPENING)
                .event(PaperEvents.REOPEN);
    }
}

完成这三步骤的设置以后,网上大多数的教程都是在测试方法里面写个简单的函数,测试下状态机的执行过程。但是像我这样初级读者很难在项目中借鉴这样的Demo。以我表单项目为例,在我的状态变换中,我还需要涉及到持久层的操作:数据库中每个表单状态都不一样,我还需要针对每个表单记录设置状态机的初始状态,以及状态机改变状态以后,如何将该变化持久化到我的数据库中。

Entity设计

在我的项目中,状态对应的是表单实体,代码如下。其中status就对应着我的状态机中的各种状态

public class Paper implements Serializable {
    @TableId(value = "id")
    private String paperId;

    private String title;

    private String image;

    private Date createDate;

    private int status;

    private int type;

    private Date beginTime;

    private Date endTime;

    private String detail;

    private List<Item> items;

    private String config;
}

Service设计

假设现在系统中已经有了一个表单草稿,我想将该表单状态改为收集状态。结合状态机的思想,我应该先获取该表单对应的状态机,设置好状态机起始状态,然后给它发送相应的事件(发布)。

public class PaperServiceImpl extends ServiceImpl<PaperMapper, Paper> implements IPaperService {
    @Autowired
    private StateMachineFactory<PaperStates,PaperEvents> stateMachineFactory;

    private static final String PAPER_ID_HEADER = "paperId";
    //根据表单ID,构建表单对应的状态机
    public StateMachine<PaperStates, PaperEvents> buildStateMachine(String paperId){
        Paper paper = this.getById(paperId);
        StateMachine<PaperStates, PaperEvents> stateMachine = stateMachineFactory.getStateMachine(paperId);
        //创建状态机后首先要停止状态机,将状态机状态设置为表单记录的状态(起始状态)
        stateMachine.stop();
        stateMachine.getStateMachineAccessor()
                .doWithAllRegions(sma -> {
                    //添加一个拦截器,在状态机状态发生改变的时候,将对应的状态持久化到数据库
                    sma.addStateMachineInterceptor(new StateMachineInterceptorAdapter<PaperStates, PaperEvents>() {
                        @Override
                        public void preStateChange(State<PaperStates, PaperEvents> state, Message<PaperEvents> message, Transition<PaperStates, PaperEvents> transition, StateMachine<PaperStates, PaperEvents> stateMachine1) {
                            Optional.ofNullable(message).ifPresent(msg->{
                               Optional.ofNullable((String) msg.getHeaders().getOrDefault(PAPER_ID_HEADER,"")).ifPresent(paperId1->{
                                   Paper paper1 = getById(paperId);
                                   paper1.setPaperStatus(state.getId());
                                   saveOrUpdate(paper1);
                               });
                            });
                        }
                    });
                    //设置好状态机起始状态
                    sma.resetStateMachine(new DefaultStateMachineContext<>(PaperStates.getPaperState(paper.getStatus()),null,null,null));
                });
        //启动状态机
        stateMachine.start();
        return stateMachine;
    }
	//封装一个通用的方法,该方法会根据表单ID创建状态机,然后将指定的事件发送给状态机
    public void changeState(String paperId,PaperEvents  events){
        StateMachine<PaperStates, PaperEvents> sm = this.buildStateMachine(paperId);
        log.info("状态机初始状态:"+sm.getState());
        Message<PaperEvents> message = MessageBuilder.withPayload(events)
                .setHeader(PAPER_ID_HEADER, paperId)
                .build();
        sm.sendEvent(message);
        log.info("状态机发布后状态:"+sm.getState());
    }
}

测试结果

从日志结果可以看到,状态机随着事件的发送会改变状态,同时该状态值也会持久化到数据库当中。

2021-09-01 09:00:10.687  INFO 19040 --- [           main] u.z.c.service.impl.PaperServiceImpl      : 状态机初始状态:ObjectState [getIds()=[DRAFT], getClass()=class org.springframework.statemachine.state.ObjectState, hashCode()=667591046, toString()=AbstractState [id=DRAFT, pseudoState=org.springframework.statemachine.state.DefaultPseudoState@6ebf9c2d, deferred=[], entryActions=[], exitActions=[], stateActions=[], regions=[], submachine=null]]

2021-09-01 09:00:10.757  INFO 19040 --- [           main] u.z.c.service.impl.PaperServiceImpl      : 状态机发布后状态:ObjectState [getIds()=[OPENING], getClass()=class org.springframework.statemachine.state.ObjectState, hashCode()=892589968, toString()=AbstractState [id=OPENING, pseudoState=null, deferred=[], entryActions=[], exitActions=[], stateActions=[], regions=[], submachine=null]]

你可能感兴趣的:(Spring,spring)