首页 > 其他分享 >Spring Statemachine状态机的概念(二)

Spring Statemachine状态机的概念(二)

时间:2022-12-20 12:01:28浏览次数:65  
标签:Statemachine 示例 Spring void class 状态机 Override public

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

使用作用域

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

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

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

@Configuration
public 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​

@Controller
public 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。

使用操作

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

@Override
public 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​

@Bean
public Action<States, Events> action1() {
return new Action<States, Events>() {

@Override
public void execute(StateContext<States, Events> context) {
}
};
}

@Bean
public BaseAction action2() {
return new BaseAction();
}

@Bean
public 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​

@Override
public 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​

@Bean
public Guard<States, Events> guard1() {
return new Guard<States, Events>() {

@Override
public boolean evaluate(StateContext<States, Events> context) {
return true;
}
};
}

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

@Autowired
StateMachine<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
@EnableStateMachine
public 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) {
}
}

@Configuration
public class ListenerConfig {

@Bean
public StateMachineApplicationEventListener contextListener() {
return new StateMachineApplicationEventListener();
}
}

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

@Configuration
@EnableStateMachine
public 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​

@WithStateMachine
public 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​

@WithStateMachine
public class Bean3 {

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

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

@WithStateMachine
public 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​

@WithStateMachine
public class Bean5 {

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

@OnTransition
public void anyTransition() {
}
}

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

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

@WithStateMachine
public 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)
@OnTransition
public @interface StatesOnTransition {

States[] source() default {};

States[] target() default {};
}

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

@WithStateMachine
public class Bean7 {

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

状态注释

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

@WithStateMachine
public class Bean8 {

@OnStateChanged
public void anyStateChange() {
}
}

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

@WithStateMachine
public class Bean9 {

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

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@OnStateChanged
public @interface StatesOnStates {

States[] source() default {};

States[] target() default {};
}
@WithStateMachine
public class Bean10 {

@StatesOnStates(source = States.S1, target = States.S2)
public void fromS1ToS2() {
}
}

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

@WithStateMachine
public class Bean11 {

@OnStateEntry
public void anyStateEntry() {
}

@OnStateExit
public void anyStateExit() {
}
}

事件注释

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

@WithStateMachine
public class Bean12 {

@OnEventNotAccepted
public void anyEventNotAccepted() {
}

@OnEventNotAccepted(event = "E1")
public void e1EventNotAccepted() {
}
}

状态机注释

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

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

@WithStateMachine
public class Bean13 {

@OnStateMachineStart
public void onStateMachineStart() {
}

@OnStateMachineStop
public void onStateMachineStop() {
}
}

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

@WithStateMachine
public class Bean14 {

@OnStateMachineError
public void onStateMachineError() {
}
}

扩展状态注释

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

@WithStateMachine
public 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
@EnableStateMachine
static 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
@EnableStateMachine
static 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
@EnableStateMachine
static 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
@EnableStateMachine
static 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
@EnableStateMachine
static 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
@EnableStateMachine
static 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

@Autowired
private StateMachine<String, String> machine;

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

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

标签:Statemachine,示例,Spring,void,class,状态机,Override,public
From: https://blog.51cto.com/u_15326439/5954691

相关文章