最近在公司做一个Spring Boot表单项目,表单涉及的状态如图所示:
在设计表单状态转换模块时,想到状态机这个概念。在网上检索相关的实现框架,发现Spring StateMachine框架。网上大多数的教程都是非常简单的Demo,只有一个状态机连续切换的示例,很难作为一个实战入门的Demo。幸运的是在网上找到了一个Spring系列的视频,其中涉及到了状态机的实战项目。这篇文章也是基于该视频教程,结合自己的项目实践做一个完整的状态机实战笔记。
Spring StatsMachine主要涉及到两个重要的概念,一个是State(状态)、一个是Event(事件)。
在我的表单项目中,我的状态有:草稿、收集、统计、领取、关闭。
状态一般通过枚举类型进行定义,代码如下:
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;
}
}
状态之间通过事件完成切换。
事件的定义一般也是通过枚举类型,代码如下:
public enum PaperEvents {
PUBLISH,//从草稿变成发布状态
EXPIRE,//从发布变成统计状态
CLAIM,//从统计状态变成可领取状态
CLOSE,//从发布状态变成下架状态,从可领取状态变成下架状态,从下架状态变成草稿状态
REOPEN;//重新发布
}
定义好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。以我表单项目为例,在我的状态变换中,我还需要涉及到持久层的操作:数据库中每个表单状态都不一样,我还需要针对每个表单记录设置状态机的初始状态,以及状态机改变状态以后,如何将该变化持久化到我的数据库中。
在我的项目中,状态对应的是表单实体,代码如下。其中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;
}
假设现在系统中已经有了一个表单草稿,我想将该表单状态改为收集状态。结合状态机的思想,我应该先获取该表单对应的状态机,设置好状态机起始状态,然后给它发送相应的事件(发布)。
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]]