In 8 Schritten um einen Workflow Prozess zu bauen
Camunda + Spring Boot ein kleines einfaches Beispiel
Erwartete Lesezeit: 7 minuten
Bei der Entwicklung eines prozessgesteuerten Mikroservices, habe ich mir verschiedene Prozessengines angesehen und die Entwicklungsvariante mit Camunda gefiel mir recht gut. Ich war überrascht , wie einfach es war.
Hier ein kleines Beispiel für einen Prozess, der prüft ob meine Website online ist. Zur Anzeige habe ich hier einen Human Task eingebaut , in dem man das Ergebnis sehen wird.
Schritt 1: Anlage eines einfachen Maven Projektes
Als Dependencies benötigen wir zum einen Spring Boot und einmal Camunda
Hier die fertige POM.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>de.oderkerk.bpm</groupId> <artifactId>testprocess</artifactId> <version>0.0.1-SNAPSHOT</version> <name>Testapp combining spring boot and camunda</name> <properties> <build.plugins.plugin.version>2.0.5.RELEASE</build.plugins.plugin.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.0.5.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.camunda.bpm.springboot</groupId> <artifactId>camunda-bpm-spring-boot-starter-webapp</artifactId> <version>3.0.0</version> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${build.plugins.plugin.version}</version> <configuration> <layout>ZIP</layout> </configuration> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
Schritt 2: Hinterlegung einer Datenbank
Für dieses Beispiel habe ich eine lokale H2 Datenbank gewählt und diese in der Application.properties Datei konfiguriert :
# Datasource spring.datasource.url=jdbc:h2:file:./target/db spring.datasource.username=sa spring.datasource.password= spring.datasource.driver-class-name=org.h2.Driver spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
Um während des Tests mal in die Datenbank schauen zu können , habe ich zusätzlich die H2 Console ebenfalls durch einen Eintrag in der Application.properties Datei aktiviert. (Kein Muss)
# H2 spring.h2.console.enabled=true spring.h2.console.path=/h2
Schritt 3: Automatische Anlage Admin User
Um nach dem Start der Anwendung gleich nutzen zu können, legen wir einen Admin User an, der beim ersten Start automatisch angelegt wird. Hat man einen LDAP Server im Einsatz, kann man diesen natürlich nehmen. Für dieses Beispiel bleiben wir aber bei einem lokalen Adminuser.
In der Application.properties Datei fügen wir die folgenden Zeilen hinzu :
camunda.bpm.admin-user.id=User camunda.bpm.admin-user.email=mail@example.com camunda.bpm.admin-user.first-name=Admin camunda.bpm.admin-user.last-name=Mustermann camunda.bpm.admin-user.password=password
Nun können wir uns nach dem Start der Anwendung mit dem Logininformationen
Benutzername: User
Passwort: password
anmelden.
Zur Konfiguration eines ersten Filters für Tasks erweitern wir die Application.properties noch um die Zeile:
camunda.bpm.filter.create=All tasks
sodass die gesamte Application.properties Datei nun wie folgt aussieht:
# H2 spring.h2.console.enabled=true spring.h2.console.path=/h2 # Datasource spring.datasource.url=jdbc:h2:file:./target/db spring.datasource.username=sa spring.datasource.password= spring.datasource.driver-class-name=org.h2.Driver spring.jpa.database-platform=org.hibernate.dialect.H2Dialect camunda.bpm.admin-user.id=User camunda.bpm.admin-user.email=mail@example.com camunda.bpm.admin-user.first-name=Admin camunda.bpm.admin-user.last-name=Mustermann camunda.bpm.admin-user.password=password camunda.bpm.filter.create=All tasks
Schritt 4 : Definition des Prozesses
Über den Camunda Modeler definieren wir den folgenden Prozess:
<?xml version="1.0" encoding="UTF-8"?> <bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="Definitions_0xqpusa" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="2.0.1"> <bpmn:process id="Testprocess" name="Testprocess" isExecutable="true" camunda:versionTag="1.0"> <bpmn:startEvent id="StartEvent_1" camunda:asyncAfter="true" camunda:jobPriority="1"> <bpmn:extensionElements> <camunda:failedJobRetryTimeCycle>30</camunda:failedJobRetryTimeCycle> </bpmn:extensionElements> <bpmn:outgoing>SequenceFlow_0wa4k97</bpmn:outgoing> </bpmn:startEvent> <bpmn:userTask id="Task_05lu7sy" name="Approve"> <bpmn:incoming>SequenceFlow_1mv8z0d</bpmn:incoming> <bpmn:outgoing>SequenceFlow_096mb78</bpmn:outgoing> </bpmn:userTask> <bpmn:endEvent id="EndEvent_0wk49f5"> <bpmn:incoming>SequenceFlow_096mb78</bpmn:incoming> </bpmn:endEvent> <bpmn:sequenceFlow id="SequenceFlow_0wa4k97" sourceRef="StartEvent_1" targetRef="Task_Sendping" /> <bpmn:sequenceFlow id="SequenceFlow_096mb78" sourceRef="Task_05lu7sy" targetRef="EndEvent_0wk49f5" /> <bpmn:serviceTask id="Task_Sendping" name="Send Ping" camunda:class="de.oderkerk.bpm.test.PingWebsiteDelegate"> <bpmn:extensionElements> <camunda:inputOutput> <camunda:inputParameter name="stateOfPing" /> <camunda:outputParameter name="stateOfPing" /> </camunda:inputOutput> </bpmn:extensionElements> <bpmn:incoming>SequenceFlow_0wa4k97</bpmn:incoming> <bpmn:outgoing>SequenceFlow_1mv8z0d</bpmn:outgoing> </bpmn:serviceTask> <bpmn:sequenceFlow id="SequenceFlow_1mv8z0d" sourceRef="Task_Sendping" targetRef="Task_05lu7sy" /> </bpmn:process> <bpmndi:BPMNDiagram id="BPMNDiagram_1"> <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Testprocess"> <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1"> <dc:Bounds x="173" y="102" width="36" height="36" /> </bpmndi:BPMNShape> <bpmndi:BPMNShape id="UserTask_0mxlnei_di" bpmnElement="Task_05lu7sy"> <dc:Bounds x="545" y="80" width="100" height="80" /> </bpmndi:BPMNShape> <bpmndi:BPMNShape id="EndEvent_0wk49f5_di" bpmnElement="EndEvent_0wk49f5"> <dc:Bounds x="753" y="102" width="36" height="36" /> </bpmndi:BPMNShape> <bpmndi:BPMNEdge id="SequenceFlow_0wa4k97_di" bpmnElement="SequenceFlow_0wa4k97"> <di:waypoint x="209" y="120" /> <di:waypoint x="267" y="120" /> </bpmndi:BPMNEdge> <bpmndi:BPMNEdge id="SequenceFlow_096mb78_di" bpmnElement="SequenceFlow_096mb78"> <di:waypoint x="645" y="120" /> <di:waypoint x="753" y="120" /> </bpmndi:BPMNEdge> <bpmndi:BPMNShape id="ServiceTask_1v5muvw_di" bpmnElement="Task_Sendping"> <dc:Bounds x="267" y="80" width="100" height="80" /> </bpmndi:BPMNShape> <bpmndi:BPMNEdge id="SequenceFlow_1mv8z0d_di" bpmnElement="SequenceFlow_1mv8z0d"> <di:waypoint x="367" y="120" /> <di:waypoint x="545" y="120" /> </bpmndi:BPMNEdge> </bpmndi:BPMNPlane> </bpmndi:BPMNDiagram> </bpmn:definitions>
Der Service Task ruft eine Javaklasse mit einem JavaDelegate auf :
Den manuellen User Task legen wir wie folgt an :
Schritt 5 : Startklasse erstellen
Hier erstellen wir eine Klasse , die die Annontationen Spring Boot und EnableAutoconfiguration sowie die Main Methode beinhaltet :
package de.oderkerk.bpm.test; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @EnableAutoConfiguration public class TestApp { public static void main(String... args) { SpringApplication.run(TestApp.class, args); } }
Schritt 6 : JavaDelegate erstellen
Wir haben in der Prozessdefinition dem Servicetask die Klasse PingWebsiteDelegate im Package de.oderkerk.bpm.test zugewiesen.
Diese Klasse implementiert das Interface org.camunda.bpm.engine.delegate.JavaDelegate und beinhaltet eine execute Methode.
public class PingWebsiteDelegate implements JavaDelegate { private final static Logger LOGGER = LoggerFactory.getLogger(PingWebsiteDelegate.class); public void execute(DelegateExecution execution) throws Exception { if (LOGGER.isDebugEnabled()) LOGGER.debug("Starting execute Delegate PingWebsiteDelegate "); execution.setVariable("stateOfPingResult", getStatus("https://www.marcooderkerk.de")); }
Da wir hier nicht viel machen wollen, setzen wir in der Delegateexecution nur eine neue Variable stateOfPingResult mit dem Ergebnis der Verfügbarkeitsprüfung getStatus.
Die komplette Klasse sieht wie folgt aus:
package de.oderkerk.bpm.test; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import org.camunda.bpm.engine.delegate.DelegateExecution; import org.camunda.bpm.engine.delegate.JavaDelegate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class PingWebsiteDelegate implements JavaDelegate { private final static Logger LOGGER = LoggerFactory.getLogger(PingWebsiteDelegate.class); public void execute(DelegateExecution execution) throws Exception { if (LOGGER.isDebugEnabled()) LOGGER.debug("Starting execute Delegate PingWebsiteDelegate "); execution.setVariable("stateOfPingResult", getStatus("https://www.marcooderkerk.de")); } public String getStatus(String url) throws IOException { String result = ""; try { URL urlObj = new URL(url); HttpURLConnection con = (HttpURLConnection) urlObj.openConnection(); con.setRequestMethod("GET"); // Set connection timeout con.setConnectTimeout(3000); con.connect(); int code = con.getResponseCode(); if (code == 200) { result = "Online"; } } catch (Exception e) { result = "Offline"; } return result; } }
Schritt 7 : Finalisierung
Zu guter letzt erzeugen wir im resource Verzeichnis einen Ordner META-INF und dort eine leere Datei processes.xml. Die Datei kann für unser Beispiel leer bleiben.
Schritt 8 : Build und Start der Anwendung
Wir führen einen Maven Build aus und starten anschließend die Anwendung
Ergebnis:
Nach dem Start sehen wir in der Konsole, dass eine Camunda instanz startet.
____ _ ____ ____ __ __ / ___| __ _ _ __ ___ _ _ _ __ __| | __ _ | __ )| _ \| \/ | | | / _` | '_ ` _ \| | | | '_ \ / _` |/ _` | | _ \| |_) | |\/| | | |__| (_| | | | | | | |_| | | | | (_| | (_| | | |_) | __/| | | | \____/\__,_|_| |_| |_|\__,_|_| |_|\__,_|\__,_| |____/|_| |_| |_| Spring-Boot: (v2.0.5.RELEASE) Camunda BPM: (v7.9.0) Camunda BPM Spring Boot Starter: (v3.0.0)
Nachdem die Anwendung komplett hochgefahren ist, rufen wir in einem Webbrowser die Adresse http://localhost:8080 auf und melden und mit den administrierten Username und Passwort an. (User:password)
Wir landen auf der Camunda Tasklist und sehen auf der Linken Seite den erstellten Filter “All tasks”.
Über einen Klick auf Start Process können wir unseren Prozess manuell starten:
Hier wählen wir den Testprocess aus
Da wir keine weiteren Daten eingeben müssen , klicken wir einfach auf Start
Nun wechseln wir ins Cockpit und sehen , dass der Prozess nun am manuellen Schritt wartet :
Zurück in der Task List sehen wir nun den auf uns wartenden Vorgang und nach der Auswahl des Tasks Approve und klick auf Load Variables sehen wir das Ergebnis in der Variablen stateOfPingResult:
Das bedeutet unser Prozess hat den Ping ausgeführt. Der Form halber klicken wir auf Complete und der Prozess ist abgeschlossen. Zugegeben , dieses war ein sehr simpler Prozess, dieser ist aber leicht ausbaubar.