Spring State Machine:它是什么,您需要它吗?

闻人嘉木
2023-12-01

状态机基于有限状态的计算模型 ,正如Wikipedia非常明确地说的那样。 通常,工作流会与状态一起使用,这意味着您不能仅从任何状态进入任何其他状态:应遵循一些规则。 这些状态之间的转换受规则限制。

Spring框架具有一个称为Spring State Machine的完整库 。 它是该概念的实现,旨在为已经使用Spring框架的开发人员简化状态机逻辑的开发。

让我们看看它是如何工作的。

首先,我们需要一个Spring Boot应用程序,该应用程序依赖于Spring State Machine(为简化起见,还有Lombok)。 从Spring Starter页面或从Intellij IDEA之类的IDE(也使用Spring starter模板)生成一个非常容易。

要实际使用状态机,应在应用程序类中启用它:

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

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

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

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

使用@EnableStateMachine批注时,它将在应用程序启动时自动创建默认状态机。 因此可以将其注入Application类。 默认情况下,该bean将被称为stateMachine ,但是可以给它另一个名称。 我们还需要为我们的活动和州提供课程。 让我们的简单示例基于库。 我们知道图书馆的书籍可以借用或归还,也可以损坏和修理(因此无法借用)。 因此,这正是我们放入模型中的内容。

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

然后,应使用以下事务和状态来配置状态机:

@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);
}

最后但并非最不重要的一点是,我们允许状态机自动启动(默认情况下不会启动)。

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

现在我们可以在应用程序中使用它,看看会发生什么!

@Override
    public void run(String... args) {
        boolean returnAccepted = stateMachine.sendEvent(BookEvents.RETURN);
        logger.info("return accepted: " + returnAccepted);
        boolean borrowAccepted = stateMachine.sendEvent(BookEvents.BORROW);
        logger.info("borrow accepted: " + borrowAccepted);
}

运行应用程序时,我们在日志中看到以下内容:

2018-07-07 13:46:05.096 INFO 37417 --- [ main] STATE MACHINE : return accepted: false
2018-07-07 13:46:05.098 INFO 37417 --- [ main] STATE MACHINE : borrow accepted: true

我故意先打电话给RETURN,看看它会失败。 但是,它无一例外都失败:该操作未被接受,并且计算机保持在AVAILABLE状态,这使得再次执行BORROW成为可能。 那么,如果我们交换两个呼叫会怎样?

2018-07-07 13:49:46.218 INFO 37496 --- [ main] STATE MACHINE : borrow accepted: true
2018-07-07 13:49:46.218 INFO 37496 --- [ main] STATE MACHINE : return accepted: true

这意味着可以接受正确的交互。 但是,如果我们想更清楚地了解发生了什么,该怎么办? 一种方法是为状态更改配置处理程序:

@Override
    public void configure(StateMachineStateConfigurer<BookStates, BookEvents> states) throws Exception {
        states.withStates().initial(BookStates.AVAILABLE)
                .state(BookStates.AVAILABLE, entryAction(), exitAction())
                .state(BookStates.BORROWED, entryAction(), exitAction())
                .state(BookStates.IN_REPAIR, entryAction(), exitAction());
    }

    @Bean
    public Action<BookStates, BookEvents> entryAction() {
        return ctx -> LOGGER.info("Entry action {} to get from {} to {}",
                ctx.getEvent(),
                getStateInfo(ctx.getSource()),
                getStateInfo(ctx.getTarget()));
    }

    @Bean
    public Action<BookStates, BookEvents> exitAction() {
        return ctx -> LOGGER.info("Exit action {} to get from {} to {}",
                ctx.getEvent(),
                getStateInfo(ctx.getSource()),
                getStateInfo(ctx.getTarget()));
}
2018-07-07 13:53:59.940 INFO 37579 --- [ main] STATE MACHINE : Entry action null to get from EMPTY STATE to AVAILABLE
2018-07-07 13:54:00.051 INFO 37579 --- [ main] STATE MACHINE : return accepted: false
2018-07-07 13:54:00.052 INFO 37579 --- [ main] STATE MACHINE : Exit action BORROW to get from AVAILABLE to BORROWED
2018-07-07 13:54:00.052 INFO 37579 --- [ main] STATE MACHINE : Entry action BORROW to get from AVAILABLE to BORROWED
2018-07-07 13:54:00.053 INFO 37579 --- [ main] STATE MACHINE : borrow accepted: true
2018-07-07 13:54:00.053 INFO 37579 --- [ main] STATE MACHINE : Exit action RETURN to get from BORROWED to AVAILABLE
2018-07-07 13:54:00.053 INFO 37579 --- [ main] STATE MACHINE : Entry action RETURN to get from BORROWED to AVAILABLE
2018-07-07 13:54:00.053 INFO 37579 --- [ main] STATE MACHINE : return accepted: true

另一种方法是定义一个成熟的侦听器:

public class LoggingMashineListener implements StateMachineListener<BookStates, BookEvents> {
    private static final Logger LOGGER = LoggingUtils.LOGGER;

    @Override
    public void stateChanged(State<BookStates, BookEvents> from, State<BookStates, BookEvents> to) {
        LOGGER.info("State changed from {} to {}", getStateInfo(from), getStateInfo(to));
    }

    @Override
    public void stateEntered(State<BookStates, BookEvents> state) {
        LOGGER.info("Entered state {}", getStateInfo(state));
    }

    @Override
    public void stateExited(State<BookStates, BookEvents> state) {
        LOGGER.info("Exited state {}", getStateInfo(state));
    }

    @Override
    public void eventNotAccepted(Message event) {
        LOGGER.error("Event not accepted: {}", event.getPayload());
    }

    @Override
    public void transition(Transition<BookStates, BookEvents> transition) {
        // Too much logging spoils the code =)
    }

    @Override
    public void transitionStarted(Transition<BookStates, BookEvents> transition) {
        // Too much logging spoils the code =)
    }

    @Override
    public void transitionEnded(Transition<BookStates, BookEvents> transition) {
        // Too much logging spoils the code =)
    }

    @Override
    public void stateMachineStarted(StateMachine<BookStates, BookEvents> stateMachine) {
        LOGGER.info("Machine started: {}", stateMachine);
    }

    @Override
    public void stateMachineStopped(StateMachine<BookStates, BookEvents> stateMachine) {
        LOGGER.info("Machine stopped: {}", stateMachine);
    }

    @Override
    public void stateMachineError(StateMachine<BookStates, BookEvents> stateMachine, Exception exception) {
        LOGGER.error("Machine error: {}", stateMachine);
    }

    @Override
    public void extendedStateChanged(Object key, Object value) {
        LOGGER.info("Extended state changed: [{}: {}]", key, value);
    }

    @Override
    public void stateContext(StateContext<BookStates, BookEvents> stateContext) {
        // Too much logging spoils the code =)
    }
}

并在配置监听器后将其链接到计算机。 现在我们可以删除进入和退出侦听器,并且状态配置将返回到我们的第一个修订版(请参见上文)。

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

这样,您将对正在发生的事情有更多的了解:

2018-07-07 13:59:22.714 INFO 37684 --- [ main] STATE MACHINE : Entered state AVAILABLE
2018-07-07 13:59:22.716 INFO 37684 --- [ main] STATE MACHINE : State changed from EMPTY STATE to AVAILABLE
2018-07-07 13:59:22.717 INFO 37684 --- [ main] STATE MACHINE : Machine started: IN_REPAIR AVAILABLE BORROWED / AVAILABLE / uuid=815f744e-8c5c-4ab1-88d1-b5223199bc4e / id=null
2018-07-07 13:59:22.835 ERROR 37684 --- [ main] STATE MACHINE : Event not accepted: RETURN
2018-07-07 13:59:22.836 INFO 37684 --- [ main] STATE MACHINE : return accepted: false
2018-07-07 13:59:22.837 INFO 37684 --- [ main] STATE MACHINE : Exited state AVAILABLE
2018-07-07 13:59:22.838 INFO 37684 --- [ main] STATE MACHINE : Entered state BORROWED
2018-07-07 13:59:22.838 INFO 37684 --- [ main] STATE MACHINE : State changed from AVAILABLE to BORROWED
2018-07-07 13:59:22.839 INFO 37684 --- [ main] STATE MACHINE : borrow accepted: true
2018-07-07 13:59:22.839 INFO 37684 --- [ main] STATE MACHINE : Exited state BORROWED
2018-07-07 13:59:22.839 INFO 37684 --- [ main] STATE MACHINE : Entered state AVAILABLE
2018-07-07 13:59:22.839 INFO 37684 --- [ main] STATE MACHINE : State changed from BORROWED to AVAILABLE
2018-07-07 13:59:22.839 INFO 37684 --- [ main] STATE MACHINE : return accepted: true

什么时候需要状态机? Spring文档指出,如果满足以下条件,您已经在尝试实现状态机

  • 使用布尔标志或枚举对情况进行建模。
  • 具有仅对应用程序生命周期的一部分有意义的变量。
  • 遍历if / else结构并检查是否设置了特定的标志或枚举,然后进一步对当标志和枚举的某些组合存在或不存在时的处理方式作进一步的例外。

我可以想到一些示例:

  • 机器人 对于状态机来说,这通常是一个很好的例子,因为机器人通常只有几个状态,并且它们之间有不同的动作。 例如,您有一个机器人在问问题以预订酒店(一个著名的例子)。 您会问几个问题:位置,客人人数,价格范围等。每个问题都是一个州。 每个答案都是一个事件,可以转换为下一个状态。
  • 物联网 最简单的状态机具有两种状态:ON和OFF。 但是使用比电灯开关更复杂的设备,可能会在它们之间存在更多状态,并且会有更多事件进行状态转换。

Spring State Machine可以做的事情还很多。 例如, 状态可以嵌套 。 此外,还有可以配置为检查是否允许过渡防护措施 ,以及允许定义选择状态,接合状态等 状态。 事件可以由操作或在计时器上触发 。 状态机可以持久化以提高性能。 要浏览所有内容,您需要研究Spring State Machine文档并确定适合您的特定情况的文档。 在这里,我们仅轻轻地刮擦了表面。

您可以观看有关Spring State Machine的视频 ,或研究完整的规范以了解有关该主题的更多信息。

可以在这里找到本文的项目源。

翻译自: https://www.javacodegeeks.com/2018/07/spring-state-machine.html

 类似资料: