avatarNGU

Summary

Spring State Machine 4.0 helps manage complex state transitions and behavior in applications by providing a structured approach, improving maintainability, and simplifying business logic.

Abstract

Spring State Machine (SSM) is a framework built on top of the Spring platform that enables developers to implement state machines within their Spring applications. SSM offers features like simple and complex state machines, event-driven transitions, guards, actions, and type safety. By using SSM, developers can define the logic governing their application's state transitions outside of the core code, making it cleaner, more modular, and easier to reason about. SSM is beneficial for applications with state-driven behavior, complex logic management, and concurrency issues.

Bullet points

  • Spring State Machine (SSM) is a framework built on top of the Spring platform that lets you easily implement state machines within your Spring applications.
  • SSM offers features like simple and complex state machines, event-driven transitions, guards, actions, and type safety.
  • SSM improves maintainability, enhances predictability, and simplifies business logic.
  • SSM is beneficial for applications with state-driven behavior, complex logic management, and concurrency issues.
  • Real-life use cases for SSM include order lifecycle management, workflow engines, game logic, device status monitoring, session management, access control, and transaction consistency in microservice architecture.
  • Key principles for effective SSM usage include clear state modeling, explicit guards and actions, state hierarchy (optional), event-driven architecture, and testing and debugging.
  • The article provides code examples and explanations for using SSM in a Spring application.

How exactly does Spring State Machine 4.0 help us?

As usual you could find complete code example (takeaways) :)

What is State Machine?

A state machine is a way of modeling the behavior of a system in different conditions. It can be thought of as a machine that can be in one of a finite number of states at any given time. The machine changes states based on inputs it receives, and each state can perform certain actions or outputs.

Here’s a breakdown of how it works:

  • States: These are the different conditions the system can be in. For example, a traffic light might have states for red, yellow, and green.
  • Transitions: These are the rules that define how the system moves from one state to another. In the traffic light example, a transition might be “when the timer in the red state expires, move to the yellow state.”
  • Inputs: These are the events that cause the state machine to change states. Examples of inputs could be user actions, sensor readings, or external signals.
  • Outputs: These are the actions the system performs in each state. This could be anything from displaying a message on a screen to sending a signal to control a machine.

State machines are a powerful tool for modeling all sorts of systems, from simple things like traffic lights to complex systems like vending machines or video games. They are useful because they provide a clear and concise way to represent the behavior of a system, making it easier to understand, design, and debug.

What is Spring State Machine?

Spring State Machine (SSM) is a framework built on top of the Spring platform that lets you easily implement state machines within your Spring applications. It provides features to manage the states, transitions, and events that drive your application’s behavior.

Here’s what SSM offers:

  • Simple and Complex State Machines: It supports both flat, one-level state machines for basic use cases and hierarchical structures for managing complex workflows.
  • Event-Driven Transitions: The state changes are triggered by events, which can be based on user actions, sensor readings, or timers.
  • Guards and Actions: You can define conditions (guards) to control when transitions happen and actions to perform when entering or exiting states.
  • Type Safety: SSM uses a type-safe configuration approach, making your code more robust and easier to maintain.

By using SSM, you can define the logic that governs your application’s state transitions outside of your core code. This makes your code cleaner, more modular, and easier to reason about. It also simplifies handling complex workflows and state changes within your Spring applications.

Benefits of Using Spring State Machine

  • Improved Maintainability: Centralized state management promotes code clarity and reduces the risk of errors from scattered state logic.
  • Enhanced Predictability: Explicitly defined state transitions and conditions lead to more predictable application behavior.
  • Better Testability: Testable state transitions make it easier to isolate and test different application states and transitions.
  • Simplified Business Logic: By handling complex state changes and transitions, SSM reduces manual coding effort.

When to Use Spring State Machine?

You’re a good candidate to use Spring State Machine (SSM) when your application exhibits the following characteristics:

  • State-Driven Behavior: If your application’s core logic can be naturally represented as a series of states, transitions, and events, then SSM provides a structured approach to manage this complexity. Examples include order processing workflows, finite state automata implementations, or turn-based games.
  • Complex Logic Management: SSM helps break down intricate application logic into smaller, more manageable state transitions. This improves code readability and maintainability.
  • Concurrency Issues: If your application experiences concurrency problems, especially when dealing with asynchronous operations, SSM can help manage state changes in a controlled and predictable manner.

Here are some signs that using boolean flags or enums to manage states might be getting out of hand and SSM could be a better solution:

  • Conditional Logic Based on States: If your code heavily relies on boolean flags or enums to represent different application states and has a lot of conditional logic based on these states, SSM offers a more structured way to handle these transitions.
  • State-Specific Variables: If you find yourself using variables that only have meaning within a specific state of your application, SSM provides a mechanism to associate these variables with the relevant state.

The following is a real-life use cases:

Order lifecycle management

In e-commerce applications, the order status may change from creation, payment, delivery, confirmation of receipt to completion or cancellation. State machines can be used to clearly define and control the legal transition process between these states, and at the same time trigger corresponding operations when the state changes, such as sending email notifications and updating inventory.

Workflow engine

https://0097f9ca.flyingcdn.com/wp-content/uploads/2020/10/automate-any-process-anywhere.png

In enterprise applications, workflows (such as leave approval process and reimbursement process) usually have multiple steps and decision points. State machines can be used to describe the relationship and transition conditions between each step to ensure that the process proceeds according to the preset rules.

Game logic

In game development, game characters, game levels, and battle scenes all have their own states. State machines can be used to implement different action mode switching of characters, judgment of level clearance conditions, and complex logic such as battle state cycle.

Device status monitoring

In the Internet of Things (IoT) application, when tracking and managing the running status of devices in real time, state transitions can be triggered according to various signals or instructions received by the device, such as device startup, standby, running, fault, maintenance, etc. state.

Session management

In Web applications, the user’s session state (login, logout, active, timeout, etc.) can be effectively managed through state machines to simplify the complex business logic related to session state.

Access control

In systems with high security requirements, user permissions may change with their operation behavior and system status. State machines can accurately express the process of permission state changes.

Transaction consistency in microservice architecture

In scenarios involving collaboration of multiple microservices and requiring transaction consistency, state machines can be used to coordinate the state transfer of each service to ensure the consistency of the entire business process.

Then How to use it well?

Here are some key principles for effective SSM usage:

  • Clear State Modeling: Define your application states and transitions thoughtfully, ensuring a clear and well-structured state model.
  • Explicit Guards and Actions: Use guards to determine whether a transition is allowed and define actions to be executed upon state transitions.
  • State Hierarchy (Optional): For complex applications, leverage hierarchical state machines to break down complex states into sub-states for better organization.
  • Event-Driven Architecture: Design your application around events that trigger state transitions, promoting loose coupling and modularity.
  • Testing and Debugging: Use test cases to verify state transitions and utilize SSM’s debugging capabilities to identify issues.

Ok. Now lets see complete code examples:

Project structure:

Libs

<properties>
  <spring-statemachine.version>4.0.0</spring-statemachine.version>
</properties>        


        <dependency>
            <groupId>org.springframework.statemachine</groupId>
            <artifactId>spring-statemachine-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.statemachine</groupId>
            <artifactId>spring-statemachine-core</artifactId>
            <version>${spring-statemachine.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.statemachine</groupId>
            <artifactId>spring-statemachine-data-jpa</artifactId>
        </dependency>

As the diagram displayed we have:

5 State: A B C D E

4 Event: E1 E2 E3 E4.

public enum StateEnum {
    A,
    B,
    C,
    D,
    E
}


public enum EventEnum {
    E1,
    E2,
    E3,
    E4
}

Configuration Class:

defined states, transitions, config.

add guard and action into transitions.

@Slf4j
@EnableStateMachineFactory
@Configuration
public class StateMachineMainConfig extends StateMachineConfigurerAdapter<StateEnum, EventEnum> {

    @Autowired
    private StateMachineRuntimePersister<StateEnum, EventEnum, String> stateMachineRuntimePersister;

    @Autowired
    StateMachineListener<StateEnum, EventEnum> stateMachineListener;

    @Autowired
    MyGuard myGuard;

    @Autowired
    LogAction logAction;

    @Override
    public void configure(StateMachineStateConfigurer<StateEnum, EventEnum> states) throws Exception {
        states.withStates()
                .initial(StateEnum.A)
                .states(EnumSet.allOf(StateEnum.class))
                .end(StateEnum.E);
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<StateEnum, EventEnum> transitions) throws Exception {
        transitions
                .withExternal()
                .source(StateEnum.A).target(StateEnum.B).event(EventEnum.E1)
                .guard(myGuard)
                .and()
                .withExternal()
                .source(StateEnum.B).target(StateEnum.C).event(EventEnum.E2)
                .and()
                .withExternal()
                .source(StateEnum.C).target(StateEnum.D).event(EventEnum.E3)
                .and()
                .withExternal()
                .source(StateEnum.D).target(StateEnum.E).event(EventEnum.E4)
                .action(logAction);

    }

    @Override
    public void configure(StateMachineConfigurationConfigurer<StateEnum, EventEnum> config) throws Exception {
        config.withConfiguration()
                .autoStartup(true)
                .listener(stateMachineListener)
                .and()
                .withPersistence()
                .runtimePersister(stateMachineRuntimePersister);

    }
}

The StateMachineListenerConfig

@Configuration
@Slf4j
public class StateMachineListenerConfig {

    @Bean
    public StateMachineListener<StateEnum, EventEnum> stateMachineListener() {
        return new StateMachineListenerAdapter<StateEnum, EventEnum>() {
            @Override
            public void stateChanged(State<StateEnum, EventEnum> from, State<StateEnum, EventEnum> to) {
                log.info("Transitioned from {} to {}", from == null ? "none" : from.getId(), to.getId());
            }

            @Override
            public void stateEntered(State<StateEnum, EventEnum> state) {
                log.info("Entered state: {}", state.getId());
            }

            @Override
            public void stateExited(State<StateEnum, EventEnum> state) {
                log.info("Exited state: {}", state.getId());
            }

            @Override
            public void eventNotAccepted(Message<EventEnum> event) {
                log.info("Event not accepted: {}", event.getPayload());
            }

            @Override
            public void transition(Transition<StateEnum, EventEnum> transition) {
                log.info("Transition: {} -> {}",
                        transition.getSource() != null ? transition.getSource().getId() : transition.getSource(),
                        transition.getTarget().getId());
            }

            @Override
            public void stateMachineError(StateMachine<StateEnum, EventEnum> stateMachine, Exception exception) {
                log.info("stateMachineError: {}", stateMachine.getId(), exception);
            }

            @Override
            public void stateContext(StateContext<StateEnum, EventEnum> stateContext) {
                //log.info("stateContent: {}", stateContext);
            }
        };
    }
}

The StateMachinePersistenceConfig

@Configuration
public class StateMachinePersistenceConfig {

    @Bean
    public StateMachineRuntimePersister<StateEnum, EventEnum, String> stateMachineRuntimePersister(
            final JpaStateMachineRepository jpaStateMachineRepository) {
        return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository);
    }
}

The StateMachineServiceConfig

@Configuration
public class StateMachineServiceConfig {

    @Autowired
    StateMachineFactory<StateEnum, EventEnum> stateMachineFactory;

    @Autowired
    StateMachineRuntimePersister<StateEnum, EventEnum, String> stateMachineRuntimePersister;

    @Bean
    public StateMachineService<StateEnum, EventEnum> stateMachineService() {
        return new DefaultStateMachineService<>(stateMachineFactory, stateMachineRuntimePersister);
    }
}

The LogAction

@Slf4j
@Service
public class LogAction implements Action<StateEnum, EventEnum> {
  @Override
  public void execute(StateContext<StateEnum, EventEnum> stateContext) {
    log.info("Log Action Executed. {}", stateContext.getStateMachine().getState().getId());
  }
}

The Guard

which verify the machineId is exsited or not.

@Service
public class MyGuard implements Guard<StateEnum, EventEnum> {
    @Override
    public boolean evaluate(StateContext<StateEnum, EventEnum> stateContext) {
        StateEnum currentState = stateContext.getStateMachine().getState().getId();
        EventEnum event = stateContext.getEvent();
        ExtendedState extendedState = stateContext.getExtendedState();

        if (currentState == StateEnum.A && event == EventEnum.E1) {
            if (!StringUtils.hasText(extendedState.get("machineId", String.class))) {
                throw new RuntimeException("uuid not found!");
            }
            return true;
        }
        return false;
    }
}

Now lets see some usages

@Slf4j
@SpringBootTest
class SimpleDemoStateMachineApplicationTests {

    @Autowired
    private StateMachineService<StateEnum, EventEnum> stateMachineService;

    StateMachine<StateEnum, EventEnum> stateMachine;

    @BeforeEach
    void init () {
        stateMachine = stateMachineService.acquireStateMachine(UUID.randomUUID().toString(), true);
    }

    @Test
    void testSendEvent() {
        log.info("Initial: {}", stateMachine.getState().getId());

        stateMachine.sendEvent(Mono.just(MessageBuilder.withPayload(EventEnum.E1).build())).subscribe();
        stateMachine.sendEvent(Mono.just(MessageBuilder.withPayload(EventEnum.E2).build())).subscribe();
        stateMachine.sendEvent(Mono.just(MessageBuilder.withPayload(EventEnum.E3).build())).subscribe();
        stateMachine.sendEvent(Mono.just(MessageBuilder.withPayload(EventEnum.E4).build())).subscribe();
    }

    @Test
    void testSendEventCollect_byOrder(){
        log.info("Initial: {}", stateMachine.getState().getId());

        stateMachine.sendEventCollect(Mono.just(MessageBuilder.withPayload(EventEnum.E1).build()))
                .flatMap(results -> stateMachine.sendEventCollect(Mono.just(MessageBuilder.withPayload(EventEnum.E2).build())))
                .flatMap(results -> stateMachine.sendEventCollect(Mono.just(MessageBuilder.withPayload(EventEnum.E3).build())))
                .flatMap(results -> stateMachine.sendEventCollect(Mono.just(MessageBuilder.withPayload(EventEnum.E4).build())))
                .subscribe();
    }


    @Test
    void testPassParametersWithGuard() {
        String stateMachineId = stateMachine.getId();
        Map<String, Object> variables = new HashMap<>();
        variables.put("machineId", stateMachineId);
        variables.put("content", "sth important");
        stateMachine.getExtendedState().getVariables().putAll(variables);

        stateMachine.sendEvent(Mono.just(MessageBuilder.withPayload(EventEnum.E1).build())).subscribe();
    }

    @Test
    void testPassParametersWithGuard_WithAction() {
        String stateMachineId = stateMachine.getId();
        Map<String, Object> variables = new HashMap<>();
        variables.put("machineId", stateMachineId);
        variables.put("content", "sth important");
        stateMachine.getExtendedState().getVariables().putAll(variables);

        stateMachine.sendEventCollect(Mono.just(MessageBuilder.withPayload(EventEnum.E1).build()))
                .flatMap(results -> stateMachine.sendEventCollect(Mono.just(MessageBuilder.withPayload(EventEnum.E2).build())))
                .flatMap(results -> stateMachine.sendEventCollect(Mono.just(MessageBuilder.withPayload(EventEnum.E3).build())))
                .flatMap(results -> stateMachine.sendEventCollect(Mono.just(MessageBuilder.withPayload(EventEnum.E4).build())))
                .subscribe();
    }


}

in UT testSendEvent() we can see:

Guard’s verify is working

in UT testPassParametersWithGuard_WithAction() we can see the result:

log Action executed.

there are more API we could use in lib 4.0.0+. I will update them soon when there is very good usage example to show you.

That it.

Thanks for reading! If you like it or feel it helped pls click Applaud. Thanks :)

Happy coding. See you next time.

Programming
Coding
Java
Spring
Spring Boot
Recommended from ReadMedium