I am trying to extend the Persist sample of spring statemachine to two different state machine configurations. http://docs.spring.io/spring-statemachine/docs/1.0.0.RELEASE/reference/htmlsingle/#statemachine-examples-persist
Therefor I
No big deal so far. Now to the Configuration:
Furthermore I adapted the AbstractStateMachineCommands class and autowire a list of statemachines in there. The start/stop and state methods now start/stop and print state of every state machine (here I do not care about print, variables).
The occuring problem is:
So, can anybody tell me where I messed it up? Or is the Persist recipe not applicable to two state machines?
Thanks a lot.
Code Examples: The Application.java now contains these configs and entities:
@Configuration
static class PersistHandlerConfig {
@Bean
public Persist persist() throws Exception {
return new Persist(persistStateMachineHandler());
}
@Bean
public PersistStateMachineHandler persistStateMachineHandler() throws Exception {
return new PersistStateMachineHandler(persistSm());
}
@Bean
public StateMachine<String, String> persistSm() throws Exception{
Builder<String, String> builder = StateMachineBuilder.builder();
builder.configureStates()
.withStates()
.initial("PLACED")
.state("PROCESSING")
.state("SENT")
.state("DELIVERED");
builder.configureTransitions()
.withExternal()
.source("PLACED").target("PROCESSING")
.event("PROCESS")
.and()
.withExternal()
.source("PROCESSING").target("SENT")
.event("SEND")
.and()
.withExternal()
.source("SENT").target("DELIVERED")
.event("DELIVER");
return builder.build();
}
}
@Configuration
static class TicketPersistHandlerConfig {
@Bean
public TicketPersist ticketPersist() throws Exception {
return new TicketPersist(ticketPersistStateMachineHandler());
}
@Bean
public PersistStateMachineHandler ticketPersistStateMachineHandler() throws Exception {
return new PersistStateMachineHandler(buildMachine());
}
@Bean
public StateMachine<String, String> buildMachine() throws Exception {
Builder<String, String> builder = StateMachineBuilder.builder();
builder.configureStates()
.withStates()
.initial("PRINTED")
.state("BOOKED")
.state("SOLD")
.state("DELIVERED");
builder.configureTransitions()
.withExternal()
.source("PRINTED").target("BOOKED")
.event("BOOK")
.and()
.withExternal()
.source("BOOKED").target("SOLD")
.event("SELL")
.and()
.withExternal()
.source("SOLD").target("DELIVERED")
.event("DELIVER");
return builder.build();
}
}
public static class Order {
int id;
String state;
public Order(int id, String state) {
this.id = id;
this.state = state;
}
@Override
public String toString() {
return "Order [id=" + id + ", state=" + state + "]";
}
}
public static class Ticket {
int id;
String state;
public Ticket(int id, String state) {
this.id = id;
this.state = state;
}
@Override
public String toString() {
return "Ticket [id=" + id + ", state=" + state + "]";
}
}
TicketPersist.java and TicketPersistCommands.java are the same like the ones for orders (just replaced order(s) with ticket(s)). I adapted AbstractStateMachineCommands in the following way:
@Autowired
private List<StateMachine<S, E>> stateMachines;
@CliCommand(value = "sm start", help = "Start a state machine")
public String start() {
for (StateMachine<S, E> stateMachine : stateMachines)
{
stateMachine.start();
}
return "State machines started";
}
@CliCommand(value = "sm stop", help = "Stop a state machine")
public String stop() {
for (StateMachine<S, E> stateMachine : stateMachines)
{
stateMachine.stop();
}
return "State machines stopped";
}
There is a conceptual difference between plain annotation configuration(use of @EnableStateMachine
and adapter) and manual builder. Latter is really meant to be used outside of spring app context and while you can then register machine created from it as bean(like you tried to do) a lot of automatic configuration is not applied. I'll probably need to pay more attention of this use case in test(where user returns machine from builder registered as @Bean
).
If you get NPE when two machines are created with @EnableStateMachine
, that's a bug I need to look into. You should use name
field with @EnableStateMachine
indicating a bean name adapter/javaconfig would use if wanting to create multiple machines. @EnableStateMachine
defaults to bean name stateMachine
and having multiple @EnableStateMachine
adapters with same name would try to configure same machine. With multiple machines it'd be something like @EnableStateMachine(name = "sm1")
.
Trouble with TaskExecutor
is kinda obvious but none of a machine should not work with a code you posted because I don't see it created anywhere. Normally TaskExecutor
is coming either explicitely set instance or from bean factory(if it's set) as a fallback. There's hooks for setting these in config interfaces http://docs.spring.io/spring-statemachine/docs/1.0.0.RELEASE/reference/htmlsingle/#statemachine-config-commonsettings.
These samples on default use @EnableStateMachine
which does context integration automatically meaning spring application context event publisher is also registered(which doesn't happen with machines from manual builder), thus normal ways to create ApplicationListener
as done in https://github.com/spring-projects/spring-statemachine/blob/master/spring-statemachine-samples/src/main/java/demo/CommonConfiguration.java#L57 no longer works. You can also use StateMachineListenerAdapter
and register those with machine via configuration.
I would not try to build any apps around specific shell concepts in samples. Shell was just used to give easy way to interact with machine from a command line. I looks like you might get away from all trouble by using different bean names for all machines, i.e. @EnableStateMachine(name = "sm1")
.
I'll try to create some gh issues based on these use cases. There always seem to be different ways how people try to use this stuff what we're not anticipated in our tests.