了解Spring状态机器

banq 18-07-16
         

状态机是基于有限状态的计算模型,通常工作流程与状态一起使用,这意味着应该遵循一定流程规则才能从任何状态切换到任何其他状态。这些状态之间的转换是受到规则的限制。

Spring框架有一个名为Spring State Machine的库。它是该概念的一个实现,目的是简化有关状态机的业务逻辑的开发。

首先,我们建立一个Spring Boot应用程序,使用元注解激活Spring State Machine(使用Lombok简化setter/getter)。从Spring Starter页面或Intellij IDEA中生成一个新项目:


@SpringBootApplication
@EnableStateMachine
public class StatemachineApplication {

public static void main(String[] args) {
SpringApplication.run(StatemachineApplication.class, args);
}
}



@EnableStateMachine注释会在应用程序启动时自动创建默认状态机。默认情况下,bean将被称为 stateMachine,但可以为其指定另一个名称。

现在我们需要准备切换状态的事件,因为只有发生事件,才会有状态变化,我们以图书馆的简单示例为基础。我们知道图书馆的书籍可以借用或归还,也可能是损坏和修复(因此无法借用)。下面是我们的模型的内容。


public enum BookStates {
AVAILABLE,
BORROWED,
IN_REPAIR
}
public enum BookEvents {
BORROW,
RETURN,
START_REPAIR,
END_REPAIR
}


然后准备一个配置类配置一下这个状态机,下面MachineConfiguration是继承EnumStateMachineConfigurerAdapter,注意两个注解:


@Configuration
@EnableStateMachine
public class MachineConfiguration extends EnumStateMachineConfigurerAdapter<BookStates, BookEvents> {

@Override
public void configure(StateMachineStateConfigurer<BookStates, BookEvents> states) throws Exception {
states.withStates()
.initial(BookStates.AVAILABLE)
.states(EnumSet.allOf(BookStates.class));
}

@Override
public void configure(StateMachineTransitionConfigurer<BookStates, BookEvents> transitions) throws Exception {
transitions
.withExternal()
.source(BookStates.AVAILABLE)
.target(BookStates.BORROWED)
.event(BookEvents.BORROW)
.and()
.withExternal()
.source(BookStates.BORROWED)
.target(BookStates.AVAILABLE)
.event(BookEvents.RETURN)
.and()
.withExternal()
.source(BookStates.AVAILABLE)
.target(BookStates.IN_REPAIR)
.event(BookEvents.START_REPAIR)
.and()
.withExternal()
.source(BookStates.IN_REPAIR)
.target(BookStates.AVAILABLE)
.event(BookEvents.END_REPAIR);
}



最后但并非不重要的是,我们需要让状态机自动启动(默认情况下不是),在上面MachineConfiguration中增加下面方法代码:


@Override
public void configure(StateMachineConfigurationConfigurer<BookStates, BookEvents> config) throws Exception {
config.withConfiguration()
.autoStartup(true);
}


现在我们可以在应用程序中使用它,看看会发生什么!在前面Spring启动类中调用这个状态机:


@SpringBootApplication
@EnableStateMachine
public class StatemachineApplication implements CommandLineRunner {
private final StateMachine<BookStates, BookEvents> stateMachine;

public static void main(String[] args) {
SpringApplication.run(StatemachineApplication.class, args);
}

@Autowired
public StatemachineApplication(StateMachine<BookStates, BookEvents> stateMachine) {
this.stateMachine = stateMachine;
}

@Override
public void run(String... args) {
stateMachine.start();
stateMachine.sendEvent(BookEvents.RETURN);
stateMachine.sendEvent(BookEvents.BORROW);
stateMachine.stop();
}
}


上面代码run方法中,我们首先发送归还图书事件,然后再借书,运行该应用程序时,我们在日志中看到以下内容,首先是出现ERROR错误,归还事件不被接受,为什么呢?再看看借书事件却被接受了,状态从可用状态切换到被借状态。

ERROR 16672 --- [ main] STATE MACHINE : Event not accepted: RETURN
INFO 16672 --- [ main] STATE MACHINE : Exited state AVAILABLE
INFO 16672 --- [ main] STATE MACHINE : Entered state BORROWED
INFO 16672 --- [ main] STATE MACHINE : State changed from AVAILABLE to BORROWED


原来在前面configure方法中,我们初始化这个状态机初始状态就是可用:


states.withStates()
.initial(BookStates.AVAILABLE)
.states(EnumSet.allOf(BookStates.class));


什么时候需要状态机?Spring文档声明在以下情况下k可以尝试状态机:

1.使用布尔标志或枚举来建模的场景。
2.某个变量的改变对整个应用程序影响很大,比如全局变量等。
3.需要通过if / else进行循环的结构,在这个循环中检查某个标识开关是否设置了,根据不同开关做不同的事情等等。

其实,DDD领域驱动设计中领域模型有各种状态需要管理,在这种情况下用模型类封装状态机,接受外界不同事件进行切换,甚至可以结合事件溯源EventSourcing进行方便状态管理。

github源码

英文原文