Traditionally an instance of a state machine is used as is within a running program. More dynamic behaviour is possible to achieve via dynamic builders and factories which allows state machine instantiation on-demand. Building an instance of a state machine is relatively heavy operation so if there is a need to i.e. handle arbitrary state change in a database using a state machine we need to find a better and faster way to do it.
Persist feature allows user to save a state of a state machine itself into an external repository and later reset a state machine based of serialized state. For example if you have a database table keeping orders it would be way too expensive to update order state via a state machine if a new instance would need to be build for every change. Persist feature allows you to reset a state machine state without instantiating a new state machine instance.
![]() | Note |
|---|---|
|
There is one recipe Chapter 28, Persist and one sample Chapter 35, Persist which provides more info about persisting states. |
While it is possible to build a custom persistence feature using a
StateMachineListener it has one conceptual problem. When listener
notifies a change of state, state change has already happened. If a
custom persistent method within a listener fails to update serialized
state in an external repository, state in a state machine and state in
an external repository are then in inconsistent state.
State machine interceptor can be used instead of where attempt to save serialized state into an external storage is done during the a state change within a state machine. If this interceptor callback fails, state change attempt will be halted and instead of ending into an inconsistent state, user can then handle this error manually. Using the interceptors are discussed in Chapter 21, State Machine Interceptor.
It is impossible to persist a StateMachine using normal java
serialization as object graph is too rich and contains too much
dependencies into other Spring context classes. StateMachineContext
is a runtime representation of a state machine which can be used to
restore an existing machine into a state represented by a particular
StateMachineContext object.
Building a StateMachineContext and then restoring a state machine
from it has always been a little bit of a black magic if done
manually. Interface StateMachinePersister aims to ease these
operations by providing persist and restore methods. Default
implementation of this interface is DefaultStateMachinePersister
Usage of a StateMachinePersister is easy to demonstrate by following
a snippets from tests. We start by creating to two similar configs for
a state machine machine1 and machine2. We could build different
machines for this demonstration using various other ways but this
servers a purpose for this case.
@Configuration @EnableStateMachine(name = "machine1") static class Config1 extends Config { } @Configuration @EnableStateMachine(name = "machine2") static class Config2 extends Config { } static class Config extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineStateConfigurer<String, String> states) throws Exception { states .withStates() .initial("S1") .state("S1") .state("S2"); } @Override public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception { transitions .withExternal() .source("S1") .target("S2") .event("E1"); } }
As we’re using a StateMachinePersist we simply create an in-memory
implementation.
![]() | Note |
|---|---|
|
In-memory sample is just for demostration purposes, use a real persistent storage implementations. |
static class InMemoryStateMachinePersist implements StateMachinePersist<String, String, String> { private final HashMap<String, StateMachineContext<String, String>> contexts = new HashMap<>(); @Override public void write(StateMachineContext<String, String> context, String contextOjb) throws Exception { contexts.put(contextOjb, context); } @Override public StateMachineContext<String, String> read(String contextOjb) throws Exception { return contexts.get(contextOjb); } }
After we have instantiated two different machines we can transfer
machine1 into state S2 via event E1, then persist it and restore
machine2.
InMemoryStateMachinePersist stateMachinePersist = new InMemoryStateMachinePersist(); StateMachinePersister<String, String, String> persister = new DefaultStateMachinePersister<>(stateMachinePersist); StateMachine<String, String> stateMachine1 = context.getBean("machine1", StateMachine.class); StateMachine<String, String> stateMachine2 = context.getBean("machine2", StateMachine.class); stateMachine1.start(); stateMachine1.sendEvent("E1"); assertThat(stateMachine1.getState().getIds(), contains("S2")); persister.persist(stateMachine1, "myid"); persister.restore(stateMachine2, "myid"); assertThat(stateMachine2.getState().getIds(), contains("S2"));
Support for persisting State Machine into Redis is done via
RepositoryStateMachinePersist which implements
StateMachinePersist. Specific implementation is a
RedisStateMachineContextRepository whic uses kryo serialization to
persist a StateMachineContext into Redis.
For StateMachinePersister we have a redis related
RedisStateMachinePersister implementation which takes an instance of
a StateMachinePersist and uses String as its context object.
![]() | Tip |
|---|---|
|
Check sample Chapter 40, Event Service for detailed usage. |
RedisStateMachineContextRepository will need a
RedisConnectionFactory for it to work and we recommend a
JedisConnectionFactory for it as seeing from above example.