使用作用域
状态机对作用域的支持非常有限,但您可以 通过以下两种方式之一使用普通的 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();
}
}
在作用域中使用状态机需要仔细规划, 主要是因为它是一个相对较重的组件。 |
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
|
带有操作的 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>> {
}
内部旧接口包裹着一个可运行的反应堆单声道,因为它 共享相同的返回类型。我们无法控制您用这种方法做什么! |
使用防护装置
如要记住的事情中所示,和 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;
}
}
|
带防护装置的 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>> {
}
内部旧接口包装有反应器单声道函数。我们没有 控制你在这种方法中做什么! |
使用扩展状态
假设您需要创建一个状态机来跟踪 很多时候,用户按下键盘上的键,然后终止 当按键被按下 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
- 当前或(或其 ,如果已知)。
Message
Event
MessageHeaders
- 状态机的 .
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
消息标头通常会传递,直到计算机运行到 特定事件的完成。例如,如果事件导致 转换为具有匿名转换的状态 状态 ,原始事件可用于状态 中的操作或守卫。 |
也可以发送消息,而不是仅发送 一个带有 .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
在幕后,计时器是简单的触发器,可能会导致 过渡发生。使用保留定义过渡 仅当源状态处于活动状态时,触发才会触发并导致转换。 过渡有点不同,因为它 仅在实际进入源状态的延迟后触发。 |
如果您希望延迟后发生某些事情,请使用 正好在进入状态时一次。 |
侦听状态机事件
在某些用例中,您想知道发生了什么 状态机,对某事做出反应,或获取日志记录详细信息 调试目的。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机器是 不知道调用方法的处理程序。 |
方法参数
每个注释都支持完全相同的一组可能的方法 参数,但运行时行为会有所不同,具体取决于 批注本身和调用批注方法的阶段。自 更好地了解上下文的工作原理,请参阅使用状态上下文。
实际上,所有带注释的方法都是使用 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) {
}
}
您可以使用 来获取所有事件标头,而不是使用 ,它可以绑定到单个标头。 |
过渡批注
过渡的注释是 、 和。@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
表达 | 描述 |
| 如果当前主体具有指定的角色,则返回。由 默认值,如果提供的角色不以 开头,则为 添加。您可以通过修改 on . |
| 如果当前主体具有任何提供的 角色(以逗号分隔的字符串列表形式给出)。默认情况下,如果每个 提供的角色不是以 开头的,而是添加的。您可以自定义此 通过修改 on . |
| 如果当前主体具有指定的权限,则返回。 |
| 如果当前主体具有任何提供的 角色(以逗号分隔的字符串列表形式给出)。 |
| 允许直接访问表示 当前用户。 |
| 允许直接访问获得的当前对象 从 . |
| 始终计算结果为 。 |
| 始终计算结果为 。 |
| 如果当前主体是匿名用户,则返回。 |
| 如果当前主体是“记住我”用户,则返回。 |
| 如果用户不是匿名的,则返回。 |
| 如果用户不是匿名用户或记住我用户,则返回。 |
| 如果用户有权访问提供的目标,则返回 授予权限 — 例如,. |
| 如果用户有权访问提供的目标,则返回 授予权限 — 例如,. |
事件属性
可以使用前缀 来匹配事件 ID。例如,匹配 事件将与 的属性匹配。EVENT_
A
EVENT_A
事件表达式
事件的表达式根对象的基类是 。它提供对对象的访问,该对象随事件一起传递。 只有一种方法,下表描述了该方法:EventSecurityExpressionRoot
Message
EventSecurityExpressionRoot
Table 2. Event expressions
表达 | 描述 |
| 如果事件与给定事件匹配,则返回。 |
过渡属性
匹配转换源和目标时,可以分别使用 和 前缀。TRANSITION_SOURCE_
TRANSITION_TARGET_
过渡表达式
用于转换的表达式根对象的基类是 。它提供对对象的访问,该对象被传递以进行过渡更改。 有两种方法,分别是 表描述:TransitionSecurityExpressionRoot
Transition
TransitionSecurityExpressionRoot
Table 3. Transition expressions
表达 | 描述 |
| 如果转换源与给定源匹配,则返回。 |
| 如果转换目标与给定目标匹配,则返回。 |
了解安全性
本节提供有关安全性如何在 状态机。你可能真的不需要知道,但它是 总是最好保持透明,而不是隐藏所有的魔力 发生在幕后。
只有当 Spring 状态机在围墙中运行时,安全性才有意义 用户无法直接访问应用程序的花园,因此可以 修改 Spring 安全性在本地线程中的保留。 如果用户控制JVM,那么实际上没有安全性 完全。 |
安全性的集成点是使用 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");
}
进入/退出操作中的错误不会阻止转换的发生。 |