03.01.2019 - Gerd Wütherich - gw@code-kontor.io

Inhalt

Was ist das und wofür brauche ich das?

Spring Statemachine ist ein Framework zur Implementierung von Zustandsmaschinen innerhalb von Spring-Anwendungen.

Die Verwendung von Zustandsmaschinen in eigenen Anwendungen macht immer dann Sinn, wenn sich die Anwendung (oder Teile davon) über Zustände beschreiben lässt und innerhalb der Anwendung abgrenzbare und autonome Tasks abgearbeitet werden, die dann wiederum zu neuen Zuständen führen. Klassische Beispiele für solche Anwendungen sind technische Gerätesteuerungen (Automaten). Aber auch bei der Implementierung fachlicher Prozessabläufe lassen sich Zustandmaschinen sehr gut einsetzen, etwa als leichtgewichtige Alternative zu Business Process Engines.

Eine Beispielanwendung

In diesem Tutorial schauen wir uns das Spring Statemachine Framework am Beispiel eines einfachen Dienstes an (IExampleService), der gestoppt (OUT_OF_SERVICE) oder gestartet (IN_SERVICE) sein kann. Der Source-Code der Beispielanwendung ist als GitHub-Projekt verfügbar und folgt strukturiert:

Struktur des Beispielprojektes
Struktur des Beispielprojektes

Die Schnittstelle des zu implementierenden (und sehr simplifizierten) Dienstes besitzt drei Methoden, mit denen der Zustand des Dienstes geändert werden. Wichtig für die Implementierung ist, dass die Methoden nur dann ausgeführt werden, wenn es der Zustand des Dienstes erlaubt (es macht bspw. keinen Sinn, einen Dienst zweimal zu starten):

IExampleService.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package io.codekontor.blog.springstatemachine.example.service;

public interface IExampleService {

	void start();
	
	void stop();
	
	void terminate();
}

Das Zustandsmodell des Beispieldienstes

Um den Dienst mithilfe des Spring Statemachine Frameworks implementieren zu können, müssen wir zunächst ein Zustandsmodell erstellen:

Lebenszyklus des Beispieldienstes als Zustandsmodell
Lebenszyklus des Beispieldienstes als Zustandsmodell

Der Zustand INITIAL ist der Startzustand, der ohne externen Trigger direkt in den Zustand OUT_OF_SERVICE übergeht. Von dort aus kann der Service mit dem Trigger START in den Zustand STARTING versetzt werden. Beim Betreten des Zustandes wird die Initialisierung des Dienstes angestoßen - ist diese erfolgreich, dann wechselt die Zustandsmaschine automatisch in den Zustand IN_SERVICE. In diesem Zustand kann der Dienst über den Trigger STOP gestoppt werden.

Das Starten und Stoppen des Dienstes kann beliebig oft durchgeführt werden. Wird der Service nicht mehr benötigt, dann kann er über den Trigger TERMINATE terminiert werden (Zustand TERMINATED).

Das Spring Statemachine Framework einbinden (Maven)

Bevor das Spring Statemachine Framework verwendet werden kan, muss es als Projektabhängigkeit in der pom.xml eingetragen werden. Dafür steht (falls man Spring Boot verwendet) ein passendes Starter-POM bereit, die alle benötigten Framework-Komponenten bereitstellt:

pom.xml (Auszug)
1
2
3
4
5
6
7
8
9
[...]

<dependency>
    <groupId>org.springframework.statemachine</groupId>
    <artifactId>spring-statemachine-starter</artifactId>
    <version>2.0.3.RELEASE</version>
</dependency>

[...]

Natürlich ist die Verwendung auch ohne Spring Boot möglich, dann müssen jedoch je nach Anwendungskontext die benötigten Komponenten manuell zur Projektdefinition hinzugefügt werden. Im einfachsten Fall sind dies die Komponenten org.springframework.statemachine:spring-statemachine-core sowie ggf. org.springframework.statemachine:spring-statemachine-test.


Implementierung von Zuständen und Triggern

Für die Implementierung der Statemachine müssen zunächst alle möglichen Zustände sowie die (externen) Trigger/Events spezifiziert werden. Dies kann mit Hilfe von Java-Aufzählungstypen (Enums) erfolgen:

States.java
1
2
3
4
5
package io.codekontor.blog.springstatemachine.example.service.impl;

public enum States {
    INITIAL, STARTING, IN_SERVICE, STOPPING, OUT_OF_SERVICE, TERMINATED
}

Analog werden die möglichen externen Events als Enum-Typ angegeben:

Trigger.java
1
2
3
4
5
package io.codekontor.blog.springstatemachine.example.service.impl;

public enum Trigger {
    START, STOP, TERMINATE;
}

Implementierung der Statemachine

Die Implementierung der Statemachine erfolgt anschließend über eine eigene Konfigurationsklasse, die von der Klasse EnumStateMachineConfigurerAdapter abgeleitet wird. Diese Klasse muss mit der Annotation @EnableStateMachine annotiert sein, damit die Statemachine beim Start der Spring-Boot-Anwendung erzeugt und als Spring-Komponente bereitgestellt ist:

StateMachineConfig.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package io.codekontor.blog.springstatemachine.example.service.impl;

[...]

@EnableStateMachine
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<States, Trigger> {

    [...]

	@Override
	public void configure(StateMachineConfigurationConfigurer<States, Trigger> config) 
		throws Exception {

		config
			.withConfiguration()
			.autoStartup(true);
	}

	@Override
	public void configure(StateMachineStateConfigurer<States, Trigger> states) 
		throws Exception {
		
		states
			.withStates()
			.initial(States.INITIAL)
			.end(States.TERMINATED)
			.states(EnumSet.allOf(States.class));
	}
}

Die Definition der Zustandmaschine erfolgt über die Implementierung von unterschiedlichen configure()-Methoden, die als Parameter verschiedenen XXXConfigurer-Klassen übergeben bekommen:

  • Über den Typ StateMachineConfigurationConfigurer können allgemeine Eigenschaften der Zustandsmaschine definiert werden (bspw. die Eigenschaft, dass die Zustandsmaschine nach der Erzeugung automatisch gestartet werden soll).
  • Über den Typ StateMachineStateConfigurer werden die Menge der möglichen Zustände sowie die Start- und Endzustände konfiguriert.

Spezifikation der Zustandsübergänge

Für die Spezifikation der Zustandsübergänge (Transitionen) wird eine configure()-Methode implementiert, die als Parameter ein Objekt vom Typ StateMachineTransitionConfigurer übergeben bekommt. Über dieses Objekt können die möglichen Transationen spezifiziert werden, wobei alle in der UML definierten Transitionstypen unterstützt werden.

In unserem Beispiel verwenden wir ausschließlich den Transitionsyp ‘External’, der immer zu einem Verlassen (Exit) des Quellzustandes und zu einen Betreten (Entry) des Zielzustandes führt. Bemerkenswert ist hier, dass nicht alle Transitionen über ein Event getriggert werden. Eine Reihe von Zuständen werden ohne Trigger automatisch ausgeführt, nachdem der entsprechende Startzustand betreten wurde.

StateMachineConfig.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package io.codekontor.blog.springstatemachine.example.service.impl;

[...]

@EnableStateMachine
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<States, Trigger> {

    [...]

	@Override
    public void configure(StateMachineTransitionConfigurer<States, Trigger> transitions)
            throws Exception {
		
        transitions
			.withExternal()
				.source(States.INITIAL)
				.target(States.OUT_OF_SERVICE)
                .and()
			.withExternal()
				.source(States.OUT_OF_SERVICE)
				.target(States.STARTING)
				.event(Trigger.START)
				.and()
			.withExternal()
				.source(States.STARTING)
				.target(States.IN_SERVICE)
				.and()
			.withExternal()
				.source(States.IN_SERVICE)
				.target(States.STOPPING)
				.event(Trigger.STOP)
				.and()
			.withExternal()
				.source(States.STOPPING)
				.target(States.OUT_OF_SERVICE)
				.and()
			.withExternal()
				.source(States.OUT_OF_SERVICE)
				.target(States.TERMINATED)
				.event(Trigger.TERMINATE);
    }
}

Verwendung der Statemachine

Über die implementierte Konfigurationsklasse wird unsere Statemachine während des Starts der Spring-Boot-Anwendung erzeugt und als Spring-Bean bereitgestellt. Um auf die Statemachine zuzugreifen, können wir uns die Instanz via Dependency Injection injizieren lassen.

Der Aufruf von zustandsändernden Methoden am Service wird intern über die Methode triggerStateChange(Trigger) an die Statemachine delegiert. Die Statemachine führt dann den entsprechenden Zustandsübergang durch, sofern er zulässig ist. Ist ein Zustandsübergang mit dem spezifizierten Trigger nicht möglich, wird in eine Exception geworfen.

ExampleServiceImplementation.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package io.codekontor.blog.springstatemachine.example.service.impl;

[...]

public class ExampleServiceImplementation implements IExampleService {

	@Autowired
	private StateMachine<States, Trigger> _stateMachine;

    [...]

	@Override
	public void start() {
		triggerStateChange(Trigger.START);
	}

	@Override
	public void stop() {
		triggerStateChange(Trigger.STOP);
	}

	@Override
	public void terminate() {
		triggerStateChange(Trigger.TERMINATE);
	}

	[...]
	
	private void triggerStateChange(Trigger trigger) {

		// throw an exception if the trigger was not accepted
		if (!_stateMachine.sendEvent(trigger)) {
	    	throw new RuntimeException("Trigger was not accepted.");
		}
	}
}

Um mit der Zustandsmaschine “echte” Aktionen ausführen zu können (wie die Initialiserung des zu startenden Dienstes), müssen wir für die gewünschten Ereignisse Callback-Methoden implementieren und dafür sorgen, dass die Callback-Methoden beim Auftreten der gewünschten Erreigisse (also bspw. dem Ausführen einer Transition oder Betreten eines Zustands) ausgeführt werden.

Eine Implementierungsmöglichkeit besteht darin, die Callback-Methoden entsprechenden Annotationen zu versehen (in der Beispielanwendung @OnStateEntry, @OnStateExit - die vollständige Liste aller verfügbaren Annotationen ist hier verfügbar). Die annotierten Methoden werden aufgerufenn, wenn die entsprechenden Ereignisse eintreten. Wichtig hierbei ist, dass die Klasse, die die annotierten Callback-Methoden enthält, zusätzlich mit der Annotation @WithStateMachine versehen ist.

ExampleServiceImplementation.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package io.codekontor.blog.springstatemachine.example.service.impl;

[...]

@WithStateMachine
public class ExampleServiceImplementation implements IExampleService {

    [...]

	@OnStateEntry(target = "STARTING")
	public void onEntrySTARTING(StateContext<States, Trigger> stateContext) {

    	[...]
	}

	@OnStateEntry(target = "STOPPING")
	public void onEntrySTOPPING(StateContext<States, Trigger> stateContext) {

    	[...]
	}
}

Der Beispieldienst in Aktion

Um den implementierten Service zu verwenden, können wir uns die Service-Implementierung (bzw. die entsprechende Instanz) via Dependency Injection injizieren lassen.

Application.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package io.codekontor.blog.springstatemachine.example;

[...]

@SpringBootApplication
public class Application implements CommandLineRunner {

	[...]

	@Autowired
	private IExampleService _service;

	@Override
	public void run(String... args) throws Exception {

		_service.start();

		try {
			// fails as the service already has been started
			_service.start();
		} catch (Exception e) {
			[...]
		}

		_service.stop();
		_service.start();
		_service.stop();
		_service.terminate();
	}

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}

Source-Code

Der Source-Code zum Blog ist unter https://github.com/code-kontor/Blog-Spring-Statemachine verfügbar.

Weiterführende Resourcen

  1. https://projects.spring.io/spring-statemachine/
  2. https://github.com/spring-projects/spring-statemachine
  3. https://docs.spring.io/spring-statemachine/docs/current/api
  4. https://en.wikipedia.org/wiki/UML_state_machine