Spring State Machines
Usually, the first major hurdle in the implementation of a software project is to design a well-formed application architecture. Along with this, core processes must be recognized together with application states as well as the depth of their interactions. There are many ways and methods to achieve this. In cases where a project meets one of these criteria, it is probably a good candidate for managing the states with a state machine:
- The application or a part of its structure can be represented by states
- Complex logic must be split into smaller manageable tasks
- Some processes must run in parallel or they become asynchronous
- Processes are controlled by global Boolean variables or enumerated variables
- Some variables become relevant in some parts of the application lifecycle.
Good candidates for a state machine will usually have the following aspects:
- Individual processes of the application are controlled by clearly formed events (ie. “Order creation”, “Order cancellation”, “Account activation” etc.)
- Processes are exclusively controlled by the user, according to what type of event was triggered
- The application process never ends or the process status may remain stable for several weeks.
In this lesson, we will study the relatively recently introduced project Spring State Machine.
Features
Spring Statemachine aims to provide the following list of features:
- Easy to use flat one level state machine for simple use cases.
- Hierarchical state machine structure to ease complex state configuration.
- State machine regions to provide even more complex state configurations.
- Usage of triggers, transitions, guards and actions.
- Type safe configuration adapter.
- Builder pattern for easy instantiation for use outside of Spring Application context
- Recipes for usual use cases
- Distributed state machine based on a Zookeeper
- State machine event listeners.
- UML Eclipse Papyrus modeling.
- Store machine config in a persistent storage.
- Spring IOC integration to associate beans with a state machine.
State machines are powerful because their behaviour is always guaranteed to be consistent, making it relatively easy to debug. This is because operational rules are written in stone when the machine is started. The idea is that your application may exist in a finite number of states and certain predefined triggers can take your application from one state to the next. Such triggers can be based on either events or timers.
SSM provides a compact framework for application developers using the concept of a traditional model of a finite state machine in combination with some of the Spring core frameworks. SSM provides the following key features:
- Easy to use flat one level state machine for simple use case,
- Usage of triggers, transitions, guards and actions,
- Type safe configuration adapter,
- SM event listeners,
- Spring IoC integration to associate beans with state machine.
The following modules build the bare-bones of SSM:
- spring-statemachine-core: core system of SSM,
- spring-statemachine-recipes-common: commonly used examples and recipes of a core framework,
- spring-statemachine-zookeeper: Zookeeper integration for distribute SM,
- spring-statemachine-test: Module supporting testing SSM.
Now, let’s get started with a sample project and have a look at the real power of Spring State machines.
Maven Dependency
Let’s add required dependency:
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
<version>1.2.3.RELEASE</version>
</dependency>
State Machine Configuration
The following samples should give us an idea of how a statemachine is configured and used. Assuming we have states STATE1, STATE2 and events EVENT1, EVENT2.
static enum States {
STATE1, STATE2
}
static enum Events {
EVENT1, EVENT2
}
State Machine Configuration
Now, let’s start by defining a simple state machine:
@Configuration
@EnableStateMachine
public class SimpleStateMachineConfiguration
extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> states)
throws Exception {
states
.withStates()
.initial("SI")
.end("SF")
.states(
new HashSet<String>(Arrays.asList("S1", "S2", "S3")));
}
@Override
public void configure(
StateMachineTransitionConfigurer<String, String> transitions)
throws Exception {
transitions.withExternal()
.source("SI").target("S1").event("E1").and()
.withExternal()
.source("S1").target("S2").event("E2").and()
.withExternal()
.source("S2").target("SF").event("end");
}
}
Note that this class is annotated as a conventional Spring configuration as well as a state machine. It also must extend StateMachineConfigurerAdapter allowing the various initialization methods to be invoked. In one of the configuration methods we define all of the possible states of the state machine—in the other, how events change the current state.
Global Listeners
We can define Global event listeners for the state machine. These listeners will be invoked each time a state transition occurs and may be utilized for things such as logging or security.
First, we need to add another configuration method—one that doesn’t deal with states or transitions but rather with the config for the state machine itself.
We need to define a listener by extending StateMachineListenerAdapter:
public class StateMachineListener extends StateMachineListenerAdapter {
@Override
public void stateChanged(State from, State to) {
System.out.printf("Transitioned from %s to %s%n", from == null ?
"none" : from.getId(), to.getId());
}
}
Extended State
Spring State Machine keeps track of its state, but to keep track of our application state, whether it’s some computed values, entries from admins or responses from calling external systems, we need to use what is know as an extended state.
Imagine we want to make sure that an account application goes through two levels of approval. We can keep track of the number of approvals using an integer stored in the extended state:
@Bean
public Action<String, String> executeAction() {
return ctx -> {
int approvals = (int) ctx.getExtendedState().getVariables()
.getOrDefault("approvalCount", 0);
approvals++;
ctx.getExtendedState().getVariables()
.put("approvalCount", approvals);
};
}
Junctions (Choices)
So far we’ve created state transitions which were linear in nature. This is this both uninteresting, and also does not reflect real-life use-cases that a developer will be asked to implement. Changes are that conditional paths will need to be implemented, and Spring state machine’s junctions (or choices) allow us to do just that.
First, we need to mark a state a junction (choice) in the state definition:
states
.withStates()
.junction("SJ")
Then in the transitions, we define first/then/last options which correspond to an if-then-else structure:
.withJunction()
.source("SJ")
.first("high", highGuard())
.then("medium", mediumGuard())
.last("low")
first and then take a second argument which is a regular guard which will be invoked to find out which path to take:
@Bean
public Guard<String, String> mediumGuard() {
return ctx -> false;
}
@Bean
public Guard<String, String> highGuard() {
return ctx -> false;
}
Note that a transition does not stop at a junction node but will immediately execute defined guards and go to one of the designated routes.
In the example above, instructing the state machine to transition to SJ will result in the actual state becoming low, as the both guards just return false.
One last thing to note is that the API provides both junctions and choices. However, they are functionally identical in every aspect.
Recent Stories
Top DiscoverSDK Experts
Compare Products
Select up to three two products to compare by clicking on the compare icon () of each product.
{{compareToolModel.Error}}
{{CommentsModel.TotalCount}} Comments
Your Comment