awesome-state-machine is a Spring-Boot library which aims to provide an easy-to-use state machine for Spring-Boot with persistence of the state.
Add the following Maven dependency
<dependency>
<groupId>net.consensys.spring</groupId>
<artifactId>awesome-state-machine</artifactId>
<version>0.0.2T</version>
</dependency>
Declare an Enumeration with every states
public enum TaskState {
OPENED, IN_PROGRESS, CLOSED, CANCELED;
}
Declare an Enumeration to list all events that can change the state
public enum TaskEvent {
START_WORKING, END_TASK;
}
Add a field annotated with @State on the entity field that drives the state
@Data @NoArgsConstructor
@Document
public class Task {
private @Id String id;
private @State TaskState state;
private String taskContent;
}
Configure your state machine by creating a @Configuration class that extends StateMachineConfiguration<S, E, T, I>
| Type | Description |
|---|---|
| S | State enumeration |
| E | Event enumeration |
| T | Entity type |
| I | ID type |
Parameters:
| Name | Type | Mandatory | Description |
|---|---|---|---|
| repository | CRUDRepository | no | CRUDRepository used to retrieve before and persist the entity after the state transition |
| transitions | Transition[] | yes | List of transitions |
Configure a transitions:
Call add(transition().field1().field2().fieldn().build());
| Name | Type | Mandatory | Description |
|---|---|---|---|
| event | Event | yes [1] | Event triggered |
| from | State | yes [1] | Initial state |
| to | State | yes [1, n] | Target states (if multiple target states, a condition is required to take the right branch) |
| before | Consumer<Entity, Context> | no [0, 1] | Execute the consumer before the state transition |
| after | Consumer<Entity, Context> | no [0, 1] | Execute the consumer after the state transition |
@Configuration
static class MyStateMachineConfiguration extends StateMachineConfiguration<TaskState, TaskEvent, Task, String> {
@Autowired
public ConfigurationNoRepository(TaskRepository repository) {
super(repository);
add(transition()
.event(TaskEvent.START_WORKING)
.from(TaskState.OPENED)
.to(TaskState.IN_PROGRESS)
.before((e, c) -> log.info("running before with entity {} and context {}", e, c))
.after((e, c) -> log.info("running after with entity {} and context {}", e, c))
.build());
add(transition()
.event(TaskEvent.END_TASK)
.from(TaskState.IN_PROGRESS)
.to(TaskState.CLOSED, (e, c) -> !e.getValue().equals("cancel"))
.to(TaskState.CANCELED, (e, c) -> e.getValue().equals("cancel"))
.build());
}
}
After the configuration loaded, a StateMachine<S, E, T, I> service is available to trigger transition based on event
| Type | Description |
|---|---|
| S | State enumeration |
| E | Event enumeration |
| T | Entity type |
| I | ID type |
Service Bean
@Autowired
StateMachine<TaskState, TaskState, Task, String> stateMachine;
Signal a transition
stateMachine.onTransition(TaskEvent.START_WORKING, task.getId());
Pass a context object (Object) that can be used in before and after consumers. (use case: context could be Principal to check permission to change the state)
stateMachine.onTransition(TaskEvent.START_WORKING, task.getId(), contextObject);