21. Context Integration

It is a little limited to do interaction with a state machine by either listening its events or using actions with states and transitions. Time to time this approach would be too limited and verbose to create interaction with the application a state machine is working with. For this specific use case we have made a spring style context integration which easily attach state machine functionality into your beans.

Available annotations has been harmonised to enable access to same state machine execution points than what is available from Chapter 20, Listening State Machine Events.

@WithStateMachine annotation can be used to associate a state machine with an existing bean. Then it is possible to start adding supported annotations to methods of that bean.

@WithStateMachine
public class Bean1 {

    @OnTransition
    public void anyTransition() {
    }
}

It is also possible to attach to any other state machine from an application context by using annotation name field.

@WithStateMachine(name = "myMachineBeanName")
public class Bean2 {

    @OnTransition
    public void anyTransition() {
    }
}

Sometimes it is more convenient to use machine id which is something user can set to better identify multiple instances. This id maps to getId() method in a StateMachine interface.

@WithStateMachine(id = "myMachineId")
public class Bean16 {

    @OnTransition
    public void anyTransition() {
    }
}

@WithStateMachine can also be used as a meta-annotation as shown above. In this case you could annotate your bean with WithMyBean.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@WithStateMachine(name = "myMachineBeanName")
public @interface WithMyBean {
}
[Note]Note

Return type of these methods doesn’t matter and is effectively discard.

21.1 Enabling Integration

All features for @WithStateMachine can be enabled by using annotation @EnableWithStateMachine which simply imports needed configuration into Spring Application Context. Both @EnableStateMachine and @EnableStateMachineFactory are already annotated with this so there is no need for user to add it again. However if machine is build and configured without a use of configuration adapters, @EnableWithStateMachine must be used order to use features with @WithStateMachine. Idea for this is shown below.

public static StateMachine<String, String> buildMachine(BeanFactory beanFactory) throws Exception {
    Builder<String, String> builder = StateMachineBuilder.builder();

    builder.configureConfiguration()
        .withConfiguration()
            .machineId("myMachineId")
            .beanFactory(beanFactory);

    builder.configureStates()
        .withStates()
            .initial("S1")
            .state("S2");

    builder.configureTransitions()
        .withExternal()
            .source("S1")
            .target("S2")
            .event("E1");

    return builder.build();
}

@WithStateMachine(id = "myMachineId")
static class Bean17 {

    @OnStateChanged
    public void onStateChanged() {
    }
}
[Important]Important

If machine is not created as a Bean then it is mandatory to set BeanFactory for a machine as shown above. Otherwise machine will be unaware of handlers calling your @WithStateMachine methods.

21.2 Method Parameters

Every annotation is supporting exactly same set of possible method parameters but runtime behaviour is different depending on an annotation itself and a stage where annotated method is called. To better understand how context works see Chapter 18, Using StateContext.

[Note]Note

For differences between method parameters, see individual annotation docs below.

Effectively all annotated methods are called using Spring SPel expressions which are build dynamically during the process. As to make this work these expressions needs to have a root object it evaluates against. This root object is a StateContext and we have also made some tweaks internally so that it is possible to access StateContext methods directly without going through the context handle.

Simplest method parameter would naturally be a StateContext itself.

@WithStateMachine
public class Bean3 {

    @OnTransition
    public void anyTransition(StateContext<String, String> stateContext) {
    }
}

Rest of the StateContext content can be accessed as shown below. Number of parameters or order of those doesn’t matter.

@WithStateMachine
public class Bean4 {

    @OnTransition
    public void anyTransition(
            @EventHeaders Map<String, Object> headers,
            ExtendedState extendedState,
            StateMachine<String, String> stateMachine,
            Message<String> message,
            Exception e) {
    }
}

21.3 Transition Annotations

Annotations for transitions are OnTransition, OnTransitionStart and OnTransitionEnd.

These annotations behave exactly same and let’s see how OnTransition is used. Within this annotation a property’s source and target can be used to qualify a transition. If source and target is left empty then any transition is matched.

@WithStateMachine
public class Bean5 {

    @OnTransition(source = "S1", target = "S2")
    public void fromS1ToS2() {
    }

    @OnTransition
    public void anyTransition() {
    }
}

Default @OnTransition annotation can’t be used with a state and event enums user have created due to java language limitations, thus string representation have to be used.

Additionally it is possible to access Event Headers and ExtendedState by adding needed arguments to a method. Method is then called automatically with these arguments.

@WithStateMachine
public class Bean6 {

    @StatesOnTransition(source = States.S1, target = States.S2)
    public void fromS1ToS2(@EventHeaders Map<String, Object> headers, ExtendedState extendedState) {
    }
}

However if you want to have a type safe annotation it is possible to create a new annotation and use @OnTransition as meta annotation. This user level annotation can make a reference to actual states and events enums and framework will try to match these in a same way.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@OnTransition
public @interface StatesOnTransition {

    States[] source() default {};

    States[] target() default {};
}

Above we created a @StatesOnTransition annotation which defines source and target as a type safe manner.

@WithStateMachine
public class Bean7 {

    @StatesOnTransition(source = States.S1, target = States.S2)
    public void fromS1ToS2() {
    }
}

In your own bean you can then use this @StatesOnTransition as is and use type safe source and target.

21.4 State Annotations

Annotations for states are OnStateChanged, OnStateEntry and OnStateExit.

@WithStateMachine
public class Bean8 {

    @OnStateChanged
    public void anyStateChange() {
    }
}

In a same way that in transition annotations it’s possible to define target and source states.

@WithStateMachine
public class Bean9 {

    @OnStateChanged(source = "S1", target = "S2")
    public void stateChangeFromS1toS2() {
    }
}

For type safety a new annotation needs to be created for enums with OnStateChanged as a meta annotation.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@OnStateChanged
public @interface StatesOnStates {

    States[] source() default {};

    States[] target() default {};
}
@WithStateMachine
public class Bean10 {

    @StatesOnStates(source = States.S1, target = States.S2)
    public void fromS1ToS2() {
    }
}

Methods for state entry and exit behave in a same way.

@WithStateMachine
public class Bean11 {

    @OnStateEntry
    public void anyStateEntry() {
    }

    @OnStateExit
    public void anyStateExit() {
    }
}

21.5 Event Annotation

There is one event related annotation named OnEventNotAccepted. It is possible to listen only specific event by defining event property with the annotation.

@WithStateMachine
public class Bean12 {

    @OnEventNotAccepted
    public void anyEventNotAccepted() {
    }

    @OnEventNotAccepted(event = "E1")
    public void e1EventNotAccepted() {
    }
}

21.6 State Machine Annotations

Annotations for state machine are OnStateMachineStart, OnStateMachineStop and OnStateMachineError.

During a state machine start and stop lifecycle methods are called.

@WithStateMachine
public class Bean13 {

    @OnStateMachineStart
    public void onStateMachineStart() {
    }

    @OnStateMachineStop
    public void onStateMachineStop() {
    }
}

In case a state machine goes into an error with exception, below annotation is called.

@WithStateMachine
public class Bean14 {

    @OnStateMachineError
    public void onStateMachineError() {
    }
}

21.7 Extended State Annotation

There is one extended state related annotation named OnExtendedStateChanged. It’s also possible to listen changes only for specific key changes.

@WithStateMachine
public class Bean15 {

    @OnExtendedStateChanged
    public void anyStateChange() {
    }

    @OnExtendedStateChanged(key = "key1")
    public void key1Changed() {
    }
}