SpringStateMachine状态机⼊门(项⽬实战)
背景介绍
最近在公司做⼀个Spring Boot表单项⽬,表单涉及的状态如图所⽰:
在设计表单状态转换模块时,想到状态机这个概念。在⽹上检索相关的实现框架,发现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.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;
spring教学视频
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 =ById(paperId);
StateMachine<PaperStates, PaperEvents> stateMachine = StateMachine(paperId);
//创建状态机后⾸先要停⽌状态机,将状态机状态设置为表单记录的状态(起始状态)
stateMachine.stop();
.doWithAllRegions(sma ->{
//添加⼀个,在状态机状态发⽣改变的时候,将对应的状态持久化到数据库
sma.addStateMachineInterceptor(new StateMachineInterceptorAdapter<PaperStates, PaperEvents>(){
@Override
public void preStateChange(State<PaperStates, PaperEvents> state, Message<PaperEvents> message, Transition<PaperStates, PaperEve nts> transition, StateMachine<PaperStates, PaperEvents> stateMachine1){
Optional.ofNullable(message).ifPresent(msg->{
Optional.ofNullable((String) Headers().getOrDefault(PAPER_ID_HEADER,"")).ifPresent(paperId1->{
Paper paper1 =getById(paperId);
paper1.Id());
saveOrUpdate(paper1);
});
});
}
});
//设置好状态机起始状态
});
//启动状态机
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] service.impl.PaperServiceImpl : 状态
机初始状态:ObjectState [getIds()=[DRAFT], getClas s()=class org.springframework.statemachine.state.ObjectState, hashCode()=667591046, toString()=AbstractState [id=DRAFT, pseudoState=org.springfram ework.statemachine.state.DefaultPseudoState@6ebf9c2d, deferred=[], entryActions=[], exitActions=[], stateActions=[], regions=[], submachine=null]]
2021-09-01 09:00:10.757 INFO 19040 --- [ main] service.impl.PaperServiceImpl : 状态机发布后状态:ObjectState [getIds()=[OPENING], get Class()=class org.springframework.statemachine.state.ObjectState, hashCode()=892589968, toString()=AbstractState [id=OPENING, pseudoState=null, de ferred=[], entryActions=[], exitActions=[], stateActions=[], regions=[], submachine=null]]
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论