使用作用域
状态机对作用域的支持非常有限,但您可以 通过以下两种方式之一使用普通的 Spring 注释来启用范围:session@Scope
- 如果状态机是使用构建器手动构建的,并返回到 上下文作为 .@Bean
- 通过配置适配器。
两者 这些需要存在,设置为 和 设置为 。以下示例 显示两个用例:@ScopescopeNamesessionproxyModeScopedProxyMode.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@ControllerHttpSession
@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 分别附加到 和 状态。以下示例定义了这些操作(和):action1action2entryexitaction3
@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 表达式将事件发送到 状态机。action3Events.E1
StateContext中进行了描述。
带有操作的 SpEL 表达式
您还可以使用 SpEL 表达式作为 全面实施。Action
反应性操作
普通接口是一种简单的函数方法,取回 void。在你阻止之前,这里没有任何阻塞 在方法本身中,这有点问题,因为框架不能 知道里面到底发生了什么。ActionStateContext
public interface Action<S, E> { void execute(StateContext<S, E> context);}为了克服这个问题,我们在内部将处理更改为 处理普通Java的获取和返回。通过这种方式,我们可以调用操作并完全以响应方式 仅在订阅时以非阻塞方式执行操作 以等待完成。ActionFunctionStateContextMono
public interface ReactiveAction<S, E> extends Function<StateContext<S, E>, Mono<Void>> {}内部旧接口包裹着一个可运行的反应堆单声道,因为它 共享相同的返回类型。我们无法控制您用这种方法做什么!Action
使用防护装置
如要记住的事情中所示,和 bean 附加到条目和 分别是退出状态。 以下示例还对事件使用防护:guard1guard2
@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 是否扩展 名为 的状态变量的计算结果为 。 下面的示例实现一些示例防护:GuardguardExpressionmyvarTRUE
@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 表达式作为 完整的防护实施。唯一的要求是表达式需要 返回一个值以满足实现。这可以是 使用一个函数进行演示,该函数采用 表达式作为参数。BooleanGuardguardExpression()
反应防护装置
普通接口是一种简单的函数方法,取值并返回布尔值。在你阻止之前,这里没有任何阻塞 在方法本身中,这有点问题,因为框架不能 知道里面到底发生了什么。GuardStateContext
public interface Guard<S, E> { boolean evaluate(StateContext<S, E> context);}为了克服这个问题,我们在内部将处理更改为 处理普通Java的获取和返回。这样,我们可以完全以被动的方式呼叫警卫 仅在订阅时以非阻塞方式对其进行评估 以使用返回值等待完成。GuardFunctionStateContextMono<Boolean>
public interface ReactiveGuard<S, E> extends Function<StateContext<S, E>, Mono<Boolean>> {}内部旧接口包装有反应器单声道函数。我们没有 控制你在这种方法中做什么!Guard
使用扩展状态
假设您需要创建一个状态机来跟踪 很多时候,用户按下键盘上的键,然后终止 当按键被按下 1000 次时。一个可能但非常幼稚的解决方案 将是为每 1000 次按键创建一个新状态。 你可能会突然得到一个天文数字 状态,这自然不是很实用。
这就是扩展状态变量不需要的地方 以添加更多状态以驱动状态机更改。相反 您可以在转换期间执行简单的变量更改。
StateMachine有一个名为 的方法。它返回一个 名为 的接口,用于访问扩展状态 变量。您可以直接通过状态机访问这些变量,也可以在操作或转换的回调期间访问这些变量。 以下示例演示如何执行此操作:getExtendedState()ExtendedStateStateContext
public Action<String, String> myVariableAction() { return new Action<String, String>() { @Override public void execute(StateContext<String, String> context) { context.getExtendedState() .getVariables().put("mykey", "myvalue"); } };}如果您需要收到扩展状态变量的通知 更改,您有两种选择:使用或 侦听回调。以下示例 使用方法:StateMachineListenerextendedStateChanged(key, value)extendedStateChanged
public class ExtendedStateVariableListener extends StateMachineListenerAdapter<String, String> { @Override public void extendedStateChanged(Object key, Object value) { // do something with changed variable }}或者,您可以为 .如侦听状态机事件中所述, 您还可以收听所有事件。 以下示例用于侦听状态更改:OnExtendedStateChangedStateMachineEventonApplicationEvent
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传递到各种组件中,例如 和 。ActionGuard
阶段
舞台是 on 的表示 状态机当前正在与用户交互的内容。当前可用的 阶段为 、、、、、 和 。这些状态可能看起来很熟悉,因为 它们与您与侦听器交互的方式相匹配(如侦听状态机事件中所述)。stageEVENT_NOT_ACCEPTEDEXTENDED_STATE_CHANGEDSTATE_CHANGEDSTATE_ENTRYSTATE_EXITSTATEMACHINE_ERRORSTATEMACHINE_STARTSTATEMACHINE_STOPTRANSITIONTRANSITION_STARTTRANSITION_END
触发转换
驱动状态机是通过使用转换来完成的,这些转换被触发 通过触发器。当前支持的触发器是 和 。EventTriggerTimerTrigger
用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();}无论您发送一个事件还是多个事件,结果始终是一个序列 的结果。之所以如此,是因为在存在多个请求的情况下,结果将 从这些区域中的多台计算机返回。这显示 使用方法,给出结果列表。方法 本身只是一个语法糖收集列表。如果有 只有一个区域,此列表包含一个结果。sendEventCollectFlux
Message<String> message1 = MessageBuilder .withPayload("E1") .build();Mono<List<StateMachineEventResult<String, String>>> results = stateMachine.sendEventCollect(Mono.just(message1));results.subscribe();在订阅返回的通量之前,不会发生任何反应。从StateMachineEventResult查看更多相关信息。
前面的示例通过构造包装来发送事件 a 并订阅返回的结果。 让 我们向事件添加任意额外信息,然后可见 到(例如)何时实施操作。MonoMessageFluxMessageStateContext
消息标头通常会传递,直到计算机运行到 特定事件的完成。例如,如果事件导致 转换为具有匿名转换的状态 状态 ,原始事件可用于状态 中的操作或守卫。ABB
也可以发送消息,而不是仅发送 一个带有 .FluxMono
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包含有关结果的更多详细信息 的事件发送。从中您可以得到一个处理事件,它本身以及什么是实际的.来自您 可以查看邮件是被接受、拒绝还是延迟。一般来说,当 订阅完成,事件将传递到计算机中。RegionMessageResultTypeResultType
用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 的外部过渡 事件和 ,分别。有趣的部分 对于使用 是 当我们定义时 源状态和 的内部转换。S1S2S3S1S2S1S3E1E2TimerTriggerS2S3
对于这两个转换,我们调用 bean (),其中 源状态使用和使用 。 给出的值以毫秒为单位(在两种情况下都是毫秒或一秒)。ActiontimerActionS2timerS3timerOnce1000
一旦状态机收到事件,它就会进行转换 从 到 ,计时器启动。当状态为 时,将运行并导致与之关联的转换 状态 — 在本例中为已定义的内部转换。E1S1S2S2TimerTriggertimerAction
一旦状态机收到 ,事件就会执行转换 从 到 ,计时器启动。此计时器仅执行一次 进入状态后(在计时器中定义的延迟之后)。E2S1S3
在幕后,计时器是简单的触发器,可能会导致 过渡发生。使用保留定义过渡 仅当源状态处于活动状态时,触发才会触发并导致转换。 过渡有点不同,因为它 仅在实际进入源状态的延迟后触发。timer()timerOnce()
如果您希望延迟后发生某些事情,请使用 正好在进入状态时一次。timerOnce()
侦听状态机事件
在某些用例中,您想知道发生了什么 状态机,对某事做出反应,或获取日志记录详细信息 调试目的。Spring 状态机提供了用于添加侦听器的接口。这些侦听器 然后给出一个选项,以便在各种状态更改时获取回调, 动作,等等。
你基本上有两个选择:听Spring应用程序 上下文事件或直接将侦听器附加到状态机。两者 这些基本上提供相同的信息。一个生产 事件作为事件类,另一个通过侦听器产生回调 接口。这两者都有优点和缺点,我们将在后面讨论。
应用程序上下文事件
应用程序上下文事件类包括 、、 ,以及扩展基事件类 的其他类。这些可以按原样使用 弹簧 .OnTransitionStartEventOnTransitionEventOnTransitionEndEventOnStateExitEventOnStateEntryEventOnStateChangedEventOnStateMachineStartOnStateMachineStopStateMachineEventApplicationListener
StateMachine通过 发送上下文事件。 如果类用 注释,则会自动创建默认实现。 下面的示例从类中定义的 Bean 中获取 a:StateMachineEventPublisher@Configuration@EnableStateMachineStateMachineApplicationEventListener@Configuration
public class StateMachineApplicationEventListener implements ApplicationListener<StateMachineEvent> { @Override public void onApplicationEvent(StateMachineEvent event) { }}@Configurationpublic class ListenerConfig { @Bean public StateMachineApplicationEventListener contextListener() { return new StateMachineApplicationEventListener(); }}还可以通过使用 自动启用上下文事件 , 用于构建机器并注册为豆类, 如以下示例所示:@EnableStateMachineStateMachine
@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
通过使用 ,您可以扩展它和 实现所有回调方法或使用包含存根方法实现的类并选择哪些实现 以覆盖。 以下示例使用后一种方法:StateMachineListenerStateMachineListenerAdapter
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) { }}在前面的示例中,我们创建了自己的侦听器类 () 扩展 .StateMachineEventListenerStateMachineListenerAdapter
侦听器方法允许访问不同阶段的各种更改。您可以在使用 StateContext 中找到有关它的更多信息。stateContextStateContext
定义自己的侦听器后,可以在 使用该方法的状态机。这是一个问题 调味是将其连接到弹簧配置中还是执行 在应用程序生命周期中的任何时间手动操作。 以下示例演示如何附加侦听器: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 应用程序上下文事件:StateMachineListenercontextEvents@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 idgetId()StateMachine
@WithStateMachine(id = "myMachineId")public class Bean16 { @OnTransition public void anyTransition() { }}当使用状态机工厂生成状态机时,状态机使用动态提供,bean name将默认为无法使用,因为仅在运行时已知。idstateMachine@WithStateMachine (id = "some-id")id
在这种情况下,请使用工厂生成的所有状态机或所有状态机都将与您的 bean 或 bean 相关联。@WithStateMachine@WithStateMachine(name = "stateMachine")
您也可以用作元注释,如图所示 在前面的示例中。在这种情况下,您可以使用 注释您的 bean。 以下示例演示如何执行此操作:@WithStateMachineWithMyBean
@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 调用的 表达式,在此过程中动态构建。要使 这项工作,这些表达式需要有一个根对象(它们根据该对象进行评估)。 此根对象是一个 .我们也做了一些 内部调整,以便可以访问方法 直接不通过上下文句柄。StateContextStateContext
最简单的方法参数是 a 本身。 以下示例演示如何使用它:StateContext
@WithStateMachinepublic class Bean3 { @OnTransition public void anyTransition(StateContext<String, String> stateContext) { }}您可以访问其余内容。 参数的数量和顺序无关紧要。 下面的示例演示如何访问内容的各个部分:StateContextStateContext
@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
这些批注的行为完全相同。为了展示它们是如何工作的,我们展示了 如何使用。在此批注中,属性的 您可以使用 和 限定转换。如果 和 留空,则匹配任何过渡。 下面的示例演示如何使用批注 (记住这一点并以同样的方式工作):@OnTransitionsourcetargetsourcetarget@OnTransition@OnTransitionStart@OnTransitionEnd
@WithStateMachinepublic class Bean5 { @OnTransition(source = "S1", target = "S2") public void fromS1ToS2() { } @OnTransition public void anyTransition() { }}默认情况下,不能将批注与状态和 由于 Java 语言限制而创建的事件枚举。 因此,您需要使用字符串表示形式。@OnTransition
此外,您可以访问 和 通过将所需的参数添加到方法中。方法 然后使用这些参数自动调用。 以下示例演示如何执行此操作:Event HeadersExtendedState
@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 中使用该注释:@StatesOnTransitionsourcetarget
@WithStateMachinepublic class Bean7 { @StatesOnTransition(source = States.S1, target = States.S2) public void fromS1ToS2() { }}状态注释
可以使用以下状态注释:、 和 。以下示例演示如何使用批注 ( 其他两个工作方式相同):@OnStateChanged@OnStateEntry@OnStateExitOnStateChanged
@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() { }}事件注释
有一个与事件相关的注释。它被命名为. 如果指定该属性,则可以侦听特定事件不是 接受。如果未指定事件,则可以列出任何未指定的事件 接受。以下示例显示了使用批注的两种方法:@OnEventNotAcceptedevent@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() { }}扩展状态注释
有一个与状态相关的扩展注释。它被命名为.您也可以只收听更改 对于具体更改。下面的示例演示如何使用 ,带属性和不带属性:@OnExtendedStateChangedkey@OnExtendedStateChangedkey
@WithStateMachinepublic class Bean15 { @OnExtendedStateChanged public void anyStateChange() { } @OnExtendedStateChanged(key = "key1") public void key1Changed() { }}用StateMachineAccessor
StateMachine是与状态机通信的主接口。 有时,您可能需要获得更多动态和 以编程方式访问状态机的内部结构及其 嵌套的计算机和区域。对于这些用例,公开一个名为 的功能接口,该接口提供 用于访问个人和实例的界面。StateMachineStateMachineAccessorStateMachineRegion
StateMachineFunction是一个简单的功能界面,让 将接口应用于状态机。跟 JDK 7,这些创建的代码有点冗长。但是,对于 JDK 8 lambda, 文档相对不冗长。StateMachineAccess
该方法提供对 状态机。以下示例演示如何使用它:doWithAllRegionsRegion
stateMachine.getStateMachineAccessor().doWithAllRegions(function -> function.setRelay(stateMachine));stateMachine.getStateMachineAccessor() .doWithAllRegions(access -> access.setRelay(stateMachine));该方法允许访问 状态机。以下示例演示如何使用它:doWithRegionRegion
stateMachine.getStateMachineAccessor().doWithRegion(function -> function.setRelay(stateMachine));stateMachine.getStateMachineAccessor() .doWithRegion(access -> access.setRelay(stateMachine));该方法提供对 状态机。以下示例演示如何使用它:withAllRegionsRegion
for (StateMachineAccess<String, String> access : stateMachine.getStateMachineAccessor().withAllRegions()) { access.setRelay(stateMachine);}stateMachine.getStateMachineAccessor().withAllRegions() .stream().forEach(access -> access.setRelay(stateMachine));该方法允许访问 状态机。以下示例演示如何使用它:withRegionRegion
stateMachine.getStateMachineAccessor() .withRegion().setRelay(stateMachine);用StateMachineInterceptor
您可以不使用接口,而不是使用接口 使用 .一个概念上的区别是您可以使用 拦截器,用于拦截和停止当前状态 更改或更改其转换逻辑。而不是实现完整的接口, 可以使用调用的适配器类来重写 默认的无操作方法。StateMachineListenerStateMachineInterceptorStateMachineInterceptorAdapter
一个配方(持续)和一个样品 (持久)与使用 拦截 器。
您可以通过 注册侦听器。的概念 拦截器是一个相对较深的内部特征,因此不是 直接通过界面公开。StateMachineAccessorStateMachine
以下示例演示如何添加和覆盖选定的 方法: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 安全参考文档 (可在此处获得)。
第一级安全防御自然是保护事件, 这真正推动了将要发生的事情 发生在状态机中。然后,您可以定义更精细的安全设置 用于过渡和操作。这与让员工进入建筑物平行 然后允许访问建筑物内的特定房间,甚至能够 以打开和关闭特定房间的灯。如果你信任 您的用户、事件安全可能就是您所需要的。如果没有, 您需要应用更详细的安全性。
您可以在了解安全性中找到更多详细信息。
有关完整示例,请参阅安全性示例。
配置安全性
所有安全性的通用配置都在 中完成,该配置可从 中获取。默认情况下,安全性处于禁用状态, 即使春季安全类是 目前。以下示例演示如何启用安全性:SecurityConfigurerStateMachineConfigurationConfigurer
@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); }}如果绝对需要,可以针对事件和 转换。如果未定义决策经理或 将它们设置为 ,默认管理器将在内部创建。AccessDecisionManagernull
保护事件
事件安全性在全局级别由 定义。 以下示例演示如何启用事件安全性: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 。有关使用属性的详细信息 和表达式,请参阅使用安全属性和表达式。trueTRUETRUETRUEFALSEROLE_ANONYMOUSComparisonTypeANY
保护转换
您可以全局定义转换安全性,如以下示例所示。
@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@Beanexecute@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 安全性参考指南(可在此处获得)。
使用安全属性和表达式
通常,可以通过以下两种方式之一定义安全属性:通过 使用安全属性和使用安全表达式。 属性更易于使用,但在以下方面相对有限 功能性。表达式提供更多功能,但有点 更难使用。
泛型属性用法
默认情况下,事件和 转换都使用 ,这意味着您可以使用角色属性 来自春季安全。AccessDecisionManagerRoleVoter
对于属性,我们有三种不同的比较类型:、 和 。这些比较类型映射到默认访问决策管理器 (、 和 分别)。 如果定义了自定义 ,则比较类型为 有效地丢弃,因为它仅用于创建默认管理器。ANYALLMAJORITYAffirmativeBasedUnanimousBasedConsensusBasedAccessDecisionManager
泛型表达式用法
安全表达式必须返回 或 。TRUEFALSE
表达式根对象的基类是 。它提供了一些常见的表达式,这些表达式 在转换和事件安全性中都可用。下表 描述最常用的内置表达式:SecurityExpressionRoot
Table 1. Common built-in expressions
表达
描述
hasRole([role])
如果当前主体具有指定的角色,则返回。由 默认值,如果提供的角色不以 开头,则为 添加。您可以通过修改 on .trueROLE_defaultRolePrefixDefaultWebSecurityExpressionHandler
hasAnyRole([role1,role2])
如果当前主体具有任何提供的 角色(以逗号分隔的字符串列表形式给出)。默认情况下,如果每个 提供的角色不是以 开头的,而是添加的。您可以自定义此 通过修改 on .trueROLE_defaultRolePrefixDefaultWebSecurityExpressionHandler
hasAuthority([authority])
如果当前主体具有指定的权限,则返回。true
hasAnyAuthority([authority1,authority2])
如果当前主体具有任何提供的 角色(以逗号分隔的字符串列表形式给出)。true
principal
允许直接访问表示 当前用户。
authentication
允许直接访问获得的当前对象 从 .AuthenticationSecurityContext
permitAll
始终计算结果为 。true
denyAll
始终计算结果为 。false
isAnonymous()
如果当前主体是匿名用户,则返回。true
isRememberMe()
如果当前主体是“记住我”用户,则返回。true
isAuthenticated()
如果用户不是匿名的,则返回。true
isFullyAuthenticated()
如果用户不是匿名用户或记住我用户,则返回。true
hasPermission(Object target, Object permission)
如果用户有权访问提供的目标,则返回 授予权限 — 例如,.truehasPermission(domainObject, 'read')
hasPermission(Object targetId, String targetType, Object permission)
如果用户有权访问提供的目标,则返回 授予权限 — 例如,.truehasPermission(1, 'com.example.domain.Message', 'read')
事件属性
可以使用前缀 来匹配事件 ID。例如,匹配 事件将与 的属性匹配。EVENT_AEVENT_A
事件表达式
事件的表达式根对象的基类是 。它提供对对象的访问,该对象随事件一起传递。 只有一种方法,下表描述了该方法:EventSecurityExpressionRootMessageEventSecurityExpressionRoot
Table 2. Event expressions
表达
描述
hasEvent(Object event)
如果事件与给定事件匹配,则返回。true
过渡属性
匹配转换源和目标时,可以分别使用 和 前缀。TRANSITION_SOURCE_TRANSITION_TARGET_
过渡表达式
用于转换的表达式根对象的基类是 。它提供对对象的访问,该对象被传递以进行过渡更改。 有两种方法,分别是 表描述:TransitionSecurityExpressionRootTransitionTransitionSecurityExpressionRoot
Table 3. Transition expressions
表达
描述
hasSource(Object source)
如果转换源与给定源匹配,则返回。true
hasTarget(Object target)
如果转换目标与给定目标匹配,则返回。true
了解安全性
本节提供有关安全性如何在 状态机。你可能真的不需要知道,但它是 总是最好保持透明,而不是隐藏所有的魔力 发生在幕后。
只有当 Spring 状态机在围墙中运行时,安全性才有意义 用户无法直接访问应用程序的花园,因此可以 修改 Spring 安全性在本地线程中的保留。 如果用户控制JVM,那么实际上没有安全性 完全。SecurityContext
安全性的集成点是使用 StateMachineInterceptor 创建的,然后自动将其添加到 状态机(如果启用了安全性)。特定的类是 ,它截获事件和 转换。然后,此拦截器会咨询 Spring 安全性,以确定是否可以发送事件或是否可以进行转换 执行。实际上,如果决定或投票导致异常,则事件或转换将被拒绝。StateMachineSecurityInterceptorAccessDecisionManagerAccessDecisionManager
由于Spring Security的工作方式,我们 每个受保护对象需要一个实例。这就是为什么有 是事件和转换的不同管理器。在这种情况下,事件 转换是我们保护的不同类对象。AccessDecisionManager
默认情况下,对于事件,投票者 (、 和 ) 将添加到 .EventExpressionVoterEventVoterRoleVoterAccessDecisionManager
默认情况下,对于转换,投票者 (、 和 ) 将添加到 .TransitionExpressionVoterTransitionVoterRoleVoterAccessDecisionManager
状态机错误处理
如果状态机在状态转换期间检测到内部错误 逻辑,它可能会引发异常。在处理此异常之前 在内部,您有机会拦截。
通常,您可以使用 拦截错误和 以下清单显示了一个示例: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 }}以下示例显示了一个通用检查:ApplicationListenerStateMachineEvent
public class GenericApplicationEventListener implements ApplicationListener<StateMachineEvent> { @Override public void onApplicationEvent(StateMachineEvent event) { if (event instanceof OnStateMachineError) { // do something with error } }}您也可以直接定义为 仅识别实例,如以下示例所示:ApplicationListenerStateMachineEvent
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");}进入/退出操作中的错误不会阻止转换的发生。