Een gids voor het Spring State Machine-project

1. Inleiding

Dit artikel is gericht op het State Machine-project van Spring - dat kan worden gebruikt om workflows of andere soorten problemen met de weergave van eindige toestanden weer te geven.

2. Maven Afhankelijkheid

Om te beginnen, moeten we de belangrijkste Maven-afhankelijkheid toevoegen:

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

De laatste versie van deze afhankelijkheid is hier te vinden.

3. Status van machineconfiguratie

Laten we nu beginnen met het definiëren van een eenvoudige toestandsmachine:

@Configuration @EnableStateMachine openbare klasse SimpleStateMachineConfiguration breidt StateMachineConfigurerAdapter uit {@Override public void configure (StateMachineStateConfigurer states) genereert Uitzondering {states .withStates () .initial ("SI") .end ("SF") .states (nieuwe Hash.Set (Array ("S1", "S2", "S3"))); } @Override public void configure (StateMachineTransitionConfigurer transitions) genereert Uitzondering {transitions.withExternal () .source ("SI"). Target ("S1"). Event ("E1"). En () .withExternal () .source ( "S1"). Target ("S2"). Event ("E2"). En () .withExternal () .source ("S2"). Target ("SF"). Event ("end"); }}

Merk op dat deze klasse zowel wordt geannoteerd als een conventionele Spring-configuratie als een statusmachine. Het moet ook worden uitgebreid StateMachineConfigurerAdapter zodat verschillende initialisatiemethoden kunnen worden aangeroepen. In een van de configuratiemethoden definiëren we alle mogelijke toestanden van de toestandsmachine, in de andere hoe gebeurtenissen de huidige toestand veranderen.

De bovenstaande configuratie beschrijft een vrij eenvoudige, lineaire overgangstoestandmachine die gemakkelijk genoeg te volgen zou moeten zijn.

Nu moeten we een Spring-context starten en een verwijzing krijgen naar de statusmachine die is gedefinieerd door onze configuratie:

@Autowired privé StateMachine stateMachine;

Zodra we de statusmachine hebben, moet deze worden gestart:

stateMachine.start ();

Nu onze machine zich in de oorspronkelijke staat bevindt, kunnen we gebeurtenissen verzenden en zo overgangen activeren:

stateMachine.sendEvent ("E1");

We kunnen altijd de huidige staat van de toestandsmachine controleren:

stateMachine.getState ();

4. Acties

Laten we enkele acties toevoegen die moeten worden uitgevoerd rond toestandsovergangen. Ten eerste definiëren we onze actie als een springboon in hetzelfde configuratiebestand:

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

Dan kunnen we de hierboven gemaakte actie op de overgang registreren in onze configuratieklasse:

@Override public void configure (StateMachineTransitionConfigurer transitions) genereert uitzondering {transitions.withExternal () transitions.withExternal () .source ("SI"). Target ("S1") .event ("E1"). Action (initAction ())

Deze actie wordt uitgevoerd wanneer de overgang van SI naar S1 via evenement E1 treedt op. Acties kunnen aan de staten zelf worden gekoppeld:

@Bean public Action executeAction () {return ctx -> System.out.println ("Do" + ctx.getTarget (). GetId ()); } staten .withStates () .state ("S3", executeAction (), errorAction ());

Deze toestandsdefinitiefunctie accepteert een bewerking die moet worden uitgevoerd wanneer de machine zich in de doeltoestand bevindt en optioneel een foutactie-afhandelaar.

Een foutactie-afhandelaar verschilt niet veel van elke andere actie, maar deze wordt aangeroepen als er een uitzondering wordt gegenereerd tijdens de evaluatie van de acties van de status:

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

Het is ook mogelijk om individuele acties voor aan te melden binnenkomst, Doen en Uitgang toestandsovergangen:

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

Respectieve acties zullen worden uitgevoerd op de corresponderende toestandsovergangen. We willen bijvoorbeeld enkele randvoorwaarden verifiëren op het moment van binnenkomst of wat rapportage activeren op het moment van vertrek.

5. Wereldwijde luisteraars

Globale gebeurtenislisteners kunnen worden gedefinieerd voor de statusmachine. Deze luisteraars worden opgeroepen telkens wanneer er een statusovergang plaatsvindt en kunnen worden gebruikt voor zaken als logboekregistratie of beveiliging.

Eerst moeten we een andere configuratiemethode toevoegen - een die geen betrekking heeft op toestanden of overgangen, maar op de configuratie voor de toestandsmachine zelf.

We moeten een luisteraar definiëren door uit te breiden StateMachineListenerAdapter:

public class StateMachineListener breidt StateMachineListenerAdapter uit {@Override public void stateChanged (State from, State to) {System.out.printf ("Transitioned from% s to% s% n", from == null? "none": from.getId ( ), to.getId ()); }}

Hier hebben we alleen maar overwonnen stateChanged hoewel er veel andere zelfs haken beschikbaar zijn.

6. Uitgebreide staat

Spring State Machine houdt zijn staat bij, maar om onze bij te houden toepassing staat, of het nu gaat om berekende waarden, invoer van beheerders of reacties van het aanroepen van externe systemen, we moeten een zogenaamde uitgebreide staat.

Stel dat we ervoor willen zorgen dat een accountaanvraag twee goedkeuringsniveaus doorloopt. We kunnen het aantal goedkeuringen bijhouden met behulp van een geheel getal dat is opgeslagen in de uitgebreide status:

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

7. Bewakers

Een bewaker kan worden gebruikt om bepaalde gegevens te valideren voordat een overgang naar een toestand wordt uitgevoerd. Een bewaker lijkt erg op een actie:

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

Het opvallende verschil hier is dat een bewaker een waar of false die de toestandsmachine zal informeren of de overgang moet plaatsvinden.

Ondersteuning voor SPeL-expressies als bewakers bestaat ook. Het bovenstaande voorbeeld had ook kunnen worden geschreven als:

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

8. State Machine van een bouwer

StateMachineBuilder kan worden gebruikt om een ​​toestandsmachine te maken zonder Spring-annotaties te gebruiken of een Spring-context te creëren:

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. Hiërarchische staten

Hiërarchische statussen kunnen worden geconfigureerd door meerdere te gebruiken withStates () in combinatie met ouder():

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

Met dit soort instellingen kan de toestandsmachine meerdere toestanden hebben, dus een oproep naar getState () zal meerdere ID's produceren. Direct na het opstarten resulteert de volgende uitdrukking bijvoorbeeld in:

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

10. Knooppunten (keuzes)

Tot nu toe hebben we toestandsovergangen gecreëerd die van nature lineair waren. Dit is niet alleen tamelijk oninteressant, maar het weerspiegelt ook geen real-life use-cases die een ontwikkelaar zal moeten implementeren. De kans is groot dat voorwaardelijke paden moeten worden geïmplementeerd, en de knooppunten (of keuzes) van Spring State Machine stellen ons in staat om precies dat te doen.

Eerst moeten we een staat markeren als een knooppunt (keuze) in de toestanddefinitie:

staten .withStates () .junction ("SJ")

Vervolgens definiëren we in de overgangen de eerste / dan / laatste opties die overeenkomen met een als-dan-anders-structuur:

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

eerste en dan neem een ​​tweede argument dat een gewone bewaker is die zal worden aangeroepen om erachter te komen welk pad je moet nemen:

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

Merk op dat een overgang niet stopt bij een knooppunt, maar onmiddellijk gedefinieerde bewakers zal uitvoeren en naar een van de aangewezen routes zal gaan.

In het bovenstaande voorbeeld zal het instrueren van de toestandsmachine om over te schakelen naar SJ resulteren in de feitelijke toestand die moet worden laag aangezien de beide bewakers gewoon vals terugkeren.

Een laatste opmerking is dat de API biedt zowel knooppunten als keuzes. Functioneel zijn ze echter in elk aspect identiek.

11. Vork

Soms wordt het nodig om de uitvoering op te splitsen in meerdere onafhankelijke uitvoeringspaden. Dit kan worden bereikt met behulp van de vork functionaliteit.

Eerst moeten we een knooppunt aanwijzen als een vorkknooppunt en hiërarchische gebieden maken waarin de toestandsmachine de splitsing zal uitvoeren:

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

Definieer vervolgens de vorkovergang:

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

12. Doe mee

Het complement van de vorkbediening is de join. Het stelt ons in staat om een ​​toestand in te stellen die afhankelijk is van het voltooien van een aantal andere toestanden:

Net als bij forking, moeten we een join-knooppunt aanwijzen in de statusdefinitie:

staten .withStates () .join ("SJoin")

Vervolgens definiëren we in overgangen welke staten moeten worden voltooid om onze join-status in te schakelen:

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

Dat is het! Met deze configuratie, wanneer beide Sub1-2 en Sub2-2 bereikt, zal de toestandsmachine overgaan naar Doe mee

13. Enums In plaats van Snaren

In de bovenstaande voorbeelden hebben we stringconstanten gebruikt om toestanden en gebeurtenissen te definiëren voor duidelijkheid en eenvoud. Op een echt productiesysteem zou je waarschijnlijk Java's enums willen gebruiken om spelfouten te voorkomen en meer typeveiligheid te krijgen.

Eerst moeten we alle mogelijke toestanden en gebeurtenissen in ons systeem definiëren:

openbare enum ApplicationReviewStates {PEER_REVIEW, PRINCIPAL_REVIEW, APPROVED, REJECTED} openbare enum ApplicationReviewEvents {APPROVE, REJECT}

We moeten ook onze enums doorgeven als generieke parameters wanneer we de configuratie uitbreiden:

openbare klasse SimpleEnumStateMachineConfiguration breidt StateMachineConfigurerAdapter uit 

Eenmaal gedefinieerd, kunnen we onze enum-constanten gebruiken in plaats van strings. Om bijvoorbeeld een overgang te definiëren:

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

14. Conclusie

In dit artikel zijn enkele kenmerken van de Spring State-machine onderzocht.

Zoals altijd kun je de voorbeeldbroncode vinden op GitHub.


$config[zx-auto] not found$config[zx-overlay] not found