State Machine Examples

This part of the reference documentation explains the use of state machines together with sample code and UML state charts. We use a few shortcuts when representing the relationship between a state chart, Spring Statemachine configuration, and what an application does with a state machine. For complete examples, you should study the samples repository.spring-doc.cn

Samples are built directly from a main source distribution during a normal build cycle. This chapter includes the following samples:spring-doc.cn

The following listing shows how to build the samples:spring-doc.cn

./gradlew clean build -x test

Every sample is located in its own directory under spring-statemachine-samples. The samples are based on Spring Boot and Spring Shell, and you can find the usual Boot fat jars under every sample project’s build/libs directory.spring-doc.cn

The filenames for the jars to which we refer in this section are populated during a build of this document, meaning that, if you build samples from main, you have files with a BUILD-SNAPSHOT postfix.

38. Turnstile

Turnstile is a simple device that gives you access if payment is made. It is a concept that is simple to model using a state machine. In its simplest, form there are only two states: LOCKED and UNLOCKED. Two events, COIN and PUSH can happen, depending on whether someone makes a payment or tries to go through the turnstile. The following image shows the state machine:spring-doc.cn

statechart1

The following listing shows the enumeration that defines the possible states:spring-doc.cn

States
public enum States {
    LOCKED, UNLOCKED
}

The following listing shows the enumeration that defines the events:spring-doc.cn

Events
public enum Events {
    COIN, PUSH
}

The following listing shows the code that configures the state machine:spring-doc.cn

Configuration
@Configuration
@EnableStateMachine
static class StateMachineConfig
		extends EnumStateMachineConfigurerAdapter<States, Events> {

	@Override
	public void configure(StateMachineStateConfigurer<States, Events> states)
			throws Exception {
		states
			.withStates()
				.initial(States.LOCKED)
				.states(EnumSet.allOf(States.class));
	}

	@Override
	public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
			throws Exception {
		transitions
			.withExternal()
				.source(States.LOCKED)
				.target(States.UNLOCKED)
				.event(Events.COIN)
				.and()
			.withExternal()
				.source(States.UNLOCKED)
				.target(States.LOCKED)
				.event(Events.PUSH);
	}

}

You can see how this sample state machine interacts with events by running the turnstile sample. The following listing shows how to do so and shows the command’s output:spring-doc.cn

$ java -jar spring-statemachine-samples-turnstile-4.0.1-SNAPSHOT.jar

sm>sm print
+----------------------------------------------------------------+
|                              SM                                |
+----------------------------------------------------------------+
|                                                                |
|         +----------------+          +----------------+         |
|     *-->|     LOCKED     |          |    UNLOCKED    |         |
|         +----------------+          +----------------+         |
|     +---| entry/         |          | entry/         |---+     |
|     |   | exit/          |          | exit/          |   |     |
|     |   |                |          |                |   |     |
| PUSH|   |                |---COIN-->|                |   |COIN |
|     |   |                |          |                |   |     |
|     |   |                |          |                |   |     |
|     |   |                |<--PUSH---|                |   |     |
|     +-->|                |          |                |<--+     |
|         |                |          |                |         |
|         +----------------+          +----------------+         |
|                                                                |
+----------------------------------------------------------------+

sm>sm start
State changed to LOCKED
State machine started

sm>sm event COIN
State changed to UNLOCKED
Event COIN send

sm>sm event PUSH
State changed to LOCKED
Event PUSH send

39. Turnstile Reactive

Turnstile reactive is an enhacement to Turnstile sample using same StateMachine concept and adding a reactive web layer communicating reactively with a StateMachine reactive interfaces.spring-doc.cn

StateMachineController is a simple @RestController where we autowire our StateMachine.spring-doc.cn

@Autowired
private StateMachine<States, Events> stateMachine;

We create first mapping to return a machine state. As state doesn’t come out from a machine reactively, we can defer it so that when a returned Mono is subscribed, actual state is requested.spring-doc.cn

@GetMapping("/state")
public Mono<States> state() {
	return Mono.defer(() -> Mono.justOrEmpty(stateMachine.getState().getId()));
}

To send a single event or multiple events to a machine we can use a Flux in both incoming and outgoing layers. EventResult here is just for this sample and simply wraps ResultType and event.spring-doc.cn

@PostMapping("/events")
public Flux<EventResult> events(@RequestBody Flux<EventData> eventData) {
	return eventData
		.filter(ed -> ed.getEvent() != null)
		.map(ed -> MessageBuilder.withPayload(ed.getEvent()).build())
		.flatMap(m -> stateMachine.sendEvent(Mono.just(m)))
		.map(EventResult::new);
}

You can use the following command to run the sample:spring-doc.cn

$ java -jar spring-statemachine-samples-turnstilereactive-4.0.1-SNAPSHOT.jar

Example of getting a state:spring-doc.cn

GET http://localhost:8080/state

Would then response:spring-doc.cn

"LOCKED"

Example of sending an event:spring-doc.cn

POST http://localhost:8080/events
content-type: application/json

{
    "event": "COIN"
}

Would then response:spring-doc.cn

[
  {
    "event": "COIN",
    "resultType": "ACCEPTED"
  }
]

You can post multiple events:spring-doc.cn

POST http://localhost:8080/events
content-type: application/json

[
    {
        "event": "COIN"
    },
    {
        "event": "PUSH"
    }
]

Response then contains results for both events:spring-doc.cn

[
  {
    "event": "COIN",
    "resultType": "ACCEPTED"
  },
  {
    "event": "PUSH",
    "resultType": "ACCEPTED"
  }
]

40. Showcase

Showcase is a complex state machine that shows all possible transition topologies up to four levels of state nesting. The following image shows the state machine:spring-doc.cn

statechart2

The following listing shows the enumeration that defines the possible states:spring-doc.cn

States
public enum States {
    S0, S1, S11, S12, S2, S21, S211, S212
}

The following listing shows the enumeration that defines the events:spring-doc.cn

Events
public enum Events {
    A, B, C, D, E, F, G, H, I
}

The following listing shows the code that configures the state machine:spring-doc.cn

Configuration - states
@Override
public void configure(StateMachineStateConfigurer<States, Events> states)
		throws Exception {
	states
		.withStates()
			.initial(States.S0, fooAction())
			.state(States.S0)
			.and()
			.withStates()
				.parent(States.S0)
				.initial(States.S1)
				.state(States.S1)
				.and()
				.withStates()
					.parent(States.S1)
					.initial(States.S11)
					.state(States.S11)
					.state(States.S12)
					.and()
			.withStates()
				.parent(States.S0)
				.state(States.S2)
				.and()
				.withStates()
					.parent(States.S2)
					.initial(States.S21)
					.state(States.S21)
					.and()
					.withStates()
						.parent(States.S21)
						.initial(States.S211)
						.state(States.S211)
						.state(States.S212);
}

The following listing shows the code that configures the state machine’s transitions:spring-doc.cn

Configuration - transitions
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
		throws Exception {
	transitions
		.withExternal()
			.source(States.S1).target(States.S1).event(Events.A)
			.guard(foo1Guard())
			.and()
		.withExternal()
			.source(States.S1).target(States.S11).event(Events.B)
			.and()
		.withExternal()
			.source(States.S21).target(States.S211).event(Events.B)
			.and()
		.withExternal()
			.source(States.S1).target(States.S2).event(Events.C)
			.and()
		.withExternal()
			.source(States.S2).target(States.S1).event(Events.C)
			.and()
		.withExternal()
			.source(States.S1).target(States.S0).event(Events.D)
			.and()
		.withExternal()
			.source(States.S211).target(States.S21).event(Events.D)
			.and()
		.withExternal()
			.source(States.S0).target(States.S211).event(Events.E)
			.and()
		.withExternal()
			.source(States.S1).target(States.S211).event(Events.F)
			.and()
		.withExternal()
			.source(States.S2).target(States.S11).event(Events.F)
			.and()
		.withExternal()
			.source(States.S11).target(States.S211).event(Events.G)
			.and()
		.withExternal()
			.source(States.S211).target(States.S0).event(Events.G)
			.and()
		.withInternal()
			.source(States.S0).event(Events.H)
			.guard(foo0Guard())
			.action(fooAction())
			.and()
		.withInternal()
			.source(States.S2).event(Events.H)
			.guard(foo1Guard())
			.action(fooAction())
			.and()
		.withInternal()
			.source(States.S1).event(Events.H)
			.and()
		.withExternal()
			.source(States.S11).target(States.S12).event(Events.I)
			.and()
		.withExternal()
			.source(States.S211).target(States.S212).event(Events.I)
			.and()
		.withExternal()
			.source(States.S12).target(States.S212).event(Events.I);

}

The following listing shows the code that configures the state machine’s actions and guards:spring-doc.cn

Configuration - actions and guards
@Bean
public FooGuard foo0Guard() {
	return new FooGuard(0);
}

@Bean
public FooGuard foo1Guard() {
	return new FooGuard(1);
}

@Bean
public FooAction fooAction() {
	return new FooAction();
}

The following listing shows how the single action is defined:spring-doc.cn

Action
private static class FooAction implements Action<States, Events> {

	@Override
	public void execute(StateContext<States, Events> context) {
		Map<Object, Object> variables = context.getExtendedState().getVariables();
		Integer foo = context.getExtendedState().get("foo", Integer.class);
		if (foo == null) {
			log.info("Init foo to 0");
			variables.put("foo", 0);
		} else if (foo == 0) {
			log.info("Switch foo to 1");
			variables.put("foo", 1);
		} else if (foo == 1) {
			log.info("Switch foo to 0");
			variables.put("foo", 0);
		}
	}
}

The following listing shows how the single guard is defined:spring-doc.cn

Guard
private static class FooGuard implements Guard<States, Events> {

	private final int match;

	public FooGuard(int match) {
		this.match = match;
	}

	@Override
	public boolean evaluate(StateContext<States, Events> context) {
		Object foo = context.getExtendedState().getVariables().get("foo");
		return !(foo == null || !foo.equals(match));
	}
}

The following listing shows the output that this state machine produces when it runs and various events are sent to it:spring-doc.cn

sm>sm start
Init foo to 0
Entry state S0
Entry state S1
Entry state S11
State machine started

sm>sm event A
Event A send

sm>sm event C
Exit state S11
Exit state S1
Entry state S2
Entry state S21
Entry state S211
Event C send

sm>sm event H
Switch foo to 1
Internal transition source=S0
Event H send

sm>sm event C
Exit state S211
Exit state S21
Exit state S2
Entry state S1
Entry state S11
Event C send

sm>sm event A
Exit state S11
Exit state S1
Entry state S1
Entry state S11
Event A send

In the preceding output, we can see that:spring-doc.cn

  • The state machine is started, which takes it to its initial state (S11) through superstates (S1) and (S0). Also, the extended state variable, foo, is initialized to 0.spring-doc.cn

  • We try to execute a self transition in state S1 with event A, but nothing happens because the transition is guarded by variable foo to be 1.spring-doc.cn

  • We send event C, which takes us to the other state machine, where the initial state (S211) and its superstates are entered. In there, we can use event H, which does a simple internal transition to flip the foo variable. Then we go back by using event C.spring-doc.cn

  • Event A is sent again, and now S1 does a self transition because the guard evaluates to true.spring-doc.cn

The following example offers a closer look at how hierarchical states and their event handling works:spring-doc.cn

sm>sm variables
No variables

sm>sm start
Init foo to 0
Entry state S0
Entry state S1
Entry state S11
State machine started

sm>sm variables
foo=0

sm>sm event H
Internal transition source=S1
Event H send

sm>sm variables
foo=0

sm>sm event C
Exit state S11
Exit state S1
Entry state S2
Entry state S21
Entry state S211
Event C send

sm>sm variables
foo=0

sm>sm event H
Switch foo to 1
Internal transition source=S0
Event H send

sm>sm variables
foo=1

sm>sm event H
Switch foo to 0
Internal transition source=S2
Event H send

sm>sm variables
foo=0

In the preceding sample:spring-doc.cn

  • We print extended state variables in various stages.spring-doc.cn

  • With event H, we end up running an internal transition, which is logged with its source state.spring-doc.cn

  • Note how event H is handled in different states (S0, S1, and S2). This is a good example of how hierarchical states and their event handling works. If state S2 is unable to handle event H due to a guard condition, its parent is checked next. This guarantees that, while the machine is on state S2, the foo flag is always flipped around. However, in state S1, event H always matches to its dummy transition without guard or action, so it never happens.spring-doc.cn

41. CD Player

CD Player is a sample which resembles a use case that many people have used in the real world. CD Player itself is a really simple entity that allows a user to open a deck, insert or change a disk, and then drive the player’s functionality by pressing various buttons (eject, play, stop, pause, rewind, and backward).spring-doc.cn

How many of us have really given thought to what it will take to make code that interacts with hardware to drive a CD Player. Yes, the concept of a player is simple, but, if you look behind the scenes, things actually get a bit convoluted.spring-doc.cn

You have probably noticed that, if your deck is open and you press play, the deck closes and a song starts to play (if a CD was inserted). In a sense, when the deck is open, you first need to close it and then try to start playing (again, if a CD is actually inserted). Hopefully, you have now realized that a simple CD Player is so simple. Sure, you can wrap all this with a simple class that has a few boolean variables and probably a few nested if-else clauses. That will do the job, but what about if you need to make all this behavior much more complex? Do you really want to keep adding more flags and if-else clauses?spring-doc.cn

The following image shows the state machine for our simple CD player:spring-doc.cn

statechart3

The rest of this section goes through how this sample and its state machine is designed and how those two interacts with each other. The following three configuration sections are used within an EnumStateMachineConfigurerAdapter.spring-doc.cn

@Override
public void configure(StateMachineStateConfigurer<States, Events> states)
		throws Exception {
	states
		.withStates()
			.initial(States.IDLE)
			.state(States.IDLE)
			.and()
			.withStates()
				.parent(States.IDLE)
				.initial(States.CLOSED)
				.state(States.CLOSED, closedEntryAction(), null)
				.state(States.OPEN)
				.and()
		.withStates()
			.state(States.BUSY)
			.and()
			.withStates()
				.parent(States.BUSY)
				.initial(States.PLAYING)
				.state(States.PLAYING)
				.state(States.PAUSED);

}
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
		throws Exception {
	transitions
		.withExternal()
			.source(States.CLOSED).target(States.OPEN).event(Events.EJECT)
			.and()
		.withExternal()
			.source(States.OPEN).target(States.CLOSED).event(Events.EJECT)
			.and()
		.withExternal()
			.source(States.OPEN).target(States.CLOSED).event(Events.PLAY)
			.and()
		.withExternal()
			.source(States.PLAYING).target(States.PAUSED).event(Events.PAUSE)
			.and()
		.withInternal()
			.source(States.PLAYING)
			.action(playingAction())
			.timer(1000)
			.and()
		.withInternal()
			.source(States.PLAYING).event(Events.BACK)
			.action(trackAction())
			.and()
		.withInternal()
			.source(States.PLAYING).event(Events.FORWARD)
			.action(trackAction())
			.and()
		.withExternal()
			.source(States.PAUSED).target(States.PLAYING).event(Events.PAUSE)
			.and()
		.withExternal()
			.source(States.BUSY).target(States.IDLE).event(Events.STOP)
			.and()
		.withExternal()
			.source(States.IDLE).target(States.BUSY).event(Events.PLAY)
			.action(playAction())
			.guard(playGuard())
			.and()
		.withInternal()
			.source(States.OPEN).event(Events.LOAD).action(loadAction());
}
@Bean
public ClosedEntryAction closedEntryAction() {
	return new ClosedEntryAction();
}

@Bean
public LoadAction loadAction() {
	return new LoadAction();
}

@Bean
public TrackAction trackAction() {
	return new TrackAction();
}

@Bean
public PlayAction playAction() {
	return new PlayAction();
}

@Bean
public PlayingAction playingAction() {
	return new PlayingAction();
}

@Bean
public PlayGuard playGuard() {
	return new PlayGuard();
}

In the preceding configuration:spring-doc.cn

  • We used EnumStateMachineConfigurerAdapter to configure states and transitions.spring-doc.cn

  • The CLOSED and OPEN states are defined as substates of IDLE, and the PLAYING and PAUSED states are defined as substates of BUSY.spring-doc.cn

  • With the CLOSED state, we added an entry action as a bean called closedEntryAction.spring-doc.cn

  • In the transitions we mostly map events to expected state transitions, such as EJECT closing and opening a deck and PLAY, STOP, and PAUSE doing their natural transitions. For other transitions, we did the following:spring-doc.cn

    • For source state PLAYING, we added a timer trigger, which is needed to automatically track elapsed time within a playing track and to have a facility for making the decision about when to switch the to next track.spring-doc.cn

    • For the PLAY event, if the source state is IDLE and the target state is BUSY, we defined an action called playAction and a guard called playGuard.spring-doc.cn

    • For the LOAD event and the OPEN state, we defined an internal transition with an action called loadAction, which tracks inserting a disc with extended-state variables.spring-doc.cn

    • The PLAYING state defines three internal transitions. One is triggered by a timer that runs an action called playingAction, which updates the extended state variables. The other two transitions use trackAction with different events (BACK and FORWARD, respectively) to handle when the user wants to go back or forward in tracks.spring-doc.cn

This machine has only have six states, which are defined by the following enumeration:spring-doc.cn

public enum States {
	// super state of PLAYING and PAUSED
    BUSY,
    PLAYING,
    PAUSED,
	// super state of CLOSED and OPEN
    IDLE,
    CLOSED,
    OPEN
}

Events represent the buttons the user can press and whether the user loads a disc into the player. The following enumeration defines the events:spring-doc.cn

public enum Events {
    PLAY, STOP, PAUSE, EJECT, LOAD, FORWARD, BACK
}

The cdPlayer and library beans are used to drive the application. The following listing shows the definition of these two beans:spring-doc.cn

@Bean
public CdPlayer cdPlayer() {
	return new CdPlayer();
}

@Bean
public Library library() {
	return Library.buildSampleLibrary();
}

We define extended state variable keys as simple enumerations, as the following listing shows:spring-doc.cn

public enum Variables {
	CD, TRACK, ELAPSEDTIME
}

public enum Headers {
	TRACKSHIFT
}

We wanted to make this sample type safe, so we define our own annotation (@StatesOnTransition), which has a mandatory meta annotation (@OnTransition). The following listing defines the @StatesOnTransition annotation:spring-doc.cn

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@OnTransition
public @interface StatesOnTransition {

	States[] source() default {};

	States[] target() default {};

}

ClosedEntryAction is an entry action for the CLOSED state, to send a PLAY event to the state machine if a disc is present. The following listing defines ClosedEntryAction:spring-doc.cn

public static class ClosedEntryAction implements Action<States, Events> {

	@Override
	public void execute(StateContext<States, Events> context) {
		if (context.getTransition() != null
				&& context.getEvent() == Events.PLAY
				&& context.getTransition().getTarget().getId() == States.CLOSED
				&& context.getExtendedState().getVariables().get(Variables.CD) != null) {
			context.getStateMachine()
				.sendEvent(Mono.just(MessageBuilder
					.withPayload(Events.PLAY).build()))
				.subscribe();
		}
	}
}

LoadAction update an extended state variable if event headers contain information about a disc to load. The following listing defines LoadAction:spring-doc.cn

public static class LoadAction implements Action<States, Events> {

	@Override
	public void execute(StateContext<States, Events> context) {
		Object cd = context.getMessageHeader(Variables.CD);
		context.getExtendedState().getVariables().put(Variables.CD, cd);
	}
}

PlayAction resets the player’s elapsed time, which is kept as an extended state variable. The following listing defines PlayAction:spring-doc.cn

public static class PlayAction implements Action<States, Events> {

	@Override
	public void execute(StateContext<States, Events> context) {
		context.getExtendedState().getVariables().put(Variables.ELAPSEDTIME, 0l);
		context.getExtendedState().getVariables().put(Variables.TRACK, 0);
	}
}

PlayGuard guards the transition from IDLE to BUSY with the PLAY event if the CD extended state variable does not indicate that a disc has been loaded. The following listing defines PlayGuard:spring-doc.cn

public static class PlayGuard implements Guard<States, Events> {

	@Override
	public boolean evaluate(StateContext<States, Events> context) {
		ExtendedState extendedState = context.getExtendedState();
		return extendedState.getVariables().get(Variables.CD) != null;
	}
}

PlayingAction updates an extended state variable called ELAPSEDTIME, which the player can use to read and update its LCD status display. PlayingAction also handles track shifting when the user goe back or forward in tracks. The following example defines PlayingAction:spring-doc.cn

public static class PlayingAction implements Action<States, Events> {

	@Override
	public void execute(StateContext<States, Events> context) {
		Map<Object, Object> variables = context.getExtendedState().getVariables();
		Object elapsed = variables.get(Variables.ELAPSEDTIME);
		Object cd = variables.get(Variables.CD);
		Object track = variables.get(Variables.TRACK);
		if (elapsed instanceof Long) {
			long e = ((Long)elapsed) + 1000l;
			if (e > ((Cd) cd).getTracks()[((Integer) track)].getLength()*1000) {
				context.getStateMachine()
					.sendEvent(Mono.just(MessageBuilder
						.withPayload(Events.FORWARD)
						.setHeader(Headers.TRACKSHIFT.toString(), 1).build()))
					.subscribe();
			} else {
				variables.put(Variables.ELAPSEDTIME, e);
			}
		}
	}
}

TrackAction handles track shift actions when the user goes back or forward in tracks. If a track is the last on a disc, playing is stopped and the STOP event is sent to a state machine. The following example defines TrackAction:spring-doc.cn

public static class TrackAction implements Action<States, Events> {

	@Override
	public void execute(StateContext<States, Events> context) {
		Map<Object, Object> variables = context.getExtendedState().getVariables();
		Object trackshift = context.getMessageHeader(Headers.TRACKSHIFT.toString());
		Object track = variables.get(Variables.TRACK);
		Object cd = variables.get(Variables.CD);
		if (trackshift instanceof Integer && track instanceof Integer && cd instanceof Cd) {
			int next = ((Integer)track) + ((Integer)trackshift);
			if (next >= 0 &&  ((Cd)cd).getTracks().length > next) {
				variables.put(Variables.ELAPSEDTIME, 0l);
				variables.put(Variables.TRACK, next);
			} else if (((Cd)cd).getTracks().length <= next) {
				context.getStateMachine()
					.sendEvent(Mono.just(MessageBuilder
						.withPayload(Events.STOP).build()))
					.subscribe();
			}
		}
	}
}

One other important aspect of state machines is that they have their own responsibilities (mostly around handling states) and that all application level logic should be kept outside. This means that applications need to have a ways to interact with a state machine. Also, note that we annotated CdPlayer with @WithStateMachine, which instructs a state machine to find methods from your POJO, which are then called with various transitions. The following example shows how it updates its LCD status display:spring-doc.cn

@OnTransition(target = "BUSY")
public void busy(ExtendedState extendedState) {
	Object cd = extendedState.getVariables().get(Variables.CD);
	if (cd != null) {
		cdStatus = ((Cd)cd).getName();
	}
}

In the preceding example, we use the @OnTransition annotation to hook a callback when a transition happens with a target state of BUSY.spring-doc.cn

The following listing shows how our state machine handles whether the player is closed:spring-doc.cn

@StatesOnTransition(target = {States.CLOSED, States.IDLE})
public void closed(ExtendedState extendedState) {
	Object cd = extendedState.getVariables().get(Variables.CD);
	if (cd != null) {
		cdStatus = ((Cd)cd).getName();
	} else {
		cdStatus = "No CD";
	}
	trackStatus = "";
}

@OnTransition (which we used in the preceding examples) can only be used with strings that are matched from enumerations. @StatesOnTransition lets you create your own type-safe annotations that use real enumerations.spring-doc.cn

The following example shows how this state machine actually works.spring-doc.cn

sm>sm start
Entry state IDLE
Entry state CLOSED
State machine started

sm>cd lcd
No CD

sm>cd library
0: Greatest Hits
  0: Bohemian Rhapsody  05:56
  1: Another One Bites the Dust  03:36
1: Greatest Hits II
  0: A Kind of Magic  04:22
  1: Under Pressure  04:08

sm>cd eject
Exit state CLOSED
Entry state OPEN

sm>cd load 0
Loading cd Greatest Hits

sm>cd play
Exit state OPEN
Entry state CLOSED
Exit state CLOSED
Exit state IDLE
Entry state BUSY
Entry state PLAYING

sm>cd lcd
Greatest Hits Bohemian Rhapsody 00:03

sm>cd forward

sm>cd lcd
Greatest Hits Another One Bites the Dust 00:04

sm>cd stop
Exit state PLAYING
Exit state BUSY
Entry state IDLE
Entry state CLOSED

sm>cd lcd
Greatest Hits

In the preceding run:spring-doc.cn

42. Tasks

The Tasks sample demonstrates parallel task handling within regions and adds error handling to either automatically or manually fix task problems before continuing back to a state where the tasks can be run again. The following image shows the Tasks state machine:spring-doc.cn

statechart5

On a high level, in this state machine:spring-doc.cn

  • We always try to get into the READY state so that we can use the RUN event to execute tasks.spring-doc.cn

  • The TASKS state, which is composed of three independent regions, has been put in the middle of FORK and JOIN states, which will cause the regions to go into their initial states and to be joined by their end states.spring-doc.cn

  • From the JOIN state, we automatically go into a CHOICE state, which checks for the existence of error flags in extended state variables. Tasks can set these flags, and doing so gives the CHOICE state the ability to go into the ERROR state, where errors can be handled either automatically or manually.spring-doc.cn

  • The AUTOMATIC state in ERROR can try to automatically fix an error and goes back to READY if it succeeds. If the error is something what cannot be handled automatically, user intervention is needed and the machine is put into the MANUAL state by the FALLBACK event.spring-doc.cn

The following listing shows the enumeration that defines the possible states:spring-doc.cn

States
public enum States {
    READY,
    FORK, JOIN, CHOICE,
    TASKS, T1, T1E, T2, T2E, T3, T3E,
    ERROR, AUTOMATIC, MANUAL
}

The following listing shows the enumeration that defines the events:spring-doc.cn

Events
public enum Events {
    RUN, FALLBACK, CONTINUE, FIX;
}

The following listing configures the possible states:spring-doc.cn

Configuration - states
@Override
public void configure(StateMachineStateConfigurer<States, Events> states)
		throws Exception {
	states
		.withStates()
			.initial(States.READY)
			.fork(States.FORK)
			.state(States.TASKS)
			.join(States.JOIN)
			.choice(States.CHOICE)
			.state(States.ERROR)
			.and()
			.withStates()
				.parent(States.TASKS)
				.initial(States.T1)
				.end(States.T1E)
				.and()
			.withStates()
				.parent(States.TASKS)
				.initial(States.T2)
				.end(States.T2E)
				.and()
			.withStates()
				.parent(States.TASKS)
				.initial(States.T3)
				.end(States.T3E)
				.and()
			.withStates()
				.parent(States.ERROR)
				.initial(States.AUTOMATIC)
				.state(States.AUTOMATIC, automaticAction(), null)
				.state(States.MANUAL);
}

The following listing configures the possible transitions:spring-doc.cn

Configuration - transitions
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
		throws Exception {
	transitions
		.withExternal()
			.source(States.READY).target(States.FORK)
			.event(Events.RUN)
			.and()
		.withFork()
			.source(States.FORK).target(States.TASKS)
			.and()
		.withExternal()
			.source(States.T1).target(States.T1E)
			.and()
		.withExternal()
			.source(States.T2).target(States.T2E)
			.and()
		.withExternal()
			.source(States.T3).target(States.T3E)
			.and()
		.withJoin()
			.source(States.TASKS).target(States.JOIN)
			.and()
		.withExternal()
			.source(States.JOIN).target(States.CHOICE)
			.and()
		.withChoice()
			.source(States.CHOICE)
			.first(States.ERROR, tasksChoiceGuard())
			.last(States.READY)
			.and()
		.withExternal()
			.source(States.ERROR).target(States.READY)
			.event(Events.CONTINUE)
			.and()
		.withExternal()
			.source(States.AUTOMATIC).target(States.MANUAL)
			.event(Events.FALLBACK)
			.and()
		.withInternal()
			.source(States.MANUAL)
			.action(fixAction())
			.event(Events.FIX);
}

The following guard sends a choice entry into the ERROR state and needs to return TRUE if an error has happened. This guard checks that all extended state variables(T1, T2, and T3) are TRUE.spring-doc.cn

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

		@Override
		public boolean evaluate(StateContext<States, Events> context) {
			Map<Object, Object> variables = context.getExtendedState().getVariables();
			return !(ObjectUtils.nullSafeEquals(variables.get("T1"), true)
					&& ObjectUtils.nullSafeEquals(variables.get("T2"), true)
					&& ObjectUtils.nullSafeEquals(variables.get("T3"), true));
		}
	};
}

The following actions below send events to the state machine to request the next step, which is either to fall back or to continue back to ready.spring-doc.cn

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

		@Override
		public void execute(StateContext<States, Events> context) {
			Map<Object, Object> variables = context.getExtendedState().getVariables();
			if (ObjectUtils.nullSafeEquals(variables.get("T1"), true)
					&& ObjectUtils.nullSafeEquals(variables.get("T2"), true)
					&& ObjectUtils.nullSafeEquals(variables.get("T3"), true)) {
				context.getStateMachine()
					.sendEvent(Mono.just(MessageBuilder
						.withPayload(Events.CONTINUE).build()))
					.subscribe();
			} else {
				context.getStateMachine()
					.sendEvent(Mono.just(MessageBuilder
						.withPayload(Events.FALLBACK).build()))
					.subscribe();
			}
		}
	};
}

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

		@Override
		public void execute(StateContext<States, Events> context) {
			Map<Object, Object> variables = context.getExtendedState().getVariables();
			variables.put("T1", true);
			variables.put("T2", true);
			variables.put("T3", true);
			context.getStateMachine()
				.sendEvent(Mono.just(MessageBuilder
					.withPayload(Events.CONTINUE).build()))
				.subscribe();
		}
	};
}

Default region execution is synchronous meaning a regions would be processed sequentially. In this sample we simply want all task regions to get processed parallel. This can be accomplished by defining RegionExecutionPolicy:spring-doc.cn

@Override
public void configure(StateMachineConfigurationConfigurer<States, Events> config)
		throws Exception {
	config
		.withConfiguration()
			.regionExecutionPolicy(RegionExecutionPolicy.PARALLEL);
}

The following example shows how this state machine actually works:spring-doc.cn

sm>sm start
State machine started
Entry state READY

sm>tasks run
Exit state READY
Entry state TASKS
run task on T2
run task on T1
run task on T3
run task on T2 done
run task on T1 done
run task on T3 done
Entry state T2
Entry state T1
Entry state T3
Exit state T2
Exit state T1
Exit state T3
Entry state T3E
Entry state T1E
Entry state T2E
Exit state TASKS
Entry state READY

In the preceding listing, we can see that tasks run multiple times. In the next listing, we introduce errors:spring-doc.cn

sm>tasks list
Tasks {T1=true, T3=true, T2=true}

sm>tasks fail T1

sm>tasks list
Tasks {T1=false, T3=true, T2=true}

sm>tasks run
Entry state TASKS
run task on T1
run task on T3
run task on T2
run task on T1 done
run task on T3 done
run task on T2 done
Entry state T1
Entry state T3
Entry state T2
Entry state T1E
Entry state T2E
Entry state T3E
Exit state TASKS
Entry state JOIN
Exit state JOIN
Entry state ERROR
Entry state AUTOMATIC
Exit state AUTOMATIC
Exit state ERROR
Entry state READY

In the preceding listing, if we simulate a failure for task T1, it is fixed automatically. In the next listing, we introduce more errors:spring-doc.cn

sm>tasks list
Tasks {T1=true, T3=true, T2=true}

sm>tasks fail T2

sm>tasks run
Entry state TASKS
run task on T2
run task on T1
run task on T3
run task on T2 done
run task on T1 done
run task on T3 done
Entry state T2
Entry state T1
Entry state T3
Entry state T1E
Entry state T2E
Entry state T3E
Exit state TASKS
Entry state JOIN
Exit state JOIN
Entry state ERROR
Entry state AUTOMATIC
Exit state AUTOMATIC
Entry state MANUAL

sm>tasks fix
Exit state MANUAL
Exit state ERROR
Entry state READY

In the precding example, if we simulate failure for either task T2 or T3, the state machine goes to the MANUAL state, where problem needs to be fixed manually before it can go back to the READY state.spring-doc.cn

43. Washer

The washer sample demonstrates how to use a history state to recover a running state configuration with a simulated power-off situation.spring-doc.cn

Anyone who has ever used a washing machine knows that if you somehow pause the program, it continue from the same state when unpaused. You can implement this kind of behavior in a state machine by using a history pseudo state. The following image shows our state machine for a washer:spring-doc.cn

statechart6

The following listing shows the enumeration that defines the possible states:spring-doc.cn

States
public enum States {
    RUNNING, HISTORY, END,
    WASHING, RINSING, DRYING,
    POWEROFF
}

The following listing shows the enumeration that defines the events:spring-doc.cn

Events
public enum Events {
    RINSE, DRY, STOP,
    RESTOREPOWER, CUTPOWER
}

The following listing configures the possible states:spring-doc.cn

Configuration - states
@Override
public void configure(StateMachineStateConfigurer<States, Events> states)
		throws Exception {
	states
		.withStates()
			.initial(States.RUNNING)
			.state(States.POWEROFF)
			.end(States.END)
			.and()
			.withStates()
				.parent(States.RUNNING)
				.initial(States.WASHING)
				.state(States.RINSING)
				.state(States.DRYING)
				.history(States.HISTORY, History.SHALLOW);
}

The following listing configures the possible transitions:spring-doc.cn

Configuration - transitions
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
		throws Exception {
	transitions
		.withExternal()
			.source(States.WASHING).target(States.RINSING)
			.event(Events.RINSE)
			.and()
		.withExternal()
			.source(States.RINSING).target(States.DRYING)
			.event(Events.DRY)
			.and()
		.withExternal()
			.source(States.RUNNING).target(States.POWEROFF)
			.event(Events.CUTPOWER)
			.and()
		.withExternal()
			.source(States.POWEROFF).target(States.HISTORY)
			.event(Events.RESTOREPOWER)
			.and()
		.withExternal()
			.source(States.RUNNING).target(States.END)
			.event(Events.STOP);
}

The following example shows how this state machine actually works:spring-doc.cn

sm>sm start
Entry state RUNNING
Entry state WASHING
State machine started

sm>sm event RINSE
Exit state WASHING
Entry state RINSING
Event RINSE send

sm>sm event DRY
Exit state RINSING
Entry state DRYING
Event DRY send

sm>sm event CUTPOWER
Exit state DRYING
Exit state RUNNING
Entry state POWEROFF
Event CUTPOWER send

sm>sm event RESTOREPOWER
Exit state POWEROFF
Entry state RUNNING
Entry state WASHING
Entry state DRYING
Event RESTOREPOWER send

In the preceding run:spring-doc.cn

  • The state machine is started, which causes machine to get initialized.spring-doc.cn

  • The state machine goes to RINSING state.spring-doc.cn

  • The state machine goes to DRYING state.spring-doc.cn

  • The state machine cuts power and goes to POWEROFF state.spring-doc.cn

  • The state is restored from the HISTORY state, which takes state machine back to its previous known state.spring-doc.cn

44. Persist

Persist is a sample that uses the Persist recipe to demonstrate how database entry update logic can be controlled by a state machine.spring-doc.cn

The following image shows the state machine logic and configuration:spring-doc.cn

statechart10

The following listing shows the state machine configuration:spring-doc.cn

StateMachine Config
@Configuration
@EnableStateMachine
static class StateMachineConfig
		extends StateMachineConfigurerAdapter<String, String> {

	@Override
	public void configure(StateMachineStateConfigurer<String, String> states)
			throws Exception {
		states
			.withStates()
				.initial("PLACED")
				.state("PROCESSING")
				.state("SENT")
				.state("DELIVERED");
	}

	@Override
	public void configure(StateMachineTransitionConfigurer<String, String> transitions)
			throws Exception {
		transitions
			.withExternal()
				.source("PLACED").target("PROCESSING")
				.event("PROCESS")
				.and()
			.withExternal()
				.source("PROCESSING").target("SENT")
				.event("SEND")
				.and()
			.withExternal()
				.source("SENT").target("DELIVERED")
				.event("DELIVER");
	}

}

The following configuration creates PersistStateMachineHandler:spring-doc.cn

Handler Config
@Configuration
static class PersistHandlerConfig {

	@Autowired
	private StateMachine<String, String> stateMachine;

	@Bean
	public Persist persist() {
		return new Persist(persistStateMachineHandler());
	}

	@Bean
	public PersistStateMachineHandler persistStateMachineHandler() {
		return new PersistStateMachineHandler(stateMachine);
	}

}

The following listing shows the Order class used with this sample:spring-doc.cn

Order Class
public static class Order {
	int id;
	String state;

	public Order(int id, String state) {
		this.id = id;
		this.state = state;
	}

	@Override
	public String toString() {
		return "Order [id=" + id + ", state=" + state + "]";
	}

}

The following example shows the state machine’s output:spring-doc.cn

sm>persist db
Order [id=1, state=PLACED]
Order [id=2, state=PROCESSING]
Order [id=3, state=SENT]
Order [id=4, state=DELIVERED]

sm>persist process 1
Exit state PLACED
Entry state PROCESSING

sm>persist db
Order [id=2, state=PROCESSING]
Order [id=3, state=SENT]
Order [id=4, state=DELIVERED]
Order [id=1, state=PROCESSING]

sm>persist deliver 3
Exit state SENT
Entry state DELIVERED

sm>persist db
Order [id=2, state=PROCESSING]
Order [id=4, state=DELIVERED]
Order [id=1, state=PROCESSING]
Order [id=3, state=DELIVERED]

In the preceding run, the state machine:spring-doc.cn

  • Listed rows from an existing embedded database, which is already populated with sample data.spring-doc.cn

  • Requested to update order 1 into the PROCESSING state.spring-doc.cn

  • List database entries again and see that the state has been changed from PLACED to PROCESSING.spring-doc.cn

  • Update order 3 to update its state from SENT to DELIVERED.spring-doc.cn

You may wonder where the database is, because there are literally no signs of it in the sample code. The sample is based on Spring Boot and, because the necessary classes are in a classpath, an embedded HSQL instance is created automatically.spring-doc.cn

Spring Boot even creates an instance of JdbcTemplate, which you can autowire, as we did in Persist.java, shown in the following listing:spring-doc.cn

@Autowired
private JdbcTemplate jdbcTemplate;

Next, we need to handle state changes. The following listing shows how we do so:spring-doc.cn

public void change(int order, String event) {
	Order o = jdbcTemplate.queryForObject("select id, state from orders where id = ?",
			new RowMapper<Order>() {
				public Order mapRow(ResultSet rs, int rowNum) throws SQLException {
					return new Order(rs.getInt("id"), rs.getString("state"));
				}
			}, new Object[] { order });
	handler.handleEventWithStateReactively(MessageBuilder
			.withPayload(event).setHeader("order", order).build(), o.state)
		.subscribe();
}

Finally, we use a PersistStateChangeListener to update the database, as the following listing shows:spring-doc.cn

private class LocalPersistStateChangeListener implements PersistStateChangeListener {

	@Override
	public void onPersist(State<String, String> state, Message<String> message,
			Transition<String, String> transition, StateMachine<String, String> stateMachine) {
		if (message != null && message.getHeaders().containsKey("order")) {
			Integer order = message.getHeaders().get("order", Integer.class);
			jdbcTemplate.update("update orders set state = ? where id = ?", state.getId(), order);
		}
	}
}

45. Zookeeper

Zookeeper is a distributed version from the Turnstile sample.spring-doc.cn

This sample needs an external Zookeeper instance that is accessible from localhost and has the default port and settings.

Configuration of this sample is almost the same as the turnstile sample. We add only the configuration for the distributed state machine where we configure StateMachineEnsemble, as the following listing shows:spring-doc.cn

@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception {
	config
		.withDistributed()
			.ensemble(stateMachineEnsemble());
}

The actual StateMachineEnsemble needs to be created as a bean, together with the CuratorFramework client, as the following example shows:spring-doc.cn

@Bean
public StateMachineEnsemble<String, String> stateMachineEnsemble() throws Exception {
	return new ZookeeperStateMachineEnsemble<String, String>(curatorClient(), "/foo");
}

@Bean
public CuratorFramework curatorClient() throws Exception {
	CuratorFramework client = CuratorFrameworkFactory.builder().defaultData(new byte[0])
			.retryPolicy(new ExponentialBackoffRetry(1000, 3))
			.connectString("localhost:2181").build();
	client.start();
	return client;
}

For the next example, we need to create two different shell instances. We need to create one instance, see what happens, and then create the second instance. The following command starts the shell instances (remember to start only one instance for now):spring-doc.cn

@n1:~# java -jar spring-statemachine-samples-zookeeper-4.0.1-SNAPSHOT.jar

When state machine is started, its initial state is LOCKED. Then it sends a COIN event to transition into UNLOCKED state. The following example shows what happens:spring-doc.cn

Shell1
sm>sm start
Entry state LOCKED
State machine started

sm>sm event COIN
Exit state LOCKED
Entry state UNLOCKED
Event COIN send

sm>sm state
UNLOCKED

Now you can open a second shell instance and start a state machine, by using the same command that you used to start the first state machine. You should see that the distributed state (UNLOCKED) is entered instead of the default initial state (LOCKED).spring-doc.cn

The following example shows the state machine and its output:spring-doc.cn

Shell2
sm>sm start
State machine started

sm>sm state
UNLOCKED

Then from either shell (we use second instance in the next example), send a PUSH event to transit from the UNLOCKED into the LOCKED state. The following example shows the state machine command and its output:spring-doc.cn

Shell2
sm>sm event PUSH
Exit state UNLOCKED
Entry state LOCKED
Event PUSH send

In the other shell (the first shell if you ran the preceding command in the second shell), you should see the state be changed automatically, based on distributed state kept in Zookeeper. The following example shows the state machine command and its output:spring-doc.cn

Shell1
sm>Exit state UNLOCKED
Entry state LOCKED

46. Web

Web is a distributed state machine example that uses a zookeeper state machine to handle distributed state. See Zookeeper.spring-doc.cn

This example is meant to be run on multiple browser sessions against multiple different hosts.

This sample uses a modified state machine structure from Showcase to work with a distributed state machine. The following image shows the state machine logic:spring-doc.cn

statechart11
Due to the nature of this sample, an instance of a Zookeeper state machine is expected to be available from a localhost for every individual sample instance.

This demonstration uses an example that starts three different sample instances. If you run different instances on the same host, you need to distinguish the port each one uses by adding --server.port=<myport> to the command. Otherwise the default port for each host is 8080.spring-doc.cn

In this sample run, we have three hosts: n1, n2, and n3. Each one has a local zookeeper instance running and a state machine sample running on a port 8080.spring-doc.cn

In there different terminals, start the three different state machines by running the following command:spring-doc.cn

# java -jar spring-statemachine-samples-web-4.0.1-SNAPSHOT.jar

When all instances are running, you should see that all show similar information when you access them with a browser. The states should be S0, S1, and S11. The extended state variable named foo should have a value of 0. The main state is S11.spring-doc.cn

sm dist n1 1

When you press the Event C button in any of the browser windows, the distributed state is changed to S211, which is the target state denoted by the transition associated with an event of type C. The following image shows the change:spring-doc.cn

sm dist n2 2

Now we can press the Event H button and see that the internal transition runs on all state machines to change the the value of the extended state variable named foo from 0 to 1. This change is first done on the state machine that receives the event and is then propagated to the other state machines. You should see only the variable named foo change from 0 to 1.spring-doc.cn

sm dist n3 3

Finally, we can send Event K, which takes the state machine state back to state S11. You should see this happen in all of the browsers. The following image shows the result in one browser:spring-doc.cn

sm dist n1 4

47. Scope

Scope is a state machine example that uses session scope to provide an individual instance for every user. The following image shows the states and events within the Scope state machine:spring-doc.cn

statechart12

This simple state machine has three states: S0, S1, and S2. Transitions between those are controlled by three events: A, B, and C.spring-doc.cn

To start the state machine, run the following command in a terminal:spring-doc.cn

# java -jar spring-statemachine-samples-scope-4.0.1-SNAPSHOT.jar

When the instance is running, you can open a browser and play with the state machine. If you open the same page in a different browser, (for example, one in Chrome and one in Firefox), you should get a new state machine instance for each user session. The following image shows the state machine in a browser:spring-doc.cn

sm scope 1

48. Security

Security is a state machine example that uses most of the possible combinations of securing a state machine. It secures sending events, transitions, and actions. The following image shows the state machine’s states and events:spring-doc.cn

statechart13

To start the state machine, run the following command:spring-doc.cn

# java -jar spring-statemachine-samples-secure-4.0.1-SNAPSHOT.jar

We secure event sending by requiring that users have a role of USER. Spring Security ensures that no other users can send events to this state machine. The following listing secures event sending:spring-doc.cn

@Override
public void configure(StateMachineConfigurationConfigurer<States, Events> config)
		throws Exception {
	config
		.withConfiguration()
			.autoStartup(true)
			.and()
		.withSecurity()
			.enabled(true)
			.event("hasRole('USER')");
}

In this sample we define two users:spring-doc.cn

The password for both users is password. The following listing configures the two users:spring-doc.cn

static class SecurityConfig {

	@Bean
	public InMemoryUserDetailsManager userDetailsService() {
		UserDetails user = User.withDefaultPasswordEncoder()
				.username("user")
				.password("password")
				.roles("USER")
				.build();
		UserDetails admin = User.withDefaultPasswordEncoder()
				.username("admin")
				.password("password")
				.roles("USER", "ADMIN")
				.build();
		return new InMemoryUserDetailsManager(user, admin);
	}
}

We define various transitions between states according to the state chart shown at the beginning of the example. Only a user with an active ADMIN role can run the external transitions between S2 and S3. Similarly only an ADMIN can run the internal transition the S1 state. The following listing defines the transitions, including their security:spring-doc.cn

@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
		throws Exception {
	transitions
		.withExternal()
			.source(States.S0).target(States.S1).event(Events.A)
			.and()
		.withExternal()
			.source(States.S1).target(States.S2).event(Events.B)
			.and()
		.withExternal()
			.source(States.S2).target(States.S0).event(Events.C)
			.and()
		.withExternal()
			.source(States.S2).target(States.S3).event(Events.E)
			.secured("ROLE_ADMIN", ComparisonType.ANY)
			.and()
		.withExternal()
			.source(States.S3).target(States.S0).event(Events.C)
			.and()
		.withInternal()
			.source(States.S0).event(Events.D)
			.action(adminAction())
			.and()
		.withInternal()
			.source(States.S1).event(Events.F)
			.action(transitionAction())
			.secured("ROLE_ADMIN", ComparisonType.ANY);
}

The following listing uses a method called adminAction whose return type is Action to specify that the action is secured with a role of ADMIN:spring-doc.cn

@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
@Bean
public Action<States, Events> adminAction() {
	return new Action<States, Events>() {

		@Secured("ROLE_ADMIN")
		@Override
		public void execute(StateContext<States, Events> context) {
			log.info("Executed only for admin role");
		}
	};
}

The following Action runs an internal transition in state S when event F is sent.spring-doc.cn

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

		@Override
		public void execute(StateContext<States, Events> context) {
			log.info("Executed only for admin role");
		}
	};
}

The transition itself is secured with a role of ADMIN, so this transition does not run if the current user does not hate that role.spring-doc.cn

49. Event Service

The event service example shows how you can use state machine concepts as a processing engine for events. This sample evolved from a question:spring-doc.cn

Can I use Spring Statemachine as a microservice to feed events to different state machine instances? In fact, Spring Statemachine can feed events to potentially millions of different state machine instances.spring-doc.cn

This example uses a Redis instance to persist state machine instances.spring-doc.cn

Obviously, a million state machine instances in a JVM would be a bad idea, due to memory constraints. This leads to other features of Spring Statemachine that let you persist a StateMachineContext and re-use existing instances.spring-doc.cn

For this example, we assume that a shopping application sends different types of PageView events to a separate microservice which then tracks user behavior by using a state machine. The following image shows the state model, which has a few states that represent a user navigating a product items list, adding and removing items from a cart, going to a payment page, and initiating a payment operation:spring-doc.cn

statechart14

An actual shopping application would send these events into this service by (for example) using a rest call. More about this later.spring-doc.cn

Remember that the focus here is to have an application that exposes a REST API that the user can use to send events that can be processed by a state machine for each request.

The following state machine configuration models what we have in a state chart. Various actions update the state machine’s Extended State to track the number of entries into various states and also how many times the internal transitions for ADD and DEL are called and whether PAY has been executed:spring-doc.cn

@Bean(name = "stateMachineTarget")
@Scope(scopeName="prototype")
public StateMachine<States, Events> stateMachineTarget() throws Exception {
	Builder<States, Events> builder = StateMachineBuilder.<States, Events>builder();

	builder.configureConfiguration()
		.withConfiguration()
			.autoStartup(true);

	builder.configureStates()
		.withStates()
			.initial(States.HOME)
			.states(EnumSet.allOf(States.class));

	builder.configureTransitions()
		.withInternal()
			.source(States.ITEMS).event(Events.ADD)
			.action(addAction())
			.and()
		.withInternal()
			.source(States.CART).event(Events.DEL)
			.action(delAction())
			.and()
		.withInternal()
			.source(States.PAYMENT).event(Events.PAY)
			.action(payAction())
			.and()
		.withExternal()
			.source(States.HOME).target(States.ITEMS)
			.action(pageviewAction())
			.event(Events.VIEW_I)
			.and()
		.withExternal()
			.source(States.CART).target(States.ITEMS)
			.action(pageviewAction())
			.event(Events.VIEW_I)
			.and()
		.withExternal()
			.source(States.ITEMS).target(States.CART)
			.action(pageviewAction())
			.event(Events.VIEW_C)
			.and()
		.withExternal()
			.source(States.PAYMENT).target(States.CART)
			.action(pageviewAction())
			.event(Events.VIEW_C)
			.and()
		.withExternal()
			.source(States.CART).target(States.PAYMENT)
			.action(pageviewAction())
			.event(Events.VIEW_P)
			.and()
		.withExternal()
			.source(States.ITEMS).target(States.HOME)
			.action(resetAction())
			.event(Events.RESET)
			.and()
		.withExternal()
			.source(States.CART).target(States.HOME)
			.action(resetAction())
			.event(Events.RESET)
			.and()
		.withExternal()
			.source(States.PAYMENT).target(States.HOME)
			.action(resetAction())
			.event(Events.RESET);

	return builder.build();
}

Do not focus on stateMachineTarget or @Scope for now, as we explain those later in this section.spring-doc.cn

We set up a RedisConnectionFactory that defaults to localhost and default port. We use StateMachinePersist with a RepositoryStateMachinePersist implementation. Finally, we create a RedisStateMachinePersister that uses a previously created StateMachinePersist bean.spring-doc.cn

These are then used in a Controller that handles REST calls, as the following listing shows:spring-doc.cn

@Bean
public RedisConnectionFactory redisConnectionFactory() {
	return new JedisConnectionFactory();
}

@Bean
public StateMachinePersist<States, Events, String> stateMachinePersist(RedisConnectionFactory connectionFactory) {
	RedisStateMachineContextRepository<States, Events> repository =
			new RedisStateMachineContextRepository<States, Events>(connectionFactory);
	return new RepositoryStateMachinePersist<States, Events>(repository);
}

@Bean
public RedisStateMachinePersister<States, Events> redisStateMachinePersister(
		StateMachinePersist<States, Events, String> stateMachinePersist) {
	return new RedisStateMachinePersister<States, Events>(stateMachinePersist);
}

We create a bean named stateMachineTarget. State machine instantiation is a relatively expensive operation, so it is better to try to pool instances instead of instantiating a new instance for every request. To do so, we first create a poolTargetSource that wraps stateMachineTarget and pools it with a max size of three. When then proxy this poolTargetSource with ProxyFactoryBean by using a request scope. Effectively, this means that every REST request gets a pooled state machine instance from a bean factory. Later, we show how these instances are used. The following listing shows how we create the ProxyFactoryBean and set the target source:spring-doc.cn

@Bean
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public ProxyFactoryBean stateMachine() {
	ProxyFactoryBean pfb = new ProxyFactoryBean();
	pfb.setTargetSource(poolTargetSource());
	return pfb;
}

The following listing shows we set the maximum size and set the target bean name:spring-doc.cn

@Bean
public CommonsPool2TargetSource poolTargetSource() {
	CommonsPool2TargetSource pool = new CommonsPool2TargetSource();
	pool.setMaxSize(3);
	pool.setTargetBeanName("stateMachineTarget");
	return pool;
}

Now we can get into actual demo. You need to have a Redis server running on localhost with default settings. Then you need to run the Boot-based sample application by running the following command:spring-doc.cn

# java -jar spring-statemachine-samples-eventservice-4.0.1-SNAPSHOT.jar

In a browser, you see something like the following:spring-doc.cn

sm eventservice 1

In this UI, you can use three users: joe, bob, and dave. Clicking a button shows the current state and the extended state. Enabling a radio button before clicking a button sends a particular event for that user. This arrangement lets you play with the UI.spring-doc.cn

In our StateMachineController, we autowire StateMachine and StateMachinePersister. StateMachine is request scoped, so you get a new instance for each request, while StateMachinePersist is a normal singleton bean. The following listing autowires StateMachine and StateMachinePersist:spring-doc.cn

@Autowired
private StateMachine<States, Events> stateMachine;

@Autowired
private StateMachinePersister<States, Events, String> stateMachinePersister;

In the following listing, feedAndGetState is used with a UI to do same things that an actual REST api might do:spring-doc.cn

@RequestMapping("/state")
public String feedAndGetState(@RequestParam(value = "user", required = false) String user,
		@RequestParam(value = "id", required = false) Events id, Model model) throws Exception {
	model.addAttribute("user", user);
	model.addAttribute("allTypes", Events.values());
	model.addAttribute("stateChartModel", stateChartModel);
	// we may get into this page without a user so
	// do nothing with a state machine
	if (StringUtils.hasText(user)) {
		resetStateMachineFromStore(user);
		if (id != null) {
			feedMachine(user, id);
		}
		model.addAttribute("states", stateMachine.getState().getIds());
		model.addAttribute("extendedState", stateMachine.getExtendedState().getVariables());
	}
	return "states";
}

In the following listing, feedPageview is a REST method that accepts a post with JSON content.spring-doc.cn

@RequestMapping(value = "/feed",method= RequestMethod.POST)
@ResponseStatus(HttpStatus.OK)
public void feedPageview(@RequestBody(required = true) Pageview event) throws Exception {
	Assert.notNull(event.getUser(), "User must be set");
	Assert.notNull(event.getId(), "Id must be set");
	resetStateMachineFromStore(event.getUser());
	feedMachine(event.getUser(), event.getId());
}

In the following listing, feedMachine sends an event into a StateMachine and persists its state by using a StateMachinePersister:spring-doc.cn

private void feedMachine(String user, Events id) throws Exception {
	stateMachine
		.sendEvent(Mono.just(MessageBuilder
			.withPayload(id).build()))
		.blockLast();
	stateMachinePersister.persist(stateMachine, "testprefix:" + user);
}

The following listing shows a resetStateMachineFromStore that is used to restore a state machine for a particular user:spring-doc.cn

private StateMachine<States, Events> resetStateMachineFromStore(String user) throws Exception {
	return stateMachinePersister.restore(stateMachine, "testprefix:" + user);
}

As you would usually send an event by using a UI, you can do the same by using REST calls, as the following curl command shows:spring-doc.cn

# curl http://localhost:8080/feed -H "Content-Type: application/json" --data '{"user":"joe","id":"VIEW_I"}'

At this point, you should have content in Redis with a key of testprefix:joe, as the following example shows:spring-doc.cn

$ ./redis-cli
127.0.0.1:6379> KEYS *
1) "testprefix:joe"

The next three images show when state for joe has been changed from HOME to ITEMS and when the ADD action has been executed.spring-doc.cn

The following image the ADD event being sent:spring-doc.cn

sm eventservice 2

Now your are still on the ITEMS state, and the internal transition caused the COUNT extended state variable to increase to 1, as the following image shows:spring-doc.cn

sm eventservice 3

Now you can run the following curl rest call a few times (or do it through the UI) and see the COUNT variable increase with every call:spring-doc.cn

# curl http://localhost:8080/feed -H "Content-Type: application/json" # --data '{"user":"joe","id":"ADD"}'

The following image shows the result of these operations:spring-doc.cn

sm eventservice 4

50. Deploy

The deploy example shows how you can use state machine concepts with UML modeling to provide a generic error handling state. This state machine is a relatively complex example of how you can use various features to provide a centralized error handling concept. The following image shows the deploy state machine:spring-doc.cn

model deployer
The preceding state chart was designed by using the Eclipse Papyrus Plugin (seeEclipse Modeling Support) and imported into Spring StateMachine through the resulting UML model file. Actions and guards defined in a model are resolved from a Spring Application Context.

In this state machine scenario, we have two different behaviors (DEPLOY and UNDEPLOY) that user tries to execute.spring-doc.cn

In the preceding state chart:spring-doc.cn

  • In the DEPLOY state, the INSTALL and START states are entered conditionally. We enter START directly if a product is already installed and have no need to try to START if install fails.spring-doc.cn

  • In the UNDEPLOY state, we enter STOP conditionally if the application is already running.spring-doc.cn

  • Conditional choices for DEPLOY and UNDEPLOY are done through a choice pseudostate within those states, and the choices are selected by guards.spring-doc.cn

  • We use exit point pseudostates to have a more controlled exit from the DEPLOY and UNDEPLOY states.spring-doc.cn

  • After exiting from DEPLOY and UNDEPLOY, we go through a junction pseudostate to choose whether to go through an ERROR state (if an error was added into an extended state).spring-doc.cn

  • Finally, we go back to the READY state to process new requests.spring-doc.cn

Now we can get to the actual demo. Run the boot based sample application by running the following command:spring-doc.cn

# java -jar spring-statemachine-samples-deploy-4.0.1-SNAPSHOT.jar

In a browser, you can see something like the following image:spring-doc.cn

sm deploy 1
As we do not have real install, start, or stop functionality, we simulate failures by checking the existence of particular message headers.

Now you can start to send events to a machine and choose various message headers to drive functionality.spring-doc.cn

51. Order Shipping

The order shipping example shows how you can use state machine concepts to build a simple order processing system.spring-doc.cn

The following image shows a state chart that drives this order shipping sample.spring-doc.cn

sm ordershipping 1

In the preceding state chart:spring-doc.cn

  • The state machine enters the WAIT_NEW_ORDER (default) state.spring-doc.cn

  • The event PLACE_ORDER transitions into the RECEIVE_ORDER state and the entry action (entryReceiveOrder) is executed.spring-doc.cn

  • If the order is OK, the state machine goes into two regions, one handling order production and one handling user-level payment. Otherwise, the state machine goes into CUSTOMER_ERROR, which is a final state.spring-doc.cn

  • The state machine loops in a lower region to remind the user to pay until RECEIVE_PAYMENT is sent successfully to indicate correct payment.spring-doc.cn

  • Both regions go into waiting states (WAIT_PRODUCT and WAIT_ORDER), where they are joined before the parent orthogonal state (HANDLE_ORDER) is exited.spring-doc.cn

  • Finally, the state machine goes through SHIP_ORDER to its final state (ORDER_SHIPPED).spring-doc.cn

The following command runs the sample:spring-doc.cn

# java -jar spring-statemachine-samples-ordershipping-4.0.1-SNAPSHOT.jar

In a browser, you can see something similar to the following image. You can start by choosing a customer and an order to create a state machine.spring-doc.cn

sm ordershipping 2

The state machine for a particular order is now created and you can start to play with placing an order and sending a payment. Other settings (such as makeProdPlan, produce, and payment) let you control how the state machine works. The following image shows the state machine waiting for an order:spring-doc.cn

sm ordershipping 3

Finally, you can see what machine does by refreshing a page, as the following image shows:spring-doc.cn

sm ordershipping 4

52. JPA Configuration

The JPA configuration example shows how you can use state machine concepts with a machine configuration kept in a database. This sample uses an embedded H2 database with an H2 Console (to ease playing with the database).spring-doc.cn

This sample uses spring-statemachine-autoconfigure (which, by default, auto-configures the repositories and entity classes needed for JPA). Thus, you need only @SpringBootApplication. The following example shows the Application class with the @SpringBootApplication annotation:spring-doc.cn

@SpringBootApplication
public class Application {

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

The following example shows how to create a RepositoryStateMachineModelFactory:spring-doc.cn

@Configuration
@EnableStateMachineFactory
public static class Config extends StateMachineConfigurerAdapter<String, String> {

	@Autowired
	private StateRepository<? extends RepositoryState> stateRepository;

	@Autowired
	private TransitionRepository<? extends RepositoryTransition> transitionRepository;

	@Override
	public void configure(StateMachineModelConfigurer<String, String> model) throws Exception {
		model
			.withModel()
				.factory(modelFactory());
	}

	@Bean
	public StateMachineModelFactory<String, String> modelFactory() {
		return new RepositoryStateMachineModelFactory(stateRepository, transitionRepository);
	}
}

You can use the following command to run the sample:spring-doc.cn

# java -jar spring-statemachine-samples-datajpa-4.0.1-SNAPSHOT.jar

Accessing the application at http://localhost:8080 brings up a newly constructed machine for each request. You can then choose to send events to a machine. The possible events and machine configuration are updated from a database with every request. The following image shows the UI and the initial events that are created when this state machine starts:spring-doc.cn

sm datajpa 1

To access the embedded console, you can use the JDBC URL (which is jdbc:h2:mem:testdb, if it is not already set). The following image shows the H2 console:spring-doc.cn

sm datajpa 2

From the console, you can see the database tables and modify them as you wish. The following image shows the result of a simple query in the UI:spring-doc.cn

sm datajpa 3

Now that you have gotten this far, you have probably wondered how those default states and transitions got populated into the database. Spring Data has a nice trick to auto-populate repositories, and we used this feature through Jackson2RepositoryPopulatorFactoryBean. The following example shows how we create such a bean:spring-doc.cn

@Bean
public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() {
	StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean();
	factoryBean.setResources(new Resource[]{new ClassPathResource("data.json")});
	return factoryBean;
}

The following listing shows the source of the data with which we populate the database:spring-doc.cn

[
	{
		"@id": "10",
		"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction",
		"spel": "T(System).out.println('hello exit S1')"
	},
	{
		"@id": "11",
		"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction",
		"spel": "T(System).out.println('hello entry S2')"
	},
	{
		"@id": "12",
		"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction",
		"spel": "T(System).out.println('hello state S3')"
	},
	{
		"@id": "13",
		"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction",
		"spel": "T(System).out.println('hello')"
	},
	{
		"@id": "1",
		"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
		"initial": true,
		"state": "S1",
		"exitActions": ["10"]
	},
	{
		"@id": "2",
		"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
		"initial": false,
		"state": "S2",
		"entryActions": ["11"]
	},
	{
		"@id": "3",
		"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
		"initial": false,
		"state": "S3",
		"stateActions": ["12"]
	},
	{
		"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
		"source": "1",
		"target": "2",
		"event": "E1",
		"kind": "EXTERNAL"
	},
	{
		"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
		"source": "2",
		"target": "3",
		"event": "E2",
		"actions": ["13"]
	}
]

53. Data Persist

The data persist sample shows how you can state machine concepts with a persisting machine in an external repository. This sample uses an embedded H2 database with an H2 Console (to ease playing with the database). Optionally, you can also enable Redis or MongoDB.spring-doc.cn

This sample uses spring-statemachine-autoconfigure (which, by default, auto-configures the repositories and entity classes needed for JPA). Thus, you need only @SpringBootApplication. The following example shows the Application class with the @SpringBootApplication annotation:spring-doc.cn

@SpringBootApplication
public class Application {

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

The StateMachineRuntimePersister interface works on the runtime level of a StateMachine. Its implementation, JpaPersistingStateMachineInterceptor, is meant to be used with a JPA. The following listing creates a StateMachineRuntimePersister bean:spring-doc.cn

@Configuration
@Profile("jpa")
public static class JpaPersisterConfig {

	@Bean
	public StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister(
			JpaStateMachineRepository jpaStateMachineRepository) {
		return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository);
	}
}

The following example shows how you can use a very similar configuration to create a bean for MongoDB:spring-doc.cn

@Configuration
@Profile("mongo")
public static class MongoPersisterConfig {

	@Bean
	public StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister(
			MongoDbStateMachineRepository jpaStateMachineRepository) {
		return new MongoDbPersistingStateMachineInterceptor<>(jpaStateMachineRepository);
	}
}

The following example shows how you can use a very similar configuration to create a bean for Redis:spring-doc.cn

@Configuration
@Profile("redis")
public static class RedisPersisterConfig {

	@Bean
	public StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister(
			RedisStateMachineRepository jpaStateMachineRepository) {
		return new RedisPersistingStateMachineInterceptor<>(jpaStateMachineRepository);
	}
}

You can configure StateMachine to use runtime persistence by using the withPersistence configuration method. The following listing shows how to do so:spring-doc.cn

@Autowired
private StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister;

@Override
public void configure(StateMachineConfigurationConfigurer<States, Events> config)
		throws Exception {
	config
		.withPersistence()
			.runtimePersister(stateMachineRuntimePersister);
}

This sample also uses DefaultStateMachineService, which makes it easier to work with multiple machines. The following listing shows how to create an instance of DefaultStateMachineService:spring-doc.cn

@Bean
public StateMachineService<States, Events> stateMachineService(
		StateMachineFactory<States, Events> stateMachineFactory,
		StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister) {
	return new DefaultStateMachineService<States, Events>(stateMachineFactory, stateMachineRuntimePersister);
}

The following listing shows the logic that drives the StateMachineService in this sample:spring-doc.cn

private synchronized StateMachine<States, Events> getStateMachine(String machineId) throws Exception {
	listener.resetMessages();
	if (currentStateMachine == null) {
		currentStateMachine = stateMachineService.acquireStateMachine(machineId);
		currentStateMachine.addStateListener(listener);
		currentStateMachine.startReactively().block();
	} else if (!ObjectUtils.nullSafeEquals(currentStateMachine.getId(), machineId)) {
		stateMachineService.releaseStateMachine(currentStateMachine.getId());
		currentStateMachine.stopReactively().block();
		currentStateMachine = stateMachineService.acquireStateMachine(machineId);
		currentStateMachine.addStateListener(listener);
		currentStateMachine.startReactively().block();
	}
	return currentStateMachine;
}

You can use the following command to run the sample:spring-doc.cn

# java -jar spring-statemachine-samples-datapersist-4.0.1-SNAPSHOT.jar

By default, the jpa profile is enabled in application.yml. If you want to try other backends, enable either the mongo profile or the redis profile. The following commands specify which profile to use (jpa is the default, but we included it for the sake of completeness):spring-doc.cn

# java -jar spring-statemachine-samples-datapersist-4.0.1-SNAPSHOT.jar --spring.profiles.active=jpa
# java -jar spring-statemachine-samples-datapersist-4.0.1-SNAPSHOT.jar --spring.profiles.active=mongo
# java -jar spring-statemachine-samples-datapersist-4.0.1-SNAPSHOT.jar --spring.profiles.active=redis

Accessing the application at http://localhost:8080 brings up a newly constructed state machine for each request, and you can choose to send events to a machine. The possible events and machine configuration are updated from a database with every request.spring-doc.cn

The state machines in this sample have a simple configuration with states 'S1' to 'S6' and events 'E1' to 'E6' to transition the state machine between those states. You can use two state machine identifiers (datajpapersist1 and datajpapersist2) to request a particular state machine. The following image shows the UI that lets you pick a machine and an event and that shows what happens when you do:spring-doc.cn

sm datajpapersist 1

The sample defaults to using machine 'datajpapersist1' and goes to its initial state 'S1'. The following image shows the result of using those defaults:spring-doc.cn

sm datajpapersist 2

If you send events E1 and E2 to the datajpapersist1 state machine, its state is persisted as 'S3'. The following image shows the result of doing so:spring-doc.cn

sm datajpapersist 3

If you then request state machine datajpapersist1 but send no events, the state machine is restored back to its persisted state, S3.spring-doc.cn

54. Data Multi Persist

The data multi ersist sample is an extension of two other samples: JPA Configuration and Data Persist. We still keep machine configuration in a database and persist into a database. However, this time, we also have a machine that contains two orthogonal regions, to show how those are persisted independently. This sample also uses an embedded H2 database with an H2 Console (to ease playing with the database).spring-doc.cn

This sample uses spring-statemachine-autoconfigure (which, by default, auto-configures the repositories and entity classes needed for JPA). Thus, you need only @SpringBootApplication. The following example shows the Application class with the @SpringBootApplication annotation:spring-doc.cn

@SpringBootApplication
public class Application {

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

As in the other data-driven samples, we again create a StateMachineRuntimePersister, as the following listing shows:spring-doc.cn

@Bean
public StateMachineRuntimePersister<String, String, String> stateMachineRuntimePersister(
		JpaStateMachineRepository jpaStateMachineRepository) {
	return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository);
}

A StateMachineService bean makes it easier to work with a machines. The following listing shows how to create such a bean:spring-doc.cn

@Bean
public StateMachineService<String, String> stateMachineService(
		StateMachineFactory<String, String> stateMachineFactory,
		StateMachineRuntimePersister<String, String, String> stateMachineRuntimePersister) {
	return new DefaultStateMachineService<String, String>(stateMachineFactory, stateMachineRuntimePersister);
}

We use JSON data to import the configuration. The following example creates a bean to do so:spring-doc.cn

@Bean
public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() {
	StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean();
	factoryBean.setResources(new Resource[] { new ClassPathResource("datajpamultipersist.json") });
	return factoryBean;
}

The following listing shows how we get a RepositoryStateMachineModelFactory:spring-doc.cn

@Configuration
@EnableStateMachineFactory
public static class Config extends StateMachineConfigurerAdapter<String, String> {

	@Autowired
	private StateRepository<? extends RepositoryState> stateRepository;

	@Autowired
	private TransitionRepository<? extends RepositoryTransition> transitionRepository;

	@Autowired
	private StateMachineRuntimePersister<String, String, String> stateMachineRuntimePersister;

	@Override
	public void configure(StateMachineConfigurationConfigurer<String, String> config)
			throws Exception {
		config
			.withPersistence()
				.runtimePersister(stateMachineRuntimePersister);
	}

	@Override
	public void configure(StateMachineModelConfigurer<String, String> model)
			throws Exception {
		model
			.withModel()
				.factory(modelFactory());
	}

	@Bean
	public StateMachineModelFactory<String, String> modelFactory() {
		return new RepositoryStateMachineModelFactory(stateRepository, transitionRepository);
	}
}

You can run the sample by using the following command:spring-doc.cn

# java -jar spring-statemachine-samples-datajpamultipersist-4.0.1-SNAPSHOT.jar

Accessing the application at http://localhost:8080 brings up a newly constructed machine for each request and lets you send events to a machine. The possible events and the state machine configuration are updated from a database for each request. We also print out all state machine contexts and the current root machine, as the following image shows:spring-doc.cn

sm datajpamultipersist 1

The state machine named datajpamultipersist1 is a simple “flat” machine where states S1, S2 and S3 are transitioned by events E1, E2, and E3 (respectively). However, the state machine named datajpamultipersist2 contains two regions (R1 and R2) directly under the root level. That is why this root level machine really does not have a state. We need that root level machine to host those regions.spring-doc.cn

Regions R1 and R2 in the datajpamultipersist2 state machine contains states S10, S11, and S12 and S20, S21, and S22 (respectively). Events E10, E11, and E12 are used for region R1 and events E20, E21, and event E22 is used for region R2. The following images shows what happens when we send events E10 and E20 to the datajpamultipersist2 state machine:spring-doc.cn

sm datajpamultipersist 2

Regions have their own contexts with their own IDs, and the actual ID is postfixed with # and the region ID. As the following image shows, different regions in a database have different contexts:spring-doc.cn

sm datajpamultipersist 3

55. Data JPA Persist

The data persist sample shows how you can state machine concepts with a persisting machine in an external repository. This sample uses an embedded H2 database with an H2 Console (to ease playing with the database). Optionally, you can also enable Redis or MongoDB.spring-doc.cn

This sample uses spring-statemachine-autoconfigure (which, by default, auto-configures the repositories and entity classes needed for JPA). Thus, you need only @SpringBootApplication. The following example shows the Application class with the @SpringBootApplication annotation:spring-doc.cn

@SpringBootApplication
public class Application {

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

The StateMachineRuntimePersister interface works on the runtime level of a StateMachine. Its implementation, JpaPersistingStateMachineInterceptor, is meant to be used with a JPA. The following listing creates a StateMachineRuntimePersister bean:spring-doc.cn

@Configuration
@Profile("jpa")
public static class JpaPersisterConfig {

	@Bean
	public StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister(
			JpaStateMachineRepository jpaStateMachineRepository) {
		return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository);
	}
}

The following example shows how you can use a very similar configuration to create a bean for MongoDB:spring-doc.cn

@Configuration
@Profile("mongo")
public static class MongoPersisterConfig {

	@Bean
	public StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister(
			MongoDbStateMachineRepository jpaStateMachineRepository) {
		return new MongoDbPersistingStateMachineInterceptor<>(jpaStateMachineRepository);
	}
}

The following example shows how you can use a very similar configuration to create a bean for Redis:spring-doc.cn

@Configuration
@Profile("redis")
public static class RedisPersisterConfig {

	@Bean
	public StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister(
			RedisStateMachineRepository jpaStateMachineRepository) {
		return new RedisPersistingStateMachineInterceptor<>(jpaStateMachineRepository);
	}
}

You can configure StateMachine to use runtime persistence by using the withPersistence configuration method. The following listing shows how to do so:spring-doc.cn

@Autowired
private StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister;

@Override
public void configure(StateMachineConfigurationConfigurer<States, Events> config)
		throws Exception {
	config
		.withPersistence()
			.runtimePersister(stateMachineRuntimePersister);
}

This sample also uses DefaultStateMachineService, which makes it easier to work with multiple machines. The following listing shows how to create an instance of DefaultStateMachineService:spring-doc.cn

@Bean
public StateMachineService<States, Events> stateMachineService(
		StateMachineFactory<States, Events> stateMachineFactory,
		StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister) {
	return new DefaultStateMachineService<States, Events>(stateMachineFactory, stateMachineRuntimePersister);
}

The following listing shows the logic that drives the StateMachineService in this sample:spring-doc.cn

private synchronized StateMachine<States, Events> getStateMachine(String machineId) throws Exception {
	listener.resetMessages();
	if (currentStateMachine == null) {
		currentStateMachine = stateMachineService.acquireStateMachine(machineId);
		currentStateMachine.addStateListener(listener);
		currentStateMachine.startReactively().block();
	} else if (!ObjectUtils.nullSafeEquals(currentStateMachine.getId(), machineId)) {
		stateMachineService.releaseStateMachine(currentStateMachine.getId());
		currentStateMachine.stopReactively().block();
		currentStateMachine = stateMachineService.acquireStateMachine(machineId);
		currentStateMachine.addStateListener(listener);
		currentStateMachine.startReactively().block();
	}
	return currentStateMachine;
}

You can use the following command to run the sample:spring-doc.cn

# java -jar spring-statemachine-samples-datapersist-4.0.1-SNAPSHOT.jar

By default, the jpa profile is enabled in application.yml. If you want to try other backends, enable either the mongo profile or the redis profile. The following commands specify which profile to use (jpa is the default, but we included it for the sake of completeness):spring-doc.cn

# java -jar spring-statemachine-samples-datapersist-4.0.1-SNAPSHOT.jar --spring.profiles.active=jpa
# java -jar spring-statemachine-samples-datapersist-4.0.1-SNAPSHOT.jar --spring.profiles.active=mongo
# java -jar spring-statemachine-samples-datapersist-4.0.1-SNAPSHOT.jar --spring.profiles.active=redis

Accessing the application at http://localhost:8080 brings up a newly constructed state machine for each request, and you can choose to send events to a machine. The possible events and machine configuration are updated from a database with every request.spring-doc.cn

The state machines in this sample have a simple configuration with states 'S1' to 'S6' and events 'E1' to 'E6' to transition the state machine between those states. You can use two state machine identifiers (datajpapersist1 and datajpapersist2) to request a particular state machine. The following image shows the UI that lets you pick a machine and an event and that shows what happens when you do:spring-doc.cn

sm datajpapersist 1

The sample defaults to using machine 'datajpapersist1' and goes to its initial state 'S1'. The following image shows the result of using those defaults:spring-doc.cn

sm datajpapersist 2

If you send events E1 and E2 to the datajpapersist1 state machine, its state is persisted as 'S3'. The following image shows the result of doing so:spring-doc.cn

sm datajpapersist 3

If you then request state machine datajpapersist1 but send no events, the state machine is restored back to its persisted state, S3.spring-doc.cn

56. Monitoring

The monitoring sample shows how you can use state machine concepts to monitor state machine transitions and actions. The following listing configures the state machine that we use for this sample:spring-doc.cn

@Configuration
@EnableStateMachine
public static class Config extends StateMachineConfigurerAdapter<String, String> {

	@Override
	public void configure(StateMachineStateConfigurer<String, String> states)
			throws Exception {
		states
			.withStates()
				.initial("S1")
				.state("S2", null, (c) -> {System.out.println("hello");})
				.state("S3", (c) -> {System.out.println("hello");}, null);
	}

	@Override
	public void configure(StateMachineTransitionConfigurer<String, String> transitions)
			throws Exception {
		transitions
			.withExternal()
				.source("S1").target("S2").event("E1")
				.action((c) -> {System.out.println("hello");})
				.and()
			.withExternal()
				.source("S2").target("S3").event("E2");
	}
}

You can use the following command to run the sample:spring-doc.cn

# java -jar spring-statemachine-samples-monitoring-4.0.1-SNAPSHOT.jar

The following image shows the state machine’s initial state:spring-doc.cn

sm monitoring 1

The following image shows the state of the state machine after we have performed some actions:spring-doc.cn

sm monitoring 2

You can view metrics from Spring Boot by running the following two curl commands (shown with their output):spring-doc.cn

# curl http://localhost:8080/actuator/metrics/ssm.transition.duration

{
  "name":"ssm.transition.duration",
  "measurements":[
    {
      "statistic":"COUNT",
      "value":3.0
    },
    {
      "statistic":"TOTAL_TIME",
      "value":0.007
    },
    {
      "statistic":"MAX",
      "value":0.004
    }
  ],
  "availableTags":[
    {
      "tag":"transitionName",
      "values":[
        "INITIAL_S1",
        "EXTERNAL_S1_S2"
      ]
    }
  ]
}
# curl http://localhost:8080/actuator/metrics/ssm.transition.transit

{
  "name":"ssm.transition.transit",
  "measurements":[
    {
      "statistic":"COUNT",
      "value":3.0
    }
  ],
  "availableTags":[
    {
      "tag":"transitionName",
      "values":[
        "EXTERNAL_S1_S2",
        "INITIAL_S1"
      ]
    }
  ]
}

You can also view tracing from Spring Boot by running the following curl command (shown with its output):spring-doc.cn

# curl http://localhost:8080/actuator/statemachinetrace

[
  {
    "timestamp":"2018-02-11T06:44:12.723+0000",
    "info":{
      "duration":2,
      "machine":null,
      "transition":"EXTERNAL_S1_S2"
    }
  },
  {
    "timestamp":"2018-02-11T06:44:12.720+0000",
    "info":{
      "duration":0,
      "machine":null,
      "action":"demo.monitoring.StateMachineConfig$Config$$Lambda$576/1499688007@22b47b2f"
    }
  },
  {
    "timestamp":"2018-02-11T06:44:12.714+0000",
    "info":{
      "duration":1,
      "machine":null,
      "transition":"INITIAL_S1"
    }
  },
  {
    "timestamp":"2018-02-11T06:44:09.689+0000",
    "info":{
      "duration":4,
      "machine":null,
      "transition":"INITIAL_S1"
    }
  }
]