Introducing Spring State Machine
Usually, the first problem that must be solved in the implementation of a software project is to design a well-formed application architecture. Alongside 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 case 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 typically 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 blog I would like to present the relatively recently introduced project Spring State Machine (SSM) and demonstrate its use with a small example. Imagine one need to create Miniapplication providing simple processing of orders in a webshop. An order can be canceled after being created in any status before shipment. In this simple model we do not take into account the different possibilities of delivery and payment methods. We do not use any guards or actions. The status diagram looks like this. SSM provides a compact framework for application developers using the concept of traditional model of a finite state machine in combination with some of the Spring core frameworks. SSM provides the fol-lowing 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.
Following modules builds the bare-bone of SSM:
- spring-statemachine-core : core system of SSM,
- spring-statemachine-recipes-common : commonly used examples and recipes of a core framework,
- sring-statemachine-zookeeper : Zookeeper integration for distribute SM,
- spring-statemachine-test : Module supporting testing SSM.
Typically having a dependency on the core module in your project is enough to implement an SSM-based application.
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
<version>1.0.1.RELEASE</version>
</dependency>
SSM supports two ways of implementing states and events:
- string
- enumeration
When we decide to use enumerations, states and events must be hardcoded for type safety:
public enum States {
ORDERED, ASSEMBLED, DELIVERED, INVOICED, PAYED, CANCELLED, RETURNED
}
public enum Events {
order, assemble, deliver, release_invoice, payment_received, cancel, claim, reassemble
}
The next step one need to do, is to configure the SM to build all states and transitions and bind them to the appropriate events. The SSM framework provides a typed interface StateMachineConfigurer. Adapters already exist thus we only need to override them in order to build an instance of a SM.
@Configuration
@EnableStateMachine
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<States, Events> …
As we know it from the Spring framework, the configuration must be marked with an annotation @Configuration to be loaded by a Spring context. The second annotation @EnableStateMachine tells the context that an instance of a SM can be built and started immediately on application startup. In some scenarios it is useful to start an instance of SM depending on a business logic. In that case the configuration must be annotated as a factory @EnableStateMachineFactory. Instance of SM is not cre-ated immediately on startup but started through the factory. The states must be added using a configuration method while initial- and end- states are optional and can be omitted. The initial state is assigned immediately after the SM is created. For the end state ap-plies the end of the lifecycle of the SM instance.
@Override
public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception {
states
.withStates()
.initial(States.ORDERED)
.end(States.PAYED)
.states(EnumSet.allOf(States.class));
Transitions between all states must be built and marked by triggering events.
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception {
transitions
.withExternal()
.source(States.ORDERED)
.target(States.ASSEMBLED)
.event(Events.assemble)
.and()
.withExternal()
.source(States.ASSEMBLED)
.target(States.DELIVERED)
.event(Events.deliver)
.and()
.withExternal()
.source(States.DELIVERED)
.target(States.INVOICED)
.event(Events.release_invoice)
.and()
.withExternal()
.source(States.INVOICED)
.target(States.PAYED)
.event(Events.payment_received)
.and()
.withExternal()
.source(States.ORDERED)
.target(States.CANCELLED)
.event(Events.cancel)
.and()
.withExternal()
.source(States.ASSEMBLED)
.target(States.CANCELLED)
.event(Events.cancel)
.and()
.withExternal()
.source(States.DELIVERED)
.target(States.RETURNED)
.event(Events.claim)
.and()
.withExternal()
.source(States.INVOICED)
.target(States.RETURNED)
.event(Events.claim)
.and()
.withExternal()
.source(States.RETURNED)
.target(States.CANCELLED)
.event(Events.cancel)
.and()
.withExternal()
.source(States.RETURNED)
.target(States.ASSEMBLED)
.event(Events.reassemble);
}
One can notice in a configuration, that some transitions are triggered by the same event names. This should work until there do not exist confusing doubled outgoing transitions in any of the states in the SM (in such cases the SM configurator would claim). In order to track state changes one need to add a listener. In our case we only print out the current state. The listener is built on a separate adapter StateMachineListenerAdapter:
@Override
public void configure(StateMachineConfigurationConfigurer<States, Events> config) throws Exception {
config
.withConfiguration()
.autoStartup(true)
.listener(new StateMachineListener());
}
private static final class StateMachineListener extends StateMachineListenerAdapter<States, Events> {
@Override
public void stateChanged(State<States, Events> from, State<States, Events> to) {
System.out.println("Order state changed to " + to.getId());
}
}
After the configuration is done, a typed instance of StateMachine<States, Event> can immediately be accessed and autowired to the business logic. An initial state States.ORDERED is already triggered. Driving a SM is realized via transitions which are triggered by the appropriate events:
@Autowired
private StateMachine<States, Events> stateMachine;
public void run() {
stateMachine.sendEvent(Events.assemble);
stateMachine.sendEvent(Events.deliver);
}
Reading out the console one can see the expected behaviour: >Order state changed to ORDERED >Order state changed to ASSEMLED >Order state changed to DELIVERED To interact with the Spring application, SSM uses the Spring event based infrastructure. The SM will send context events via StateMachineEventPublisher. The default implementation is automatically cre-ated if @Configuration is annotated with @EnableStateMachine. Using the SM listener class callbacks one can receive events as they come up. This is very useful, when one need to know, that an event change happened on a SM. If one need to do some application activities outside the SM which can lead to break the transition e.g. track changes in a repository, this will not be the right choice in case ex-ceptions can be raised. Instead one should use the second option which is the SM interceptor mechanism. The concept of an interceptor is a relatively deep internal feature and thus is not exposed directly through the StateMa-chine interface. It sticks on an appropriate instance of a SM and acts as stable part of it. The opposite to the listener concept, the interceptors, can break the state change due exception mechanism. An in-terceptor must be registered via StateMachineAccessor even before the SM has been initialized. Because this application handles states of every order coming to the system with the same instance of the SM, one need to put the information about the concrete order on the event. For this purpose, an interface can be created:
public interface OrderStateChangeListener {
void onStateChange(State<States, Events> state, Message<Events> message);
}
The order related to the event is wrapped to the appropriate message object context. The publisher and consumers must then be aware of this type of object. While the message is a standard Spring messaging framework object where the payload is of type Events, an order ID is set to the header of this message. The main part of the interception logic is built upon a component named OrdersStateHandler. Creating a lifecycle object handler enables us to instruct the SM accessor to use an additional interceptor for events related handling even before SM instance started:
@Component
public class OrdersStateHandler extends LifecycleObjectSupport {
@Autowired
private StateMachine<States, Events> stateMachine;
private Set<OrderStateChangeListener> listeners = new HashSet<>();
@Override
protected void onInit() throws Exception {
stateMachine
.getStateMachineAccessor()
.doWithAllRegions(new StateMachineFunction<State-MachineAccess<States, Events>>() {
@Override
public void apply(StateMachineAccess<States, Events> function) {
function.addStateMachineInterceptor(new StateMachineInterceptorAdapter<States, Events>() {
@Override
public void preStateChange(State<States, Events> state, Message message) {
listeners.forEach(listener -> listener.onStateChange(state, message));
}
});
}
});
}
public void registerListener(OrderStateChangeListener listener) {
listeners.add(listener);
}
public void handleEvent(Message event, States sourceState)
{
stateMachine.stop();
stateMachine
.getStateMachineAccessor()
.doWithAllRegions(access -> access.resetStateMa-chine(new DefaultStateMachineContext<States, Events>(sourceState, null, null, null)));
stateMachine.start();
stateMachine.sendEvent(event);
}
}
The events now shall not be send directly to the SM, but posted via a handler. One instance of the SM is shared across the application for many different orders. Every time a state change event is pub-lished, the handler first does a reset of the SM to the appropriate source state before changing it. At this point the core SM logic has been implemented. Now one need to let publishers and all consumers know about the handler. For the publisher responsible for delivering the order to the customer it looks like this:
stateHandler.handleEvent(
MessageBuilder
.withPayload(Events.deliver)
.setHeader("order-id", or-derId)
.build(), States.ASSEMBLED);
Consumer in this case must implement the appropriate interface OrderStateChangeListener and regis-ter itself by a handler. A possible consumer listener could be a persistence service updating the state to the database:
@Override
public void onStateChange(State<States, Events> state, Message message) {
Long orderId = message.getHeaders().get("order-id", Long.class);
Order order = repository.findOne(orderId);
order.setState(state.getId());
repository.save(order);
}
States and events within a single Spring state machine are really simple to understand. With the ex-ample implementation of a SM introduced in this article, a robust application can be obtained handling use cases, which many developers are facing off today. There is no need to draw any workflow charts nor business process UMLs, while the whole logic is simplified thanks to the limited amount of classes. With the Zookeeper SM extension a distributed SM can be gained, but the current functionality is still a preview feature that did not reach the expected maturity. The SSM implementer has to be aware that the Spring application context is not the fastest event bus out there so it is advised to give some thought about the rate of events your application is going to send using the state machine.