Um guia para o projeto Spring State Machine
1. Introdução
Este artigo é focado emState Machine project do Spring - que pode ser usado para representar fluxos de trabalho ou qualquer outro tipo de problema de representação de autômato de estado finito.
2. Dependência do Maven
Para começar, precisamos adicionar a principal dependência do Maven:
org.springframework.statemachine
spring-statemachine-core
1.2.3.RELEASE
A versão mais recente desta dependência pode ser encontradahere.
3. Configuração de máquina de estado
Agora, vamos começar definindo uma máquina de estado simples:
@Configuration
@EnableStateMachine
public class SimpleStateMachineConfiguration
extends StateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer states)
throws Exception {
states
.withStates()
.initial("SI")
.end("SF")
.states(
new HashSet(Arrays.asList("S1", "S2", "S3")));
}
@Override
public void configure(
StateMachineTransitionConfigurer transitions)
throws Exception {
transitions.withExternal()
.source("SI").target("S1").event("E1").and()
.withExternal()
.source("S1").target("S2").event("E2").and()
.withExternal()
.source("S2").target("SF").event("end");
}
}
Observe que esta classe é anotada como uma configuração convencional do Spring e também como uma máquina de estado. Ele também precisa estenderStateMachineConfigurerAdapter para que vários métodos de inicialização possam ser chamados. Em um dos métodos de configuração, definimos todos os estados possíveis da máquina de estados; no outro, como os eventos alteram o estado atual.
A configuração acima define uma máquina de estado de transição bastante simples e linear que deve ser fácil de seguir.
Agora precisamos iniciar um contexto Spring e obter uma referência à máquina de estado definida por nossa configuração:
@Autowired
private StateMachine stateMachine;
Depois de termos a máquina de estado, ela precisa ser iniciada:
stateMachine.start();
Agora que nossa máquina está no estado inicial, podemos enviar eventos e, assim, acionar transições:
stateMachine.sendEvent("E1");
Sempre podemos verificar o estado atual da máquina de estado:
stateMachine.getState();
4. Ações
Vamos adicionar algumas ações a serem executadas em torno das transições de estado. Primeiro, definimos nossa ação como um bean Spring no mesmo arquivo de configuração:
@Bean
public Action initAction() {
return ctx -> System.out.println(ctx.getTarget().getId());
}
Em seguida, podemos registrar a ação criada acima na transição em nossa classe de configuração:
@Override
public void configure(
StateMachineTransitionConfigurer transitions)
throws Exception {
transitions.withExternal()
transitions.withExternal()
.source("SI").target("S1")
.event("E1").action(initAction())
Esta ação será executada quando ocorrer a transição deSI paraS1 via eventoE1. As ações podem ser anexadas aos próprios estados:
@Bean
public Action executeAction() {
return ctx -> System.out.println("Do" + ctx.getTarget().getId());
}
states
.withStates()
.state("S3", executeAction(), errorAction());
Essa função de definição de estado aceita que uma operação seja executada quando a máquina estiver no estado de destino e, opcionalmente, um manipulador de ação de erro.
Um manipulador de ação de erro não é muito diferente de qualquer outra ação, mas será invocado se uma exceção for lançada a qualquer momento durante a avaliação das ações do estado:
@Bean
public Action errorAction() {
return ctx -> System.out.println(
"Error " + ctx.getSource().getId() + ctx.getException());
}
Também é possível registrar ações individuais para as transições de estado deentry,doeexit:
@Bean
public Action entryAction() {
return ctx -> System.out.println(
"Entry " + ctx.getTarget().getId());
}
@Bean
public Action executeAction() {
return ctx ->
System.out.println("Do " + ctx.getTarget().getId());
}
@Bean
public Action exitAction() {
return ctx -> System.out.println(
"Exit " + ctx.getSource().getId() + " -> " + ctx.getTarget().getId());
}
states
.withStates()
.stateEntry("S3", entryAction())
.stateDo("S3", executeAction())
.stateExit("S3", exitAction());
As ações respectivas serão executadas nas transições de estado correspondentes. Por exemplo, convém verificar algumas pré-condições no momento da entrada ou acionar alguns relatórios no momento da saída.
5. Ouvintes Globais
Ouvintes de eventos globais podem ser definidos para a máquina de estado. Esses ouvintes serão chamados sempre que ocorrer uma transição de estado e podem ser utilizados para coisas como log ou segurança.
Primeiro, precisamos adicionar outro método de configuração - um que não lide com estados ou transições, mas com a configuração da própria máquina de estados.
Precisamos definir um ouvinte estendendoStateMachineListenerAdapter:
public class StateMachineListener extends StateMachineListenerAdapter {
@Override
public void stateChanged(State from, State to) {
System.out.printf("Transitioned from %s to %s%n", from == null ?
"none" : from.getId(), to.getId());
}
}
Aqui, apenas substituímosstateChanged, embora muitos outros ganchos pares estejam disponíveis.
6. Estado Estendido
Spring State Machine mantém controle de seu estado, mas para manter controle de nosso estado deapplication, sejam alguns valores calculados, entradas de administradores ou respostas de chamadas de sistemas externos, precisamos usar o que é chamado deextended state.
Suponha que queremos garantir que um aplicativo de conta passe por dois níveis de aprovação. Podemos acompanhar a contagem de aprovações usando um número inteiro armazenado no estado estendido:
@Bean
public Action executeAction() {
return ctx -> {
int approvals = (int) ctx.getExtendedState().getVariables()
.getOrDefault("approvalCount", 0);
approvals++;
ctx.getExtendedState().getVariables()
.put("approvalCount", approvals);
};
}
7. Guardas
Um protetor pode ser usado para validar alguns dados antes que uma transição para um estado seja executada. Um guarda é muito parecido com uma ação:
@Bean
public Guard simpleGuard() {
return ctx -> (int) ctx.getExtendedState()
.getVariables()
.getOrDefault("approvalCount", 0) > 0;
}
A diferença perceptível aqui é que um guarda retorna umtrue oufalse que informará a máquina de estado se a transição deve ocorrer.
Também existe suporte para expressões SPeL como guardas. O exemplo acima também poderia ter sido escrito como:
.guardExpression("extendedState.variables.approvalCount > 0")
8. Máquina de estado de um construtor
StateMachineBuilder pode ser usado para criar uma máquina de estado sem usar anotações Spring ou criar um contexto Spring:
StateMachineBuilder.Builder builder
= StateMachineBuilder.builder();
builder.configureStates().withStates()
.initial("SI")
.state("S1")
.end("SF");
builder.configureTransitions()
.withExternal()
.source("SI").target("S1").event("E1")
.and().withExternal()
.source("S1").target("SF").event("E2");
StateMachine machine = builder.build();
9. Estados Hierárquicos
Os estados hierárquicos podem ser configurados usando várioswithStates() em conjunto comparent():
states
.withStates()
.initial("SI")
.state("SI")
.end("SF")
.and()
.withStates()
.parent("SI")
.initial("SUB1")
.state("SUB2")
.end("SUBEND");
Esse tipo de configuração permite que a máquina de estado tenha vários estados, portanto, uma chamada paragetState() produzirá vários IDs. Por exemplo, imediatamente após a inicialização, a seguinte expressão resulta em:
stateMachine.getState().getIds()
["SI", "SUB1"]
10. Junções (escolhas)
Até agora, criamos transições de estado que eram lineares por natureza. Isso não apenas é bastante desinteressante, mas também não reflete casos de uso da vida real que um desenvolvedor será solicitado a implementar. As chances são de que caminhos condicionais precisarão ser implementados, e as junções (ou escolhas) da máquina de estado do Spring nos permitem fazer exatamente isso.
Primeiro, precisamos marcar um estado como uma junção (escolha) na definição de estado:
states
.withStates()
.junction("SJ")
Em seguida, nas transições, definimos as opções first / then / last que correspondem a uma estrutura if-then-else:
.withJunction()
.source("SJ")
.first("high", highGuard())
.then("medium", mediumGuard())
.last("low")
first ethen pegam um segundo argumento que é uma proteção regular que será invocada para descobrir qual caminho seguir:
@Bean
public Guard mediumGuard() {
return ctx -> false;
}
@Bean
public Guard highGuard() {
return ctx -> false;
}
Observe que uma transição não para em um nó de junção, mas executa imediatamente proteções definidas e vai para uma das rotas designadas.
No exemplo acima, instruir a máquina de estado a fazer a transição para SJ fará com que o estado real se tornelow, pois os dois guardas retornam apenas falso.
Uma nota final é quethe API provides both junctions and choices. However, functionally they are identical in every aspect.
11. Fork
Às vezes, torna-se necessário dividir a execução em vários caminhos de execução independentes. Isso pode ser obtido usando a funcionalidadefork.
Primeiro, precisamos designar um nó como um nó de bifurcação e criar regiões hierárquicas nas quais a máquina de estados executará a divisão:
states
.withStates()
.initial("SI")
.fork("SFork")
.and()
.withStates()
.parent("SFork")
.initial("Sub1-1")
.end("Sub1-2")
.and()
.withStates()
.parent("SFork")
.initial("Sub2-1")
.end("Sub2-2");
Em seguida, defina a transição da bifurcação:
.withFork()
.source("SFork")
.target("Sub1-1")
.target("Sub2-1");
12. Join
O complemento da operação de garfo é a junção. Ele nos permite definir uma transição de estado para a qual depende a conclusão de alguns outros estados:
Assim como na bifurcação, precisamos designar um nó de junção na definição de estado:
states
.withStates()
.join("SJoin")
Em seguida, nas transições, definimos quais estados precisam ser concluídos para ativar nosso estado de junção:
transitions
.withJoin()
.source("Sub1-2")
.source("Sub2-2")
.target("SJoin");
É isso aí! Com esta configuração, quandoSub1-2 eSub2-2 são alcançados, a máquina de estado fará a transição paraSJoin
13. Enums Em vez deStrings
Nos exemplos acima, usamos constantes de string para definir estados e eventos para maior clareza e simplicidade. Em um sistema de produção do mundo real, provavelmente se desejaria usar enums do Java para evitar erros de ortografia e obter mais segurança de digitação.
Primeiro, precisamos definir todos os estados e eventos possíveis em nosso sistema:
public enum ApplicationReviewStates {
PEER_REVIEW, PRINCIPAL_REVIEW, APPROVED, REJECTED
}
public enum ApplicationReviewEvents {
APPROVE, REJECT
}
Também precisamos passar nossas enumerações como parâmetros genéricos quando estendemos a configuração:
public class SimpleEnumStateMachineConfiguration
extends StateMachineConfigurerAdapter
Uma vez definidos, podemos usar nossas constantes enum em vez de cadeias. Por exemplo, para definir uma transição:
transitions.withExternal()
.source(ApplicationReviewStates.PEER_REVIEW)
.target(ApplicationReviewStates.PRINCIPAL_REVIEW)
.event(ApplicationReviewEvents.APPROVE)
14. Conclusão
Este artigo explorou alguns dos recursos da máquina de estado Spring.
Como sempre, você pode encontrar o código-fonte de amostraover on GitHub.