当前位置 : 主页 > 编程语言 > java >

Spring Statemachine状态机的概念(二)

来源:互联网 收集:自由互联 发布时间:2023-02-04
使用作用域 状态机对作用域的支持非常有限,但您可以 通过以下两种方式之一使用普通的 Spring 注释来启用范围:​​session​​​​@Scope​​ 如果状态机是使用构建器手动构建的,并

Spring Statemachine状态机的概念(二)_状态机

使用作用域

状态机对作用域的支持非常有限,但您可以 通过以下两种方式之一使用普通的 Spring 注释来启用范围:​​session​​​​@Scope​​

  • 如果状态机是使用构建器手动构建的,并返回到 上下文作为 .@Bean
  • 通过配置适配器。

两者 这些需要存在,设置为 和 设置为 。以下示例 显示两个用例:​​@Scope​​​​scopeName​​​​session​​​​proxyMode​​​​ScopedProxyMode.TARGET_CLASS​​

@Configurationpublic class Config3 { @Bean @Scope(scopeName="session", proxyMode=ScopedProxyMode.TARGET_CLASS) StateMachine<String, String> stateMachine() throws Exception { Builder<String, String> builder = StateMachineBuilder.builder(); builder.configureConfiguration() .withConfiguration() .autoStartup(true); builder.configureStates() .withStates() .initial("S1") .state("S2"); builder.configureTransitions() .withExternal() .source("S1") .target("S2") .event("E1"); StateMachine<String, String> stateMachine = builder.build(); return stateMachine; }}@Configuration@EnableStateMachine@Scope(scopeName="session", proxyMode=ScopedProxyMode.TARGET_CLASS)public static class Config4 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception { config .withConfiguration() .autoStartup(true); } @Override public void configure(StateMachineStateConfigurer<String, String> states) throws Exception { states .withStates() .initial("S1") .state("S2"); } @Override public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception { transitions .withExternal() .source("S1") .target("S2") .event("E1"); }}

提示:请参阅范围​​,​​了解如何使用会话范围。

将状态机的作用域限定为 后,自动将其连接到 a 为每个会话提供一个新的状态机实例。 然后,每个状态机在失效时被销毁。 以下示例演示如何在控制器中使用状态机:​​session​​​​@Controller​​​​HttpSession​​

@Controllerpublic class StateMachineController { @Autowired StateMachine<String, String> stateMachine; @RequestMapping(path="/state", method=RequestMethod.POST) public HttpEntity<Void> setState(@RequestParam("event") String event) { stateMachine .sendEvent(Mono.just(MessageBuilder .withPayload(event).build())) .subscribe(); return new ResponseEntity<Void>(HttpStatus.ACCEPTED); } @RequestMapping(path="/state", method=RequestMethod.GET) @ResponseBody public String getState() { return stateMachine.getState().getId(); }}

在作用域中使用状态机需要仔细规划, 主要是因为它是一个相对较重的组件。​​session​​

Spring Statemachine poms不依赖于Spring MVC 类,您将需要使用会话作用域。但是,如果你是 使用 Web 应用程序,您已经拉取了这些依赖项 直接来自Spring MVC或Spring Boot。

使用操作

操作是可用于的最有用的组件之一 与状态机交互和协作。您可以运行操作 在状态机及其状态生命周期的不同位置,例如, 进入或退出状态或在转换期间。 以下示例演示如何在状态机中使用操作:

@Overridepublic void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.SI) .state(States.S1, action1(), action2()) .state(States.S2, action1(), action2()) .state(States.S3, action1(), action3());}

在前面的示例中,和 bean 分别附加到 和 状态。以下示例定义了这些操作(和):​​action1​​​​action2​​​​entry​​​​exit​​​​action3​​

@Beanpublic Action<States, Events> action1() { return new Action<States, Events>() { @Override public void execute(StateContext<States, Events> context) { } };}@Beanpublic BaseAction action2() { return new BaseAction();}@Beanpublic SpelAction action3() { ExpressionParser parser = new SpelExpressionParser(); return new SpelAction( parser.parseExpression( "stateMachine.sendEvent(T(org.springframework.statemachine.docs.Events).E1)"));}public class BaseAction implements Action<States, Events> { @Override public void execute(StateContext<States, Events> context) { }}public class SpelAction extends SpelExpressionAction<States, Events> { public SpelAction(Expression expression) { super(expression); }}

您可以直接实现为匿名函数或创建 您自己的实现,并将适当的实现定义为 豆。​​Action​​

在前面的示例中,使用 SpEL 表达式将事件发送到 状态机。​​action3​​​​Events.E1​​

​​StateContext​​​中进行了描述。

带有操作的 SpEL 表达式

您还可以使用 SpEL 表达式作为 全面实施。​​Action​​

反应性操作

普通接口是一种简单的函数方法,取回 void。在你阻止之前,这里没有任何阻塞 在方法本身中,这有点问题,因为框架不能 知道里面到底发生了什么。​​Action​​​​StateContext​​

public interface Action<S, E> { void execute(StateContext<S, E> context);}

为了克服这个问题,我们在内部将处理更改为 处理普通Java的获取和返回。通过这种方式,我们可以调用操作并完全以响应方式 仅在订阅时以非阻塞方式执行操作 以等待完成。​​Action​​​​Function​​​​StateContext​​​​Mono​​

public interface ReactiveAction<S, E> extends Function<StateContext<S, E>, Mono<Void>> {}

内部旧接口包裹着一个可运行的反应堆单声道,因为它 共享相同的返回类型。我们无法控制您用这种方法做什么!​​Action​​

使用防护装置

如要记住的事情中所示,和 bean 附加到条目和 分别是退出状态。 以下示例还对事件使用防护:​​guard1​​​​guard2​​

@Overridepublic void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.SI).target(States.S1) .event(Events.E1) .guard(guard1()) .and() .withExternal() .source(States.S1).target(States.S2) .event(Events.E1) .guard(guard2()) .and() .withExternal() .source(States.S2).target(States.S3) .event(Events.E2) .guardExpression("extendedState.variables.get('myvar')");}

您可以直接实现为匿名函数或创建 您自己的实现,并将适当的实现定义为 豆。在前面的示例中,检查 S 是否扩展 名为 的状态变量的计算结果为 。 下面的示例实现一些示例防护:​​Guard​​​​guardExpression​​​​myvar​​​​TRUE​​

@Beanpublic Guard<States, Events> guard1() { return new Guard<States, Events>() { @Override public boolean evaluate(StateContext<States, Events> context) { return true; } };}@Beanpublic BaseGuard guard2() { return new BaseGuard();}public class BaseGuard implements Guard<States, Events> { @Override public boolean evaluate(StateContext<States, Events> context) { return false; }}

​​StateContext​​​在“使用状态上下文”一节中进行了描述。

带防护装置的 SpEL 表达式

您还可以使用 SpEL 表达式作为 完整的防护实施。唯一的要求是表达式需要 返回一个值以满足实现。这可以是 使用一个函数进行演示,该函数采用 表达式作为参数。​​Boolean​​​​Guard​​​​guardExpression()​​

反应防护装置

普通接口是一种简单的函数方法,取值并返回布尔值。在你阻止之前,这里没有任何阻塞 在方法本身中,这有点问题,因为框架不能 知道里面到底发生了什么。​​Guard​​​​StateContext​​

public interface Guard<S, E> { boolean evaluate(StateContext<S, E> context);}

为了克服这个问题,我们在内部将处理更改为 处理普通Java的获取和返回。这样,我们可以完全以被动的方式呼叫警卫 仅在订阅时以非阻塞方式对其进行评估 以使用返回值等待完成。​​Guard​​​​Function​​​​StateContext​​​​Mono<Boolean>​​

public interface ReactiveGuard<S, E> extends Function<StateContext<S, E>, Mono<Boolean>> {}

内部旧接口包装有反应器单声道函数。我们没有 控制你在这种方法中做什么!​​Guard​​

使用扩展状态

假设您需要创建一个状态机来跟踪 很多时候,用户按下键盘上的键,然后终止 当按键被按下 1000 次时。一个可能但非常幼稚的解决方案 将是为每 1000 次按键创建一个新状态。 你可能会突然得到一个天文数字 状态,这自然不是很实用。

这就是扩展状态变量不需要的地方 以添加更多状态以驱动状态机更改。相反 您可以在转换期间执行简单的变量更改。

​​StateMachine​​有一个名为 的方法。它返回一个 名为 的接口,用于访问扩展状态 变量。您可以直接通过状态机访问这些变量,也可以在操作或转换的回调期间访问这些变量。 以下示例演示如何执行此操作:​​getExtendedState()​​​​ExtendedState​​​​StateContext​​

public Action<String, String> myVariableAction() { return new Action<String, String>() { @Override public void execute(StateContext<String, String> context) { context.getExtendedState() .getVariables().put("mykey", "myvalue"); } };}

如果您需要收到扩展状态变量的通知 更改,您有两种选择:使用或 侦听回调。以下示例 使用方法:​​StateMachineListener​​​​extendedStateChanged(key, value)​​​​extendedStateChanged​​

public class ExtendedStateVariableListener extends StateMachineListenerAdapter<String, String> { @Override public void extendedStateChanged(Object key, Object value) { // do something with changed variable }}

或者,您可以为 .如侦听状态机事件中所述, 您还可以收听所有事件。 以下示例用于侦听状态更改:​​OnExtendedStateChanged​​​​StateMachineEvent​​​​onApplicationEvent​​

public class ExtendedStateVariableEventListener implements ApplicationListener<OnExtendedStateChanged> { @Override public void onApplicationEvent(OnExtendedStateChanged event) { // do something with changed variable }}

用​​StateContext​​

StateContext是最重要的对象之一 使用状态机时,因为它被传递到各种方法 和回调,以给出状态机的当前状态和 它可能要去的地方。你可以把它想象成一个 当前状态机阶段的快照,当 是何时被收回。​​StateContext​​

在Spring Statemachine 1.0.x中,使用相对幼稚 就如何使用它作为简单的“POJO”传递东西而言。 从 Spring 状态机 1.1.x 开始,它的作用已经很大 通过使其成为状态机中的一等公民进行改进。​​StateContext​​

您可以使用 来访问以下内容:​​StateContext​​

  • 当前或(或其 ,如果已知)。MessageEventMessageHeaders
  • 状态机的 .Extended State
  • 本身。StateMachine
  • 到可能的状态机错误。
  • 到当前,如果适用。Transition
  • 状态机的源状态。
  • 状态机的目标状态。
  • 电流 ,如阶段中所述。Stage

​​StateContext​​传递到各种组件中,例如 和 。​​Action​​​​Guard​​

阶段

舞台是 on 的表示 状态机当前正在与用户交互的内容。当前可用的 阶段为 、、、、、 和 。这些状态可能看起来很熟悉,因为 它们与您与侦听器交互的方式相匹配(如侦听状态机事件中所述)。​​stage​​​​EVENT_NOT_ACCEPTED​​​​EXTENDED_STATE_CHANGED​​​​STATE_CHANGED​​​​STATE_ENTRY​​​​STATE_EXIT​​​​STATEMACHINE_ERROR​​​​STATEMACHINE_START​​​​STATEMACHINE_STOP​​​​TRANSITION​​​​TRANSITION_START​​​​TRANSITION_END​​

触发转换

驱动状态机是通过使用转换来完成的,这些转换被触发 通过触发器。当前支持的触发器是 和 。​​EventTrigger​​​​TimerTrigger​​

用​​EventTrigger​​

​​EventTrigger​​是最有用的触发器,因为它可以让您 通过向状态机发送事件直接与状态机交互。这些 事件也称为信号。您可以向过渡添加触发器 通过在配置期间将状态与其关联。 以下示例演示如何执行此操作:

@AutowiredStateMachine<String, String> stateMachine;void signalMachine() { stateMachine .sendEvent(Mono.just(MessageBuilder .withPayload("E1").build())) .subscribe(); Message<String> message = MessageBuilder .withPayload("E2") .setHeader("foo", "bar") .build(); stateMachine.sendEvent(Mono.just(message)).subscribe();}

无论您发送一个事件还是多个事件,结果始终是一个序列 的结果。之所以如此,是因为在存在多个请求的情况下,结果将 从这些区域中的多台计算机返回。这显示 使用方法,给出结果列表。方法 本身只是一个语法糖收集列表。如果有 只有一个区域,此列表包含一个结果。​​sendEventCollect​​​​Flux​​

Message<String> message1 = MessageBuilder .withPayload("E1") .build();Mono<List<StateMachineEventResult<String, String>>> results = stateMachine.sendEventCollect(Mono.just(message1));results.subscribe();

在订阅返回的通量之前,不会发生任何反应。从StateMachineEventResult查看更多相关信息。

前面的示例通过构造包装来发送事件 a 并订阅返回的结果。 让 我们向事件添加任意额外信息,然后可见 到(例如)何时实施操作。​​Mono​​​​Message​​​​Flux​​​​Message​​​​StateContext​​

消息标头通常会传递,直到计算机运行到 特定事件的完成。例如,如果事件导致 转换为具有匿名转换的状态 状态 ,原始事件可用于状态 中的操作或守卫。​​A​​​​B​​​​B​​

也可以发送消息,而不是仅发送 一个带有 .​​Flux​​​​Mono​​

Message<String> message1 = MessageBuilder .withPayload("E1") .build();Message<String> message2 = MessageBuilder .withPayload("E2") .build();Flux<StateMachineEventResult<String, String>> results = stateMachine.sendEvents(Flux.just(message1, message2));results.subscribe();

状态机事件结果

​​StateMachineEventResult​​包含有关结果的更多详细信息 的事件发送。从中您可以得到一个处理事件,它本身以及什么是实际的.来自您 可以查看邮件是被接受、拒绝还是延迟。一般来说,当 订阅完成,事件将传递到计算机中。​​Region​​​​Message​​​​ResultType​​​​ResultType​​

用​​TimerTrigger​​

​​TimerTrigger​​在需要触发某些内容时很有用 自动,无需任何用户交互。 被添加到 通过在配置期间将计时器与其关联来进行转换。​​Trigger​​

目前,有两种类型的支持计时器,一种是触发 持续,并在进入源状态后触发。 以下示例演示如何使用触发器:

@Configuration@EnableStateMachinepublic class Config2 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineStateConfigurer<String, String> states) throws Exception { states .withStates() .initial("S1") .state("S2") .state("S3"); } @Override public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception { transitions .withExternal() .source("S1").target("S2").event("E1") .and() .withExternal() .source("S1").target("S3").event("E2") .and() .withInternal() .source("S2") .action(timerAction()) .timer(1000) .and() .withInternal() .source("S3") .action(timerAction()) .timerOnce(1000); } @Bean public TimerAction timerAction() { return new TimerAction(); }}public class TimerAction implements Action<String, String> { @Override public void execute(StateContext<String, String> context) { // do something in every 1 sec }}

前面的示例有三种状态:、 和 。我们有一个正常的 从 to 和 from 到 with 的外部过渡 事件和 ,分别。有趣的部分 对于使用 是 当我们定义时 源状态和 的内部转换。​​S1​​​​S2​​​​S3​​​​S1​​​​S2​​​​S1​​​​S3​​​​E1​​​​E2​​​​TimerTrigger​​​​S2​​​​S3​​

对于这两个转换,我们调用 bean (),其中 源状态使用和使用 。 给出的值以毫秒为单位(在两种情况下都是毫秒或一秒)。​​Action​​​​timerAction​​​​S2​​​​timer​​​​S3​​​​timerOnce​​​​1000​​

一旦状态机收到事件,它就会进行转换 从 到 ,计时器启动。当状态为 时,将运行并导致与之关联的转换 状态 — 在本例中为已定义的内部转换。​​E1​​​​S1​​​​S2​​​​S2​​​​TimerTrigger​​​​timerAction​​

一旦状态机收到 ,事件就会执行转换 从 到 ,计时器启动。此计时器仅执行一次 进入状态后(在计时器中定义的延迟之后)。​​E2​​​​S1​​​​S3​​

在幕后,计时器是简单的触发器,可能会导致 过渡发生。使用保留定义过渡 仅当源状态处于活动状态时,触发才会触发并导致转换。 过渡有点不同,因为它 仅在实际进入源状态的延迟后触发。​​timer()​​​​timerOnce()​​

如果您希望延迟后发生某些事情,请使用 正好在进入状态时一次。​​timerOnce()​​

侦听状态机事件

在某些用例中,您想知道发生了什么 状态机,对某事做出反应,或获取日志记录详细信息 调试目的。Spring 状态机提供了用于添加侦听器的接口。这些侦听器 然后给出一个选项,以便在各种状态更改时获取回调, 动作,等等。

你基本上有两个选择:听Spring应用程序 上下文事件或直接将侦听器附加到状态机。两者 这些基本上提供相同的信息。一个生产 事件作为事件类,另一个通过侦听器产生回调 接口。这两者都有优点和缺点,我们将在后面讨论。

应用程序上下文事件

应用程序上下文事件类包括 、、 ,以及扩展基事件类 的其他类。这些可以按原样使用 弹簧 .​​OnTransitionStartEvent​​​​OnTransitionEvent​​​​OnTransitionEndEvent​​​​OnStateExitEvent​​​​OnStateEntryEvent​​​​OnStateChangedEvent​​​​OnStateMachineStart​​​​OnStateMachineStop​​​​StateMachineEvent​​​​ApplicationListener​​

​​StateMachine​​通过 发送上下文事件。 如果类用 注释,则会自动创建默认实现。 下面的示例从类中定义的 Bean 中获取 a:​​StateMachineEventPublisher​​​​@Configuration​​​​@EnableStateMachine​​​​StateMachineApplicationEventListener​​​​@Configuration​​

public class StateMachineApplicationEventListener implements ApplicationListener<StateMachineEvent> { @Override public void onApplicationEvent(StateMachineEvent event) { }}@Configurationpublic class ListenerConfig { @Bean public StateMachineApplicationEventListener contextListener() { return new StateMachineApplicationEventListener(); }}

还可以通过使用 自动启用上下文事件 , 用于构建机器并注册为豆类, 如以下示例所示:​​@EnableStateMachine​​​​StateMachine​​

@Configuration@EnableStateMachinepublic class ManualBuilderConfig { @Bean public StateMachine<String, String> stateMachine() throws Exception { Builder<String, String> builder = StateMachineBuilder.builder(); builder.configureStates() .withStates() .initial("S1") .state("S2"); builder.configureTransitions() .withExternal() .source("S1") .target("S2") .event("E1"); return builder.build(); }}

用​​StateMachineListener​​

通过使用 ,您可以扩展它和 实现所有回调方法或使用包含存根方法实现的类并选择哪些实现 以覆盖。 以下示例使用后一种方法:​​StateMachineListener​​​​StateMachineListenerAdapter​​

public class StateMachineEventListener extends StateMachineListenerAdapter<States, Events> { @Override public void stateChanged(State<States, Events> from, State<States, Events> to) { } @Override public void stateEntered(State<States, Events> state) { } @Override public void stateExited(State<States, Events> state) { } @Override public void transition(Transition<States, Events> transition) { } @Override public void transitionStarted(Transition<States, Events> transition) { } @Override public void transitionEnded(Transition<States, Events> transition) { } @Override public void stateMachineStarted(StateMachine<States, Events> stateMachine) { } @Override public void stateMachineStopped(StateMachine<States, Events> stateMachine) { } @Override public void eventNotAccepted(Message<Events> event) { } @Override public void extendedStateChanged(Object key, Object value) { } @Override public void stateMachineError(StateMachine<States, Events> stateMachine, Exception exception) { } @Override public void stateContext(StateContext<States, Events> stateContext) { }}

在前面的示例中,我们创建了自己的侦听器类 () 扩展 .​​StateMachineEventListener​​​​StateMachineListenerAdapter​​

侦听器方法允许访问不同阶段的各种更改。您可以在使用 StateContext 中找到有关它的更多信息。​​stateContext​​​​StateContext​​

定义自己的侦听器后,可以在 使用该方法的状态机。这是一个问题 调味是将其连接到弹簧配置中还是执行 在应用程序生命周期中的任何时间手动操作。 以下示例演示如何附加侦听器:​​addStateListener​​

public class Config7 { @Autowired StateMachine<States, Events> stateMachine; @Bean public StateMachineEventListener stateMachineEventListener() { StateMachineEventListener listener = new StateMachineEventListener(); stateMachine.addStateListener(listener); return listener; }}

限制和问题

Spring 应用程序上下文不是最快的事件总线,所以我们 建议考虑一下状态机的事件速率 发送。为了获得更好的性能,最好使用该接口。出于这个具体的原因, 您可以将该标志与 和 一起使用 来禁用 Spring 应用程序上下文 事件,如上一节所示。 以下示例显示了如何禁用 Spring 应用程序上下文事件:​​StateMachineListener​​​​contextEvents​​​​@EnableStateMachine​​​​@EnableStateMachineFactory​​

@Configuration@EnableStateMachine(contextEvents = false)public class Config8 extends EnumStateMachineConfigurerAdapter<States, Events> {}@Configuration@EnableStateMachineFactory(contextEvents = false)public class Config9 extends EnumStateMachineConfigurerAdapter<States, Events> {}

上下文集成

与状态机进行交互有点限制 侦听其事件或对状态和 转换。有时,这种方法会太有限,并且 详细创建与状态机的应用程序的交互 工程。对于这个特定的用例,我们制作了一个弹簧样式 轻松插入状态机功能的上下文集成 进入你的豆子。

对现有注释进行了统一,以便能够访问相同的注释 侦听状态机事件中提供的状态机执行点。

您可以使用注释关联状态 具有现有 Bean 的机器。然后你可以开始添加 支持对该 Bean 方法的注释。 以下示例演示如何执行此操作:​​@WithStateMachine​​

@WithStateMachinepublic class Bean1 { @OnTransition public void anyTransition() { }}

您还可以从 使用注释字段的应用程序上下文。 以下示例演示如何执行此操作:​​name​​

@WithStateMachine(name = "myMachineBeanName")public class Bean2 { @OnTransition public void anyTransition() { }}

有时,使用起来更方便,这是一些东西 您可以进行设置以更好地识别多个实例。此 ID 映射到 接口中的方法。 以下示例演示如何使用它:​​machine id​​​​getId()​​​​StateMachine​​

@WithStateMachine(id = "myMachineId")public class Bean16 { @OnTransition public void anyTransition() { }}

当使用状态机工厂生成状态机时,状态机使用动态提供,bean name将默认为无法使用,因为仅在运行时已知。​​id​​​​stateMachine​​​​@WithStateMachine (id = "some-id")​​​​id​​

在这种情况下,请使用工厂生成的所有状态机或所有状态机都将与您的 bean 或 bean 相关联。​​@WithStateMachine​​​​@WithStateMachine(name = "stateMachine")​​

您也可以用作元注释,如图所示 在前面的示例中。在这种情况下,您可以使用 注释您的 bean。 以下示例演示如何执行此操作:​​@WithStateMachine​​​​WithMyBean​​

@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@WithStateMachine(name = "myMachineBeanName")public @interface WithMyBean {}

这些方法的返回类型无关紧要,并且是有效的 丢弃。

启用集成

您可以使用 注释,用于导入所需的 配置到 Spring 应用程序上下文中。两者都已经 使用此注释进行注释,因此无需再次添加。 但是,如果计算机的构建和配置没有 配置适配器,您必须使用 才能将这些功能与 一起使用。 以下示例演示如何执行此操作:​​@WithStateMachine​​​​@EnableWithStateMachine​​​​@EnableStateMachine​​​​@EnableStateMachineFactory​​​​@EnableWithStateMachine​​​​@WithStateMachine​​

public static StateMachine<String, String> buildMachine(BeanFactory beanFactory) throws Exception { Builder<String, String> builder = StateMachineBuilder.builder(); builder.configureConfiguration() .withConfiguration() .machineId("myMachineId") .beanFactory(beanFactory); builder.configureStates() .withStates() .initial("S1") .state("S2"); builder.configureTransitions() .withExternal() .source("S1") .target("S2") .event("E1"); return builder.build();}@WithStateMachine(id = "myMachineId")static class Bean17 { @OnStateChanged public void onStateChanged() { }}

如果机器不是创建为 Bean,则需要为机器进行设置,如预解示例所示。否则,tge机器是 不知道调用方法的处理程序。​​BeanFactory​​​​@WithStateMachine​​

方法参数

每个注释都支持完全相同的一组可能的方法 参数,但运行时行为会有所不同,具体取决于 批注本身和调用批注方法的阶段。自 更好地了解上下文的工作原理,请参阅使用状态上下文。

实际上,所有带注释的方法都是使用 Spring SPel 调用的 表达式,在此过程中动态构建。要使 这项工作,这些表达式需要有一个根对象(它们根据该对象进行评估)。 此根对象是一个 .我们也做了一些 内部调整,以便可以访问方法 直接不通过上下文句柄。​​StateContext​​​​StateContext​​

最简单的方法参数是 a 本身。 以下示例演示如何使用它:​​StateContext​​

@WithStateMachinepublic class Bean3 { @OnTransition public void anyTransition(StateContext<String, String> stateContext) { }}

您可以访问其余内容。 参数的数量和顺序无关紧要。 下面的示例演示如何访问内容的各个部分:​​StateContext​​​​StateContext​​

@WithStateMachinepublic class Bean4 { @OnTransition public void anyTransition( @EventHeaders Map<String, Object> headers, @EventHeader("myheader1") Object myheader1, @EventHeader(name = "myheader2", required = false) String myheader2, ExtendedState extendedState, StateMachine<String, String> stateMachine, Message<String> message, Exception e) { }}

您可以使用 来获取所有事件标头,而不是使用 ,它可以绑定到单个标头。​​@EventHeaders​​​​@EventHeader​​

过渡批注

过渡的注释是 、 和。​​@OnTransition​​​​@OnTransitionStart​​​​@OnTransitionEnd​​

这些批注的行为完全相同。为了展示它们是如何工作的,我们展示了 如何使用。在此批注中,属性的 您可以使用 和 限定转换。如果 和 留空,则匹配任何过渡。 下面的示例演示如何使用批注 (记住这一点并以同样的方式工作):​​@OnTransition​​​​source​​​​target​​​​source​​​​target​​​​@OnTransition​​​​@OnTransitionStart​​​​@OnTransitionEnd​​

@WithStateMachinepublic class Bean5 { @OnTransition(source = "S1", target = "S2") public void fromS1ToS2() { } @OnTransition public void anyTransition() { }}

默认情况下,不能将批注与状态和 由于 Java 语言限制而创建的事件枚举。 因此,您需要使用字符串表示形式。​​@OnTransition​​

此外,您可以访问 和 通过将所需的参数添加到方法中。方法 然后使用这些参数自动调用。 以下示例演示如何执行此操作:​​Event Headers​​​​ExtendedState​​

@WithStateMachinepublic class Bean6 { @StatesOnTransition(source = States.S1, target = States.S2) public void fromS1ToS2(@EventHeaders Map<String, Object> headers, ExtendedState extendedState) { }}

但是,如果要具有类型安全的注释,则可以 创建新批注并用作元批注。 此用户级注释可以引用实际状态和 事件枚举,框架尝试以相同的方式匹配这些枚举。 以下示例演示如何执行此操作:​​@OnTransition​​

@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@OnTransitionpublic @interface StatesOnTransition { States[] source() default {}; States[] target() default {};}

在前面的示例中,我们创建了一个以类型安全的方式定义和的注释。 以下示例在 Bean 中使用该注释:​​@StatesOnTransition​​​​source​​​​target​​

@WithStateMachinepublic class Bean7 { @StatesOnTransition(source = States.S1, target = States.S2) public void fromS1ToS2() { }}

状态注释

可以使用以下状态注释:、 和 。以下示例演示如何使用批注 ( 其他两个工作方式相同):​​@OnStateChanged​​​​@OnStateEntry​​​​@OnStateExit​​​​OnStateChanged​​

@WithStateMachinepublic class Bean8 { @OnStateChanged public void anyStateChange() { }}

与过渡批注一样,您可以定义 目标和源状态。以下示例演示如何执行此操作:

@WithStateMachinepublic class Bean9 { @OnStateChanged(source = "S1", target = "S2") public void stateChangeFromS1toS2() { }}

为了类型安全,需要通过用作元注释来为枚举创建新的注释。以下示例演示如何执行此操作:​​@OnStateChanged​​

@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@OnStateChangedpublic @interface StatesOnStates { States[] source() default {}; States[] target() default {};}@WithStateMachinepublic class Bean10 { @StatesOnStates(source = States.S1, target = States.S2) public void fromS1ToS2() { }}

状态进入和退出的方法的行为方式相同,如以下示例所示:

@WithStateMachinepublic class Bean11 { @OnStateEntry public void anyStateEntry() { } @OnStateExit public void anyStateExit() { }}

事件注释

有一个与事件相关的注释。它被命名为. 如果指定该属性,则可以侦听特定事件不是 接受。如果未指定事件,则可以列出任何未指定的事件 接受。以下示例显示了使用批注的两种方法:​​@OnEventNotAccepted​​​​event​​​​@OnEventNotAccepted​​

@WithStateMachinepublic class Bean12 { @OnEventNotAccepted public void anyEventNotAccepted() { } @OnEventNotAccepted(event = "E1") public void e1EventNotAccepted() { }}

状态机注释

以下注释可用于状态机:、 和 。​​@OnStateMachineStart​​​​@OnStateMachineStop​​​​@OnStateMachineError​​

在状态机的启动和停止期间,将调用生命周期方法。 下面的示例演示如何使用和侦听这些事件:​​@OnStateMachineStart​​​​@OnStateMachineStop​​

@WithStateMachinepublic class Bean13 { @OnStateMachineStart public void onStateMachineStart() { } @OnStateMachineStop public void onStateMachineStop() { }}

如果状态机出现异常错误,则调用注释。以下示例演示如何使用它:​​@OnStateMachineStop​​

@WithStateMachinepublic class Bean14 { @OnStateMachineError public void onStateMachineError() { }}

扩展状态注释

有一个与状态相关的扩展注释。它被命名为.您也可以只收听更改 对于具体更改。下面的示例演示如何使用 ,带属性和不带属性:​​@OnExtendedStateChanged​​​​key​​​​@OnExtendedStateChanged​​​​key​​

@WithStateMachinepublic class Bean15 { @OnExtendedStateChanged public void anyStateChange() { } @OnExtendedStateChanged(key = "key1") public void key1Changed() { }}

用​​StateMachineAccessor​​

​​StateMachine​​是与状态机通信的主接口。 有时,您可能需要获得更多动态和 以编程方式访问状态机的内部结构及其 嵌套的计算机和区域。对于这些用例,公开一个名为 的功能接口,该接口提供 用于访问个人和实例的界面。​​StateMachine​​​​StateMachineAccessor​​​​StateMachine​​​​Region​​

​​StateMachineFunction​​是一个简单的功能界面,让 将接口应用于状态机。跟 JDK 7,这些创建的代码有点冗长。但是,对于 JDK 8 lambda, 文档相对不冗长。​​StateMachineAccess​​

该方法提供对 状态机。以下示例演示如何使用它:​​doWithAllRegions​​​​Region​​

stateMachine.getStateMachineAccessor().doWithAllRegions(function -> function.setRelay(stateMachine));stateMachine.getStateMachineAccessor() .doWithAllRegions(access -> access.setRelay(stateMachine));

该方法允许访问 状态机。以下示例演示如何使用它:​​doWithRegion​​​​Region​​

stateMachine.getStateMachineAccessor().doWithRegion(function -> function.setRelay(stateMachine));stateMachine.getStateMachineAccessor() .doWithRegion(access -> access.setRelay(stateMachine));

该方法提供对 状态机。以下示例演示如何使用它:​​withAllRegions​​​​Region​​

for (StateMachineAccess<String, String> access : stateMachine.getStateMachineAccessor().withAllRegions()) { access.setRelay(stateMachine);}stateMachine.getStateMachineAccessor().withAllRegions() .stream().forEach(access -> access.setRelay(stateMachine));

该方法允许访问 状态机。以下示例演示如何使用它:​​withRegion​​​​Region​​

stateMachine.getStateMachineAccessor() .withRegion().setRelay(stateMachine);

用​​StateMachineInterceptor​​

您可以不使用接口,而不是使用接口 使用 .一个概念上的区别是您可以使用 拦截器,用于拦截和停止当前状态 更改或更改其转换逻辑。而不是实现完整的接口, 可以使用调用的适配器类来重写 默认的无操作方法。​​StateMachineListener​​​​StateMachineInterceptor​​​​StateMachineInterceptorAdapter​​

一个配方(持续​)和一个样品 (持久)与使用 拦截 器。

您可以通过 注册侦听器。的概念 拦截器是一个相对较深的内部特征,因此不是 直接通过界面公开。​​StateMachineAccessor​​​​StateMachine​​

以下示例演示如何添加和覆盖选定的 方法:​​StateMachineInterceptor​​

stateMachine.getStateMachineAccessor() .withRegion().addStateMachineInterceptor(new StateMachineInterceptor<String, String>() { @Override public Message<String> preEvent(Message<String> message, StateMachine<String, String> stateMachine) { return message; } @Override public StateContext<String, String> preTransition(StateContext<String, String> stateContext) { return stateContext; } @Override public void preStateChange(State<String, String> state, Message<String> message, Transition<String, String> transition, StateMachine<String, String> stateMachine, StateMachine<String, String> rootStateMachine) { } @Override public StateContext<String, String> postTransition(StateContext<String, String> stateContext) { return stateContext; } @Override public void postStateChange(State<String, String> state, Message<String> message, Transition<String, String> transition, StateMachine<String, String> stateMachine, StateMachine<String, String> rootStateMachine) { } @Override public Exception stateMachineError(StateMachine<String, String> stateMachine, Exception exception) { return exception; } });

有关前面示例中所示的错误处理的详细信息,请参阅状态机错误处理。

状态机安全性

安全功能建立在 Spring 安全性的功能之上。安全功能包括 当需要保护状态机的一部分时很方便 执行和与之交互。

我们希望您相当熟悉Spring Security,这意味着 我们不详细介绍整体安全框架的工作原理。为 此信息,您应该阅读 Spring 安全参考文档 (可在此处获得)。

第一级安全防御自然是保护事件, 这真正推动了将要发生的事情 发生在状态机中。然后,您可以定义更精细的安全设置 用于过渡和操作。这与让员工进入建筑物平行 然后允许访问建筑物内的特定房间,甚至能够 以打开和关闭特定房间的灯。如果你信任 您的用户、事件安全可能就是您所需要的。如果没有, 您需要应用更详细的安全性。

您可以在了解安全性中找到更多详细信息。

有关完整示例,请参阅安全性示例。

配置安全性

所有安全性的通用配置都在 中完成,该配置可从 中获取。默认情况下,安全性处于禁用状态, 即使春季安全类是 目前。以下示例演示如何启用安全性:​​SecurityConfigurer​​​​StateMachineConfigurationConfigurer​​

@Configuration@EnableStateMachinestatic class Config4 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception { config .withSecurity() .enabled(true) .transitionAccessDecisionManager(null) .eventAccessDecisionManager(null); }}

如果绝对需要,可以针对事件和 转换。如果未定义决策经理或 将它们设置为 ,默认管理器将在内部创建。​​AccessDecisionManager​​​​null​​

保护事件

事件安全性在全局级别由 定义。 以下示例演示如何启用事件安全性:​​SecurityConfigurer​​

@Configuration@EnableStateMachinestatic class Config1 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception { config .withSecurity() .enabled(true) .event("true") .event("ROLE_ANONYMOUS", ComparisonType.ANY); }}

在前面的配置示例中,我们使用表达式 ,该表达式始终计算 自。使用始终计算结果的表达式在实际应用程序中没有意义,但表明了以下观点: 表达式需要返回 或 。我们还定义了一个 的属性和 的 a 。有关使用属性的详细信息 和表达式,请参阅使用安全属性和表达式。​​true​​​​TRUE​​​​TRUE​​​​TRUE​​​​FALSE​​​​ROLE_ANONYMOUS​​​​ComparisonType​​​​ANY​​

保护转换

您可以全局定义转换安全性,如以下示例所示。

@Configuration@EnableStateMachinestatic class Config6 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception { config .withSecurity() .enabled(true) .transition("true") .transition("ROLE_ANONYMOUS", ComparisonType.ANY); }}

如果在转换本身中定义了安全性,则它会覆盖任何 全局设置安全性。以下示例演示如何执行此操作:

@Configuration@EnableStateMachinestatic class Config2 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception { transitions .withExternal() .source("S0") .target("S1") .event("A") .secured("ROLE_ANONYMOUS", ComparisonType.ANY) .secured("hasTarget('S1')"); }}

有关使用属性和表达式的详细信息,请参阅使用安全属性和表达式。

保护操作

状态中的操作没有专用的安全定义 计算机,但您可以使用全局方法安全性来保护操作 来自春季安全。这要求 定义为代理,其方法用 注释。以下示例演示如何执行此操作:​​Action​​​​@Bean​​​​execute​​​​@Secured​​

@Configuration@EnableStateMachinestatic class Config3 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception { config .withSecurity() .enabled(true); } @Override public void configure(StateMachineStateConfigurer<String, String> states) throws Exception { states .withStates() .initial("S0") .state("S1"); } @Override public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception { transitions .withExternal() .source("S0") .target("S1") .action(securedAction()) .event("A"); } @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) @Bean public Action<String, String> securedAction() { return new Action<String, String>() { @Secured("ROLE_ANONYMOUS") @Override public void execute(StateContext<String, String> context) { } }; }}

全局方法安全性需要使用 Spring 安全性启用。 以下示例演示如何执行此操作:

@Configuration@EnableGlobalMethodSecurity(securedEnabled = true)public static class Config5 extends WebSecurityConfigurerAdapter { @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user").password("password").roles("USER"); }}

有关更多详细信息,请参阅 Spring 安全性参考指南(可在此处获得)。

使用安全属性和表达式

通常,可以通过以下两种方式之一定义安全属性:通过 使用安全属性和使用安全表达式。 属性更易于使用,但在以下方面相对有限 功能性。表达式提供更多功能,但有点 更难使用。

泛型属性用法

默认情况下,事件和 转换都使用 ,这意味着您可以使用角色属性 来自春季安全。​​AccessDecisionManager​​​​RoleVoter​​

对于属性,我们有三种不同的比较类型:、 和 。这些比较类型映射到默认访问决策管理器 (、 和 分别)。 如果定义了自定义 ,则比较类型为 有效地丢弃,因为它仅用于创建默认管理器。​​ANY​​​​ALL​​​​MAJORITY​​​​AffirmativeBased​​​​UnanimousBased​​​​ConsensusBased​​​​AccessDecisionManager​​

泛型表达式用法

安全表达式必须返回 或 。​​TRUE​​​​FALSE​​

表达式根对象的基类是 。它提供了一些常见的表达式,这些表达式 在转换和事件安全性中都可用。下表 描述最常用的内置表达式:​​SecurityExpressionRoot​​

Table 1. Common built-in expressions

表达

描述

​​hasRole([role])​​

如果当前主体具有指定的角色,则返回。由 默认值,如果提供的角色不以 开头,则为 添加。您可以通过修改 on .​​true​​​​ROLE_​​​​defaultRolePrefix​​​​DefaultWebSecurityExpressionHandler​​

​​hasAnyRole([role1,role2])​​

如果当前主体具有任何提供的 角色(以逗号分隔的字符串列表形式给出)。默认情况下,如果每个 提供的角色不是以 开头的,而是添加的。您可以自定义此 通过修改 on .​​true​​​​ROLE_​​​​defaultRolePrefix​​​​DefaultWebSecurityExpressionHandler​​

​​hasAuthority([authority])​​

如果当前主体具有指定的权限,则返回。​​true​​

​​hasAnyAuthority([authority1,authority2])​​

如果当前主体具有任何提供的 角色(以逗号分隔的字符串列表形式给出)。​​true​​

​​principal​​

允许直接访问表示 当前用户。

​​authentication​​

允许直接访问获得的当前对象 从 .​​Authentication​​​​SecurityContext​​

​​permitAll​​

始终计算结果为 。​​true​​

​​denyAll​​

始终计算结果为 。​​false​​

​​isAnonymous()​​

如果当前主体是匿名用户,则返回。​​true​​

​​isRememberMe()​​

如果当前主体是“记住我”用户,则返回。​​true​​

​​isAuthenticated()​​

如果用户不是匿名的,则返回。​​true​​

​​isFullyAuthenticated()​​

如果用户不是匿名用户或记住我用户,则返回。​​true​​

​​hasPermission(Object target, Object permission)​​

如果用户有权访问提供的目标,则返回 授予权限 — 例如,.​​true​​​​hasPermission(domainObject, 'read')​​

​​hasPermission(Object targetId, String targetType, Object permission)​​

如果用户有权访问提供的目标,则返回 授予权限 — 例如,.​​true​​​​hasPermission(1, 'com.example.domain.Message', 'read')​​

事件属性

可以使用前缀 来匹配事件 ID。例如,匹配 事件将与 的属性匹配。​​EVENT_​​​​A​​​​EVENT_A​​

事件表达式

事件的表达式根对象的基类是 。它提供对对象的访问,该对象随事件一起传递。 只有一种方法,下表描述了该方法:​​EventSecurityExpressionRoot​​​​Message​​​​EventSecurityExpressionRoot​​

Table 2. Event expressions

表达

描述

​​hasEvent(Object event)​​

如果事件与给定事件匹配,则返回。​​true​​

过渡属性

匹配转换源和目标时,可以分别使用 和 前缀。​​TRANSITION_SOURCE_​​​​TRANSITION_TARGET_​​

过渡表达式

用于转换的表达式根对象的基类是 。它提供对对象的访问,该对象被传递以进行过渡更改。 有两种方法,分别是 表描述:​​TransitionSecurityExpressionRoot​​​​Transition​​​​TransitionSecurityExpressionRoot​​

Table 3. Transition expressions

表达

描述

​​hasSource(Object source)​​

如果转换源与给定源匹配,则返回。​​true​​

​​hasTarget(Object target)​​

如果转换目标与给定目标匹配,则返回。​​true​​

了解安全性

本节提供有关安全性如何在 状态机。你可能真的不需要知道,但它是 总是最好保持透明,而不是隐藏所有的魔力 发生在幕后。

只有当 Spring 状态机在围墙中运行时,安全性才有意义 用户无法直接访问应用程序的花园,因此可以 修改 Spring 安全性在本地线程中的保留。 如果用户控制JVM,那么实际上没有安全性 完全。​​SecurityContext​​

安全性的集成点是使用 StateMachineInterceptor 创建的,然后自动将其添加到 状态机(如果启用了安全性)。特定的类是 ,它截获事件和 转换。然后,此拦截器会咨询 Spring 安全性,以确定是否可以发送事件或是否可以进行转换 执行。实际上,如果决定或投票导致异常,则事件或转换将被拒绝。​​StateMachineSecurityInterceptor​​​​AccessDecisionManager​​​​AccessDecisionManager​​

由于Spring Security的工作方式,我们 每个受保护对象需要一个实例。这就是为什么有 是事件和转换的不同管理器。在这种情况下,事件 转换是我们保护的不同类对象。​​AccessDecisionManager​​

默认情况下,对于事件,投票者 (、 和 ) 将添加到 .​​EventExpressionVoter​​​​EventVoter​​​​RoleVoter​​​​AccessDecisionManager​​

默认情况下,对于转换,投票者 (、 和 ) 将添加到 .​​TransitionExpressionVoter​​​​TransitionVoter​​​​RoleVoter​​​​AccessDecisionManager​​

状态机错误处理

如果状态机在状态转换期间检测到内部错误 逻辑,它可能会引发异常。在处理此异常之前 在内部,您有机会拦截。

通常,您可以使用 拦截错误和 以下清单显示了一个示例:​​StateMachineInterceptor​​

StateMachine<String, String> stateMachine;void addInterceptor() { stateMachine.getStateMachineAccessor() .doWithRegion(function -> function.addStateMachineInterceptor(new StateMachineInterceptorAdapter<String, String>() { @Override public Exception stateMachineError(StateMachine<String, String> stateMachine, Exception exception) { return exception; } }) );}

检测到错误时,将执行正常事件通知机制。 这使您可以使用或 Spring 应用程序 上下文事件侦听器。有关这些事件的更多信息,请参阅侦听状态机事件。​​StateMachineListener​​

话虽如此,以下示例显示了一个简单的侦听器:

public class ErrorStateMachineListener extends StateMachineListenerAdapter<String, String> { @Override public void stateMachineError(StateMachine<String, String> stateMachine, Exception exception) { // do something with error }}

以下示例显示了一个通用检查:​​ApplicationListener​​​​StateMachineEvent​​

public class GenericApplicationEventListener implements ApplicationListener<StateMachineEvent> { @Override public void onApplicationEvent(StateMachineEvent event) { if (event instanceof OnStateMachineError) { // do something with error } }}

您也可以直接定义为 仅识别实例,如以下示例所示:​​ApplicationListener​​​​StateMachineEvent​​

public class ErrorApplicationEventListener implements ApplicationListener<OnStateMachineError> { @Override public void onApplicationEvent(OnStateMachineError event) { // do something with error }}

为转换定义的操作也有其自己的错误处理 逻辑。请参阅转换操作错误处理。

使用反应式 api,可能会得到操作执行错误 从 StateMachineEventResult 返回。拥有简单的机器 操作中的错误转换为状态 。​​S1​​

@Configuration@EnableStateMachinestatic class Config1 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineStateConfigurer<String, String> states) throws Exception { states .withStates() .initial("SI") .stateEntry("S1", (context) -> { throw new RuntimeException("example error"); }); } @Override public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception { transitions .withExternal() .source("SI") .target("S1") .event("E1"); }}

下面的测试概念显示了如何消耗可能的错误 来自 StateMachineEventResult。

@Autowiredprivate StateMachine<String, String> machine;@Testpublic void testActionEntryErrorWithEvent() throws Exception { StepVerifier.create(machine.startReactively()).verifyComplete(); assertThat(machine.getState().getIds()).containsExactlyInAnyOrder("SI"); StepVerifier.create(machine.sendEvent(Mono.just(MessageBuilder.withPayload("E1").build()))) .consumeNextWith(result -> { StepVerifier.create(result.complete()).consumeErrorWith(e -> { assertThat(e).isInstanceOf(StateMachineException.class).hasMessageContaining("example error"); }).verify(); }) .verifyComplete(); assertThat(machine.getState().getIds()).containsExactlyInAnyOrder("S1");}

进入/退出操作中的错误不会阻止转换的发生。

上一篇:Spring Statemachine状态机的概念(三)
下一篇:没有了
网友评论