Ein Leitfaden für das Spring State Machine-Projekt

Ein Leitfaden für das Spring State Machine-Projekt

1. Einführung

Dieser Artikel konzentriert sich aufState Machine project von Spring, die zur Darstellung von Workflows oder anderen Problemen bei der Darstellung von Automaten mit endlichen Zuständen verwendet werden können.

2. Maven-Abhängigkeit

Um zu beginnen, müssen wir die Hauptabhängigkeit von Maven hinzufügen:


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

Die neueste Version dieser Abhängigkeit befindet sich möglicherweise inhere.

3. Konfiguration der Zustandsmaschine

Beginnen wir nun mit der Definition einer einfachen Zustandsmaschine:

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

Beachten Sie, dass diese Klasse sowohl als herkömmliche Federkonfiguration als auch als Zustandsmaschine mit Anmerkungen versehen ist. Außerdem mussStateMachineConfigurerAdapter erweitert werden, damit verschiedene Initialisierungsmethoden aufgerufen werden können. In einer der Konfigurationsmethoden definieren wir alle möglichen Zustände der Zustandsmaschine, in der anderen definieren wir, wie Ereignisse den aktuellen Zustand ändern.

Die obige Konfiguration zeigt eine ziemlich einfache, geradlinige Übergangszustandsmaschine, die leicht zu befolgen sein sollte.

image

Jetzt müssen wir einen Spring-Kontext starten und einen Verweis auf die durch unsere Konfiguration definierte Zustandsmaschine erhalten:

@Autowired
private StateMachine stateMachine;

Sobald wir die Zustandsmaschine haben, muss sie gestartet werden:

stateMachine.start();

Nachdem sich unsere Maschine im Ausgangszustand befindet, können wir Ereignisse senden und damit Übergänge auslösen:

stateMachine.sendEvent("E1");

Wir können jederzeit den aktuellen Status der Zustandsmaschine überprüfen:

stateMachine.getState();

4. Aktionen

Fügen wir einige Aktionen hinzu, die um Zustandsübergänge ausgeführt werden sollen. Zunächst definieren wir unsere Aktion als Spring Bean in derselben Konfigurationsdatei:

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

Dann können wir die oben erstellte Aktion für den Übergang in unserer Konfigurationsklasse registrieren:

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

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

Diese Aktion wird ausgeführt, wenn der Übergang vonSI zuS1 über das EreignisE1 erfolgt. Aktionen können an die Staaten selbst angehängt werden:

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

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

Diese Zustandsdefinitionsfunktion akzeptiert eine Operation, die ausgeführt werden soll, wenn sich die Maschine im Zielzustand befindet, und optional einen Fehlerhandler.

Ein Fehleraktionshandler unterscheidet sich nicht wesentlich von anderen Aktionen. Er wird jedoch aufgerufen, wenn während der Auswertung der Aktionen des Status zu irgendeinem Zeitpunkt eine Ausnahme ausgelöst wird:

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

Es ist auch möglich, einzelne Aktionen für die Zustandsübergängeentry,do undexit zu registrieren:

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

Entsprechende Aktionen werden an den entsprechenden Zustandsübergängen ausgeführt. Zum Beispiel möchten wir möglicherweise einige Vorbedingungen zum Zeitpunkt des Eintritts überprüfen oder einige Berichte zum Zeitpunkt des Austritts auslösen.

5. Globale Zuhörer

Für die Zustandsmaschine können globale Ereignis-Listener definiert werden. Diese Listener werden jedes Mal aufgerufen, wenn ein Statusübergang auftritt, und können für Dinge wie Protokollierung oder Sicherheit verwendet werden.

Zuerst müssen wir eine andere Konfigurationsmethode hinzufügen - eine, die sich nicht mit Zuständen oder Übergängen befasst, sondern mit der Konfiguration für die Zustandsmaschine selbst.

Wir müssen einen Listener definieren, indem wirStateMachineListenerAdapter erweitern:

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

Hier haben wir nurstateChanged überschrieben, obwohl viele andere gerade Haken verfügbar sind.

6. Erweiterter Zustand

Spring State Machine verfolgt seinen Zustand, aber um unserenapplication-Zustand zu verfolgen, seien es einige berechnete Werte, Einträge von Administratoren oder Antworten von aufrufenden externen Systemen, müssen wir das verwenden, was alsextended statebezeichnet wird. s.

Angenommen, wir möchten sicherstellen, dass ein Kontoantrag zwei Genehmigungsstufen durchläuft. Wir können die Anzahl der Genehmigungen mithilfe einer Ganzzahl verfolgen, die im erweiterten Status gespeichert ist:

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

7. Wachen

Ein Guard kann verwendet werden, um einige Daten zu validieren, bevor ein Übergang in einen Zustand ausgeführt wird. Ein Wächter sieht einer Aktion sehr ähnlich:

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

Der bemerkenswerte Unterschied besteht darin, dass ein Wächter eintrue oderfalse zurückgibt, das die Zustandsmaschine darüber informiert, ob der Übergang zugelassen werden soll.

Unterstützung für SPeL-Ausdrücke als Wächter ist ebenfalls vorhanden. Das obige Beispiel hätte auch so geschrieben werden können:

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

8. Zustandsmaschine von einem Builder

StateMachineBuilder kann verwendet werden, um eine Zustandsmaschine zu erstellen, ohne Spring-Annotationen zu verwenden oder einen Spring-Kontext zu erstellen:

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. Hierarchische Zustände

Hierarchische Zustände können mithilfe mehrererwithStates() in Verbindung mitparent() konfiguriert werden:

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

Diese Art der Einrichtung ermöglicht es der Zustandsmaschine, mehrere Zustände zu haben, sodass ein Aufruf vongetState() mehrere IDs erzeugt. Beispielsweise ergibt sich unmittelbar nach dem Start der folgende Ausdruck:

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

10. Kreuzungen (Auswahlmöglichkeiten)

Bisher haben wir Zustandsübergänge erstellt, die von Natur aus linear waren. Dies ist nicht nur ziemlich uninteressant, sondern spiegelt auch nicht die realen Anwendungsfälle wider, zu deren Implementierung ein Entwickler aufgefordert wird. Die Chancen stehen gut, dass bedingte Pfade implementiert werden müssen, und die Junctions (oder Auswahlmöglichkeiten) der Spring State Machine ermöglichen es uns, genau das zu tun.

Zuerst müssen wir einen Zustand als Knotenpunkt (Auswahl) in der Zustandsdefinition markieren:

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

Dann definieren wir in den Übergängen erste / dann / letzte Optionen, die einer Wenn-Dann-Sonst-Struktur entsprechen:

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

first undthen verwenden ein zweites Argument, bei dem es sich um eine reguläre Wache handelt, die aufgerufen wird, um herauszufinden, welchen Weg sie einschlagen müssen:

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

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

Beachten Sie, dass ein Übergang nicht an einem Knotenpunkt stoppt, sondern sofort definierte Wachen ausführt und zu einer der festgelegten Routen führt.

Im obigen Beispiel führt die Anweisung der Zustandsmaschine zum Übergang zu SJ dazu, dass der tatsächliche Zustandlow wird, da beide Schutzvorrichtungen nur false zurückgeben.

Eine letzte Anmerkung ist, dassthe API provides both junctions and choices. However, functionally they are identical in every aspect.

11. Fork

Manchmal ist es notwendig, die Ausführung in mehrere unabhängige Ausführungspfade aufzuteilen. Dies kann mit der Funktionalität vonforkerreicht werden.

Zunächst müssen wir einen Knoten als Gabelknoten festlegen und hierarchische Bereiche erstellen, in die die Zustandsmaschine die Aufteilung ausführt:

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

Dann definieren Sie den Gabelübergang:

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

12. Join

Das Komplement der Fork-Operation ist das Join. Damit können wir einen Statusübergang festlegen, der vom Abschluss einiger anderer Status abhängt:

image

Wie beim Forking müssen wir in der Statusdefinition einen Join-Knoten festlegen:

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

Anschließend definieren wir in Übergängen, welche Status abgeschlossen werden müssen, um unseren Join-Status zu aktivieren:

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

Das ist es! Wenn bei dieser Konfiguration sowohlSub1-2 als auchSub2-2 erreicht sind, wechselt die Zustandsmaschine zuSJoin

13. Enums Anstelle vonStrings

In den obigen Beispielen haben wir Zeichenfolgenkonstanten verwendet, um Zustände und Ereignisse zur Klarheit und Einfachheit zu definieren. In einem realen Produktionssystem möchte man wahrscheinlich die Java-Aufzählungen verwenden, um Rechtschreibfehler zu vermeiden und mehr Typensicherheit zu erzielen.

Zuerst müssen wir alle möglichen Zustände und Ereignisse in unserem System definieren:

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

public enum ApplicationReviewEvents {
    APPROVE, REJECT
}

Wir müssen unsere Aufzählungen auch als generische Parameter übergeben, wenn wir die Konfiguration erweitern:

public class SimpleEnumStateMachineConfiguration
  extends StateMachineConfigurerAdapter
  

Einmal definiert, können wir unsere Enum-Konstanten anstelle von Strings verwenden. So definieren Sie beispielsweise einen Übergang:

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

14. Fazit

Dieser Artikel befasste sich mit einigen Funktionen der Spring-Zustandsmaschine.

Wie immer finden Sie den Beispielquellcodeover on GitHub.