Guide du projet Spring State Machine

Guide du projet Spring State Machine

1. introduction

Cet article se concentre sur lesState Machine project de Spring - qui peuvent être utilisés pour représenter des flux de travail ou tout autre type de problèmes de représentation d'automates à états finis.

2. Dépendance Maven

Pour commencer, nous devons ajouter la dépendance principale Maven:


    org.springframework.statemachine
    spring-statemachine-core
    1.2.3.RELEASE

La dernière version de cette dépendance peut être trouvéehere.

3. Configuration de la machine d'état

Maintenant, commençons par définir une machine à états simple:

@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");
    }
}

Notez que cette classe est annotée comme configuration Spring classique et comme machine à états. Il doit également étendreStateMachineConfigurerAdapter afin que diverses méthodes d'initialisation puissent être appelées. Dans l’une des méthodes de configuration, nous définissons tous les états possibles de la machine à états, dans l’autre, comment les événements changent l’état actuel.

La configuration ci-dessus définit une machine à états de transition linéaire plutôt simple qui devrait être assez facile à suivre.

image

Nous devons maintenant démarrer un contexte Spring et obtenir une référence à la machine à états définie par notre configuration:

@Autowired
private StateMachine stateMachine;

Une fois que nous avons la machine à états, elle doit être démarrée:

stateMachine.start();

Maintenant que notre machine est dans l'état initial, nous pouvons envoyer des événements et ainsi déclencher des transitions:

stateMachine.sendEvent("E1");

Nous pouvons toujours vérifier l'état actuel de la machine à états:

stateMachine.getState();

4. actes

Ajoutons quelques actions à exécuter autour des transitions d’état. Tout d'abord, nous définissons notre action comme un bean Spring dans le même fichier de configuration:

@Bean
public Action initAction() {
    return ctx -> System.out.println(ctx.getTarget().getId());
}

Ensuite, nous pouvons enregistrer l'action créée ci-dessus sur la transition dans notre classe de configuration:

@Override
public void configure(
  StateMachineTransitionConfigurer transitions)
  throws Exception {

    transitions.withExternal()
      transitions.withExternal()
      .source("SI").target("S1")
      .event("E1").action(initAction())

Cette action sera exécutée lorsque la transition deSI àS1 via l'événementE1 se produit. Des actions peuvent être attachées aux états eux-mêmes:

@Bean
public Action executeAction() {
    return ctx -> System.out.println("Do" + ctx.getTarget().getId());
}

states
  .withStates()
  .state("S3", executeAction(), errorAction());

Cette fonction de définition d'état accepte une opération à exécuter lorsque la machine est à l'état cible et, éventuellement, un gestionnaire d'actions d'erreur.

Un gestionnaire d'action d'erreur n'est pas très différent de toute autre action, mais il sera appelé si une exception est levée à tout moment pendant l'évaluation des actions de l'état:

@Bean
public Action errorAction() {
    return ctx -> System.out.println(
      "Error " + ctx.getSource().getId() + ctx.getException());
}

Il est également possible d'enregistrer des actions individuelles pour les transitions d'étatentry,do etexit:

@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());

Les actions respectives seront exécutées sur les transitions d'état correspondantes. Par exemple, nous pourrions vouloir vérifier certaines conditions préalables au moment de l'entrée ou déclencher des rapports au moment de la sortie.

5. Auditeurs mondiaux

Des écouteurs d'événements globaux peuvent être définis pour la machine à états. Ces écouteurs seront appelés chaque fois qu'une transition d'état se produit et peuvent être utilisés pour des tâches telles que la journalisation ou la sécurité.

Premièrement, nous devons ajouter une autre méthode de configuration - une méthode qui ne traite pas des états ou des transitions mais de la configuration pour la machine à états elle-même.

Nous devons définir un auditeur en étendantStateMachineListenerAdapter:

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());
    }
}

Ici, nous ne dépassons questateChanged bien que de nombreux autres hooks pairs soient disponibles.

6. État étendu

Spring State Machine garde une trace de son état, mais pour garder une trace de notre étatapplication, qu'il s'agisse de valeurs calculées, d'entrées d'administrateurs ou de réponses d'appels de systèmes externes, nous devons utiliser ce qu'on appelle unextended state.

Supposons que nous voulions nous assurer qu'une demande de compte passe par deux niveaux d'approbation. Nous pouvons suivre le nombre d'approbations à l'aide d'un entier stocké dans l'état étendu:

@Bean
public Action executeAction() {
    return ctx -> {
        int approvals = (int) ctx.getExtendedState().getVariables()
          .getOrDefault("approvalCount", 0);
        approvals++;
        ctx.getExtendedState().getVariables()
          .put("approvalCount", approvals);
    };
}

7. Gardes

Un garde peut être utilisé pour valider certaines données avant l'exécution d'une transition vers un état. Un garde ressemble beaucoup à une action:

@Bean
public Guard simpleGuard() {
    return ctx -> (int) ctx.getExtendedState()
      .getVariables()
      .getOrDefault("approvalCount", 0) > 0;
}

La différence notable ici est qu'une garde renvoie untrue oufalse qui informera la machine à états si la transition doit être autorisée à se produire.

La prise en charge des expressions SPeL en tant que gardes existe également. L'exemple ci-dessus aurait également pu être écrit comme suit:

.guardExpression("extendedState.variables.approvalCount > 0")

8. State Machine à partir d'un constructeur

StateMachineBuilder peut être utilisé pour créer une machine à états sans utiliser d'annotations Spring ou créer un contexte 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. États hiérarchiques

Les états hiérarchiques peuvent être configurés en utilisant plusieurswithStates() en conjonction avecparent():

states
  .withStates()
    .initial("SI")
    .state("SI")
    .end("SF")
    .and()
  .withStates()
    .parent("SI")
    .initial("SUB1")
    .state("SUB2")
    .end("SUBEND");

Ce type de configuration permet à la machine à états d'avoir plusieurs états, donc un appel àgetState() produira plusieurs ID. Par exemple, immédiatement après le démarrage, l'expression suivante a pour résultat:

stateMachine.getState().getIds()
["SI", "SUB1"]

10. Jonctions (choix)

Jusqu'à présent, nous avons créé des transitions d'états linéaires par nature. Non seulement c'est plutôt inintéressant, mais cela ne reflète pas non plus les cas d'utilisation réels qu'un développeur sera invité à implémenter. Les chances sont que des chemins conditionnels devront être implémentés, et les jonctions (ou choix) de la machine à états Spring nous permettent de faire exactement cela.

Premièrement, nous devons marquer un état comme une jonction (choix) dans la définition de l’état:

states
  .withStates()
  .junction("SJ")

Ensuite, dans les transitions, nous définissons les options first / then / last qui correspondent à une structure if-then-else:

.withJunction()
  .source("SJ")
  .first("high", highGuard())
  .then("medium", mediumGuard())
  .last("low")

first etthen prennent un deuxième argument qui est une garde régulière qui sera invoquée pour savoir quel chemin prendre:

@Bean
public Guard mediumGuard() {
    return ctx -> false;
}

@Bean
public Guard highGuard() {
    return ctx -> false;
}

Notez qu'une transition ne s'arrête pas à un nœud de jonction, mais exécute immédiatement les gardes définis et passe à l'un des itinéraires désignés.

Dans l'exemple ci-dessus, demander à la machine à états de passer à SJ fera en sorte que l'état réel deviennelow car les deux gardes renvoient simplement false.

Une dernière remarque est quethe API provides both junctions and choices. However, functionally they are identical in every aspect.

11. Fork

Parfois, il devient nécessaire de scinder l'exécution en plusieurs chemins d'exécution indépendants. Ceci peut être réalisé en utilisant la fonctionnalitéfork.

Tout d'abord, nous devons désigner un nœud en tant que nœud fork et créer des régions hiérarchiques dans lesquelles la machine à états effectuera la scission:

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");

Puis définissez la transition de fourche:

.withFork()
  .source("SFork")
  .target("Sub1-1")
  .target("Sub2-1");

12. Join

Le complément de l'opération fork est la jointure. Cela nous permet de définir un état de transition qui dépend de la réalisation de certains autres états:

image

Comme pour le forking, nous devons désigner un noeud de jointure dans la définition d'état:

states
  .withStates()
  .join("SJoin")

Ensuite, dans les transitions, nous définissons quels états doivent être terminés pour activer notre état de jointure:

transitions
  .withJoin()
    .source("Sub1-2")
    .source("Sub2-2")
    .target("SJoin");

C'est ça! Avec cette configuration, lorsque les deuxSub1-2 etSub2-2 sont atteints, la machine à états passera àSJoin

13. Enums au lieu deStrings

Dans les exemples ci-dessus, nous avons utilisé des constantes de chaîne pour définir des états et des événements dans un souci de clarté et de simplicité. Sur un système de production réel, on voudrait probablement utiliser les énumérations Java pour éviter les fautes d’orthographe et gagner en sécurité de type.

Premièrement, nous devons définir tous les états et événements possibles dans notre système:

public enum ApplicationReviewStates {
    PEER_REVIEW, PRINCIPAL_REVIEW, APPROVED, REJECTED
}

public enum ApplicationReviewEvents {
    APPROVE, REJECT
}

Nous devons également passer nos enums en tant que paramètres génériques lorsque nous étendons la configuration:

public class SimpleEnumStateMachineConfiguration
  extends StateMachineConfigurerAdapter
  

Une fois définies, nous pouvons utiliser nos constantes enum au lieu de chaînes. Par exemple pour définir une transition:

transitions.withExternal()
  .source(ApplicationReviewStates.PEER_REVIEW)
  .target(ApplicationReviewStates.PRINCIPAL_REVIEW)
  .event(ApplicationReviewEvents.APPROVE)

14. Conclusion

Cet article a exploré certaines des fonctionnalités de la machine à états Spring.

Comme toujours, vous pouvez trouver l'exemple de code sourceover on GitHub.