Introducing Spring State Machine

January 7, 2016

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:

Good candidates for a state machine typically have the following aspects:

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.   order 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:

Following modules builds the bare-bone of 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:

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.

About the author: Vladimir Klepoch
Comments
Join us