Fermer

décembre 12, 2022

Machine d’état de printemps | AU NOUVEAU Blog

Machine d’état de printemps |  AU NOUVEAU Blog


Présentation de la machine d’état

Spring State Machine est un module Spring qui nous permet de décrire des transitions bien connues et bien comprises d’un état à un autre. Vous avez probablement construit un million de machines à états dans votre vie si vous avez déjà fait un quelconque type de programmation. Par exemple, tout type d’instruction if-then-else à un moment donné constituera une machine à états. Ainsi, toute transition prévisible ou déterministe d’un état à un autre qui peut avoir des actions associées est une machine à états et est une partie très fondamentale de ce que nous faisons.

Mais les choses peuvent mal tourner avec ces instructions if-else, si les transitions d’état deviennent juste un peu complexes où un développeur se perdra dans la structure imbriquée des instructions if-else au lieu de résoudre un problème métier. Et pour aggraver la vie, si le flux de travail a des changements, le développeur doit parcourir différentes couches de la base de code où résident toutes les décisions complexes.

Les machines à états sont un bon moyen de réfléchir aux progressions de nos applications, à la façon dont les choses devraient se passer et à la façon dont elles devraient se déplacer. Modéliser votre projet d’entreprise en termes de machine d’état est utile même si vous n’en avez pas l’intention. Les machines d’état décrivent l’état d’un processus de longue durée, donc si vous avez un processus de longue durée avec beaucoup de pièces mobiles, la machine d’état entre en scène.

La machine d’état a l’avantage d’être interrogeable. Vous pouvez interroger où nous en sommes dans le flux d’exécution. Par exemple, je remplis une commande et nous avons un certain nombre d’étapes impliquées par des tonnes d’acteurs. Pouvoir fouiller la machine d’état pour vérifier où nous en sommes et où nous allons procéder après que l’acteur a effectué une action déterministe.

Nous allons construire la machine d’état à l’aide du projet Spring State Machine.

Étape 1

Nous allons construire un projet gradle avec le build.gradle suivant.

 plugins {
 id 'java'
 id 'org.springframework.boot' version '3.0.0'
 id 'io.spring.dependency-management' version '1.1.0'
 }

 group = 'com.ttn'
 version = '0.0.1-SNAPSHOT'
 sourceCompatibility = '17'

 configurations {
 compileOnly {
 extendsFrom annotationProcessor
 }
 }

 repositories {
   mavenCentral()
 }

 dependencies {
   implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
   implementation 'org.springframework.boot:spring-boot-starter-web'
   compileOnly 'org.projectlombok:lombok'
   runtimeOnly 'com.mysql:mysql-connector-j'
   annotationProcessor 'org.projectlombok:lombok'
   testImplementation 'org.springframework.boot:spring-boot-starter-test'
   implementation group: 'org.springframework.statemachine', name: 'spring-statemachine-core', version: '3.2.0'
   }

Étape 2

Définissons les états de l’Ordre

public enum OrderStates {
   SUBMITTED, PAID, FULFILLED, CANCELED
}

Étape 3

Créons des événements

public enum OrderEvents {
   PAY, FULFILL, CANCEL;
}

Notre état change comme ci-dessous :

1. Lorsque la commande est créée, elle sera à l’état SOUMIS.

2. À partir de là, si l’événement PAY se produit, l’état passera à PAYÉ

3. À partir de PAYÉ, si l’événement FULFILL se produit, l’état passera à FULFILLED

4. L’état COMPLET est l’état final

5. Mais à partir de FULFILED, si l’événement CANCEL se produit, l’état passera à CANCELED

6. De plus, à partir de PAYÉ, si l’événement ANNULER se produit, l’état passera à ANNULÉ

7. L’état ANNULÉ est également l’état final

Nous devons couvrir la transition ci-dessus. Nous créons d’abord une machine d’état avec des états d’ordre.

Étape 4:

Configuration de la machine d’état Spring

 @Slf4j
 @Configuration
 @EnableStateMachineFactory
 class SimpleStateMachineConfiguration extends StateMachineConfigurerAdapter<OrderStates,OrderEvents> {..

Nous devons le faire passer outre les méthodes ci-dessus StateMachineConfigurerAdapterStateMachineConfigurerAdapterStateMachineConfigurerAdapter pour configurer notre machine d’état

La méthode de remplacement ci-dessous nous aide à définir les états de la machine

 @Override
 public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
    states.withStates()
            .initial(OrderStates.SUBMITTED)
            .state(OrderStates.PAID)
            .end(OrderStates.FULFILLED)
            .end(OrderStates.CANCELLED);
 }

La méthode de remplacement ci-dessous nous aide à définir les transitions dont nous avons discuté ci-dessus

 @Override
 public void configure(StateMachineTransitionConfigurer<OrderStates,
       OrderEvents> transitions) throws Exception {
   transitions
           .withExternal()
           .source(OrderStates.SUBMITTED)
           .target(OrderStates.PAID)
           .event(OrderEvents.PAY)
           .guard(ctx -> {
               log.info(“true->statechanged. false->do not change ”);
               var paymentType = String.class.cast(ctx.getExtendedState()
                       .getVariables().get("paymentType"));
               if (!StringUtils.isEmpty(paymentType) && paymentType.equals("cod"))
                   return false;
               else return true;
           })

           .and()
           .withExternal()
           .source(OrderStates.PAID)
           .target(OrderStates.FULFILLED)
           .event(OrderEvents.FULFILL)
           .action(ctx -> {
             log.info("This PAID handler where we can perform some logging");
           })

           .and()
           .withExternal()
           .source(OrderStates.SUBMITTED)
           .target(OrderStates.CANCELLED)
           .event(OrderEvents.CANCEL)
           .action(ctx -> {
             log.info("This SUBMITTED handler where we can perform some logging");
           })


           .and()
           .withExternal()
           .source(OrderStates.PAID)
           .target(OrderStates.CANCELLED)
           .event(OrderEvents.CANCEL)
           .action(ctx -> {
               log.info("This PAID handler where we can perform some logging");
           });

 }

Ici, nous définissons d’abord l’état source, à partir de la source, nous définissons l’état de destination lorsqu’un événement particulier est déclenché. Outre la source, la cible et l’événement, nous avons également défini Gardes et Actions.

Ici, nous pouvons voir que pour l’état REMPLI, ANNULÉ, PAYÉ nous avons utilisé un Action qui peut être utilisé pour des problèmes transversaux comme l’exploitation forestière.

Aussi, nous avons utilisé Gardien, qui peut être utilisé pour décider si nous voulons empêcher la modification de l’état. Si vrai, l’état changera, sinon ce ne sera pas le cas.

Gardien

Nous pouvons définir une garde comme un bean séparé, puis l’appeler directement depuis la garde.

Guard est une interface fonctionnelle et nous avons donc utilisé lambda pour sa mise en œuvre. La méthode d’implémentation doit renvoyer true ou false. True signifie que l’état va changer et false signifie que l’état ne changera pas.

 @Bean
 public Guard<OrderStates, OrderEvents> guard() {
   return ctx -> return true;
 }

Action

Nous pouvons définir l’action comme un bean séparé, puis l’appeler directement à partir de l’action. L’action est une interface fonctionnelle et nous avons donc utilisé lambda pour son implémentation. C’est juste prendre un consommateur.

 @Bean
 public Action<OrderStates, OrderEvents> guard() {
   return ctx -> log.inf("logging");
 }

Étape 5

Créons un simple CommandeFacture Entité dont nous allons changer l’état à l’aide de notre machine d’état.

 @Entity
 @Getter
 @Setter
 public class OrderInvoice {

   @Id
   @GeneratedValue
   private  Long id;
   private LocalDate localDate;
   private String state;

   @Transient
   String event;

   @Transient
   String paymentType;

 }
 public interface OrderRepository extends JpaRepository<OrderInvoice,Long> {}

Étape 6

Nous allons créer une classe de contrôleur simple pour créer d’abord une OrderInvoice pour laquelle l’état sera dans SOUMIS Etat

 @PostMapping("/createOrder")
 public OrderInvoice createOrder(){
   OrderInvoice order = new OrderInvoice();
   order.setState(OrderStates.SUBMITTED.name());
   order.setLocalDate(LocalDate.now());
   return orderRepository.save(order);
 }

Étape 7

Nous devons maintenant créer un autre gestionnaire qui utilisera la machine d’état pour changer l’état de cette commande

 @RestController
 @RequiredArgsConstructor
 public class WorkflowController {


   private final OrderRepository orderRepository;

   private final StateMachineFactory<OrderStates, OrderEvents> stateMachineFactory;



   @PutMapping("/change")
   public String changeState(@RequestBody OrderInvoice order){

        
         StateMachine<OrderStates, OrderEvents> sm =    build(order);
         sm.getExtendedState().getVariables().put("paymentType",order.getPaymentType());
         sm.sendEvent(
                 MessageBuilder.withPayload(OrderEvents.valueOf(order.getEvent()))
                         .setHeader("orderId",order.getId())
                         .setHeader("state",order.getState())
                         .build()
                 );
        return "state changed";
    }
 }

Ici, nous pouvons voir la méthode build(order) qui est très importante. Cette méthode vise à obtenir la machine d’état de l’usine de machines d’état et à la définir dans un état dans lequel se trouve notre commande actuelle. Habituellement, l’état actuel est extrait de la base de données.

Générateur de messages de printemps est très utile pour envoyer des événements car il nous permet de joindre des en-têtes que nous avons utilisés pour envoyer des événements. Dans les étapes suivantes, nous définirons un intercepteur où nous utiliserons les entrées d’en-tête.

Étape 8

(Étape la plus importante)

Extraction de l’état de la base de données et réinitialisation de la machine à cet état. C’est la partie la plus importante et peut sembler lourde à première vue.

 public StateMachine<OrderStates,OrderEvents> build(final OrderInvoice orderDto){
   var orderDb =  this.orderRepository.findById(orderDto.getId());
   var stateMachine =  this.stateMachineFactory.getStateMachine(orderDto.getId().toString());
   stateMachine.stop();
   stateMachine.getStateMachineAccessor()
      .doWithAllRegions(sma -> {
        sma.resetStateMachine(new DefaultStateMachineContext<>(OrderStates.valueOf(orderDb.get().getState()), null, null, null));
     });
    stateMachine.start();
    return stateMachine;
 }

Comme, nous pouvons voir que nous avons d’abord extrait la machine d’état de stateMachineFactory que nous avons déjà injectée, puis nous l’avons arrêtée, puis nous avons réinitialisé la machine à un état de OrderInvoice que nous avons extrait de la base de données.

Étape 9

Une fois que nous avons réinitialisé la machine à l’état existant à l’aide de db, il nous suffit d’envoyer l’événement que nous avons reçu dans la requête.

Mais nous devons toujours conserver l’état de la commande dans notre base de données lorsque la machine d’état a modifié l’état de la commande. Nous pouvons prendre l’aide de certains intercepteurs comme indiqué ci-dessous

 public StateMachine<OrderStates,OrderEvents> build(final OrderInvoice orderDto){
   var orderDb =  this.orderRepository.findById(orderDto.getId());
   var stateMachine =  this.stateMachineFactory.getStateMachine(orderDto.getId().toString());
   stateMachine.stop();
   stateMachine.getStateMachineAccessor()
           .doWithAllRegions(sma -> {
               sma.addStateMachineInterceptor(new StateMachineInterceptorAdapter<>() {
                   @Override
                   public void preStateChange(State<OrderStates, OrderEvents> state, Message<OrderEvents> message, Transition<OrderStates, OrderEvents> transition, StateMachine<OrderStates, OrderEvents> stateMachine, StateMachine<OrderStates, OrderEvents> rootStateMachine) {
                      var orderId = Long.class.cast(message.getHeaders().get("orderId"));
                      var order =  orderRepository.findById(orderId);
                       if(order.isPresent()){
                           order.get().setState(state.getId().name());
                           orderRepository.save(order.get());
                       }
                   }
               });
               sma.resetStateMachine(new DefaultStateMachineContext<>(OrderStates.valueOf(orderDb.get().getState()), null, null, null));
           });

    stateMachine.start();
    return stateMachine;

 }

Ici, nous avons ajouté le addStateMAchineInterceptorAdaptoraddStateMAchineInterceptorAdaptor et outrepasser preStateChange méthode.

Il est temps de tester notre application via les commandes curl

Testez l’étape 1 :

D’abord, nous créons simplement la commande. Cela conservera la commande dans l’état SOUMIS

curl --location --request POST 'localhost:8080/createOrder' \
--data-raw ''

Testez l’étape 2 :

Nous envoyons simplement un événement de PAY pour l’ID de commande 1

 curl 


     "id": 1,
     "event": "PAY",
     "paymentType" : "cash"
 }'

La boucle ci-dessus changera l’état en PAYÉ car le type de paiement est en espèces

Mais si la boucle est comme ci-dessous

 curl --location --request PUT 'localhost:8080/change' \
 --header 'Content-Type: application/json' \
 --data-raw '{
     "id": 1,
     "event": "PAY",
     "paymentType" : "cod"
 }'

Puisque nous avons une garde qui ne permettra pas de changer l’état si le type de paiement de l’action est « la morue”.

Ce blog est initialement publié ici.




Source link

décembre 12, 2022