13. State Machine Factories

There are use cases when state machine needs to be created dynamically instead of defining static configuration at compile time. For example if there are custom components which are using its own state machines and these components are created dynamically it is impossible to have a static state machined build during the application start. Internally state machines are always build via a factory interfaces and this then gives user an option to use this feature programmatically. Configuration for state machine factory is exactly same as you’ve seen in various examples in this document where state machine configuration is hard coded.

13.1 Factory via Adapter

Actually creating a state machine using @EnableStateMachine will work via factory so @EnableStateMachineFactory is merely exposing that factory via its interface.

@Configuration
@EnableStateMachineFactory
public class Config6
        extends EnumStateMachineConfigurerAdapter<States, Events> {

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

}

Now that you’ve used @EnableStateMachineFactory to create a factory instead of a state machine bean, it can be injected and used as is to request new state machines.

public class Bean3 {

    @Autowired
    StateMachineFactory<States, Events> factory;

    void method() {
        StateMachine<States,Events> stateMachine = factory.getStateMachine();
        stateMachine.start();
    }
}

13.1.1 Adapter Factory Limitations

Current limitation of factory is that all actions and guard it is associating with created state machine will share a same instances. This means that from your actions and guard you will need to specifically handle a case that same bean will be called by a different state machines. This limitation is something which will be resolved in future releases.

13.2 State Machine via Builder

Using adapters shown above has a limitation imposed by its requirement to work via Spring @Configuration classes and application context. While this is a very clear model to configure a state machine instances it will limit configuration at a compile time which is not always what a user wants to do. If there is a requirement to build more dynamic state machines, a simple builder pattern can be used to construct similar instances. Using strings as states and events this builder pattern can be used to build fully dynamic state machines outside of a Spring application context as shown above.

StateMachine<String, String> buildMachine1() throws Exception {
    Builder<String, String> builder = StateMachineBuilder.builder();
    builder.configureStates()
        .withStates()
            .initial("S1")
            .end("SF")
            .states(new HashSet<String>(Arrays.asList("S1","S2","S3","S4")));
    return builder.build();
}

Builder is using same configuration interfaces behind the scenes that the @Configuration model using adapter classes. Same model goes to configuring transitions, states and common configuration via builder’s methods. This simply means that whatever you can use with a normal EnumStateMachineConfigurerAdapter or StateMachineConfigurerAdapter can be used dynamically via a builder.

[Note]Note

Currently builder.configureStates(), builder.configureTransitions() and builder.configureConfiguration() interface methods cannot be chained together meaning builder methods needs to be called individually.

StateMachine<String, String> buildMachine2() throws Exception {
    Builder<String, String> builder = StateMachineBuilder.builder();
    builder.configureConfiguration()
        .withConfiguration()
            .autoStartup(false)
            .beanFactory(null)
            .taskExecutor(null)
            .taskScheduler(null)
            .listener(null);
    return builder.build();
}

It is important to understand on what cases common configuration needs to be used with a machines instantiated from a builder. Configurer returned from a withConfiguration() can be used to setup autoStart, TaskScheduler, TaskExecutor, BeanFactory and additionally register a StateMachineListener. If StateMachine instance returned from a builder is registered as a bean via @Bean, i.e. BeanFactory is attached automatically and then a default TaskExecutor can be found from there. If instances are used outside of a spring application context these methods must be used to setup needed facilities.