Camunda und External-Tasks - Komplexe Daten als Json

Mittwoch, 22.3.2023

camunda-external-task-to-json

Seit einiger Zeit hat Camunda das External-Task-Pattern in seiner Process-Engine Camunda 7 (Platform) etabliert. Es handelt sich um einen Mechanismus, welcher die Ausführung von Prozessschritten in Service-Tasks von einem Push zu einem Pull-Mechnismus umkehrt, d.h. die Process-Engine ist nicht mehr für die tatsächliche Ausführung verantwortlich, wie es bei Java-Delegates der Fall ist. Stattdessen werden für verschiedene Topics jeweils Aufgaben bereitgestellt, die sich ein External-Task-Handler über eine Rest-API abholen und nach der Ausführung abschließen kann. Das genaue Verfahren haben wir bereits in einem vorherigen Beitrag erläutert. Auch, dass das Fehlerverhalten ein anderes ist als bei Java-Delegates, lässt sich in unserem Blog nachlesenIn diesem Beitrag geht es schließlich darum, wie sich komplexe Daten aus einem External-Task-Handler im Prozesskontext speichern lassen und was es dabei im Gegensatz zu Java-Delegates zu beachten gibt. Auf GitHub ist eine lauffähige Beispielanwendung zu finden, welche das Vorgehen verdeutlicht.

  

Komplexe Daten lesen und schreiben

Java-Delegates

Bei der weit verbreiteten Implementierungsweise von Service-Tasks als Java-Delegate müssen sich Entwickler:innen über das Speichern von komplexen Objekten meistens keine expliziten Gedanken machen. Voraussetzung ist ein ausreichend konfigurierter ObjectMapper, damit z.B. Datum- oder Zeitformaten menschenleserlich dargestellt werden. Komplexe Vererbungsstrukturen bei den zu speichernden Datentypen können zu Problemen führen, aber in der Regel funktioniert bereits vieles out-of-the-box. Daten können beliebig aus dem execution-Objekt gelesen und auch hinzugefügt werden und am Ende der Transaktion befinden sich die Daten serialisiert im Prozesskontext:

Hinweis: Dies ist ein vereinfachtes Beispiel. Es ist sehr empfehlenswert keine fremden Datentypen im Prozesskontext abzulegen!)

Als Resultat befindet sich im Prozesskontext ein entsprechendes Datenobjekt des Typs Customer, serialisiert als Json:

camunda_process-context_customer_object

Ausgelesen werden Datenobjekte ebenfalls über das execution-Objekt, ganz gleich ob es sich um primitive oder komplexe Daten handelt.

 

External-Tasks

Der Kerngedanke des External-Task-Pattern liegt darin, dass die Process-Engine ausschließlich als Orchestrator fungiert, indem sie den korrekten Prozessablauf sicherstellt. Mit dieser Architektur hat sie nicht mehr die Aufgabe Geschäftlogik aktiv auszuführen, i.d.S. dass beispielsweise Web-Services von ihr aufgerufen werden. Dadurch ist es möglich, die tatsächliche Ausführung auf dezentrale Anwendungen zu verteilen, von welchen die Process-Engine folglich keine technischen Details kennen muss. Technisch gesprochen: Auf dem Classpath der Prozessanwendung, welche die Process-Engine bereitstellt, befinden sich in der Regel keine Domänen-Objekte mehr, wie in unserem Fall der Customer-Typ. Dies führt dazu, dass die (De-)Serialisierung von komplexen Daten in die Hoheit des External-Task-Handlers wechselt und im Prozesskontext lediglich generische Strukturen wie Json erlaubt sind.

Im folgenden Beispiel wird deutlich was zu machen ist. Primitive Datentypen können nach wie vor aus dem Prozesskontext gelesen werden, so wie beispielsweise die customerId. Für das Schreiben des Customer-Objekts ist es jedoch notwendig die Serialisierung manuell durchzuführen, bevor der entsprechende Json-String in ein (Camunda-)Json-Value überführt wird:

Hinweis: In diesem Beispiel fehlt noch ein try-catch-Block um eventuelle Fehler bei der Serialisierung abzufangen und darauf zu reagieren. Andernfalls gerät der External-Task-Handler ggf. in eine Endlosschleife. Weitere Details und wie sich das Fehler- und Wiederholungsverhalten für External-Task-Handler automatisieren lässt, finden Sie in diesem Artikel.

Als Resultat befindet sich im Prozesskontext nun ein entsprechendes Json-Objekt, welches dieselben Inhalte enthält wie das Ergebnis des Java-Delegates zuvor, jedoch handelt es sich nun nicht mehr um den Typ Object, sondern stattdessen um den Typ Json, erkennbar an der Bezeichnung in der Spalte "Type":

camunda_process-context_customer_json

 

Beim Auslesen von komplexen Objekten in External-Tasks müssen die serialisierten Json-Werte ebenfalls Client-seitig in die entsprechenden Datentypen umgewandelt werden. Für diesen Zweck kommt erneut die verwendete Bibliothek für Serialisierung und Deserialisierung zum Einsatz, in unserem Fall ein Jackson-JsonMapper :

(Hinweis: Erneut fehlt ein geeignetes Fehlerverhalten.)

 

Prozesskontext-Wrapper und (De-)Serialisierung kapseln

Das bis hier her beschriebene Verfahren zeigt grundsätzlich die Unterschiede zwischen Java-Delegates und External-Tasks beim Speichern von Daten im Prozesskontext. Es ist jedoch ratsam, die Logik zur Erzeugung von Json-Daten zu kapseln, damit sie nicht in jeden External-Task-Handler einzeln hinzugefügt werden muss. Zu diesem Zweck ist in unserer Beispielanwendung der entsprechende Code im Prozesskontext-Objekt verwoben, welcher dadurch einen einfachen Umgang mit komplexen Datenstrukturen ermöglicht - auch im Json-Format. Als Wrapper-Klasse sind im Prozesskontext nämlich sowohl das Auslesen strukturierter Daten als auch eventuell notwendige Typ-Castings verborgen, sodass je sprechender Methode direkt der richtige Typ zurückgegeben wird, beispielsweise ein Costumer-Objekt (s.u.). Für weitere Details zum Thema "Prozesskontext" gibt es einen älteren Blogbeitrag.

Das JsonDataType-Interface stellt u.a. eine default-Methode toJson() bereit, durch welche jeder Datentyp, der dieses Interface implementiert, mit einem einzigen Methodenaufruf zum einem Json-String umgewandelt werden kann. Die eigentliche Serialisierung sowie die Fehlerbehandlung von checked Exceptions werden in jener Methode ausgeführt. Dies sorgt dafür, dass in der eigentlichen Geschäftslogik des External-Task-Handlers keine expliziete Serialisierung und auch keine Fehlerbahandlung geschehen muss:

Der Customer-Typ und das entsprechende Interface sehen wie folgt aus:

Das ProcessContext-Objekt enthält typisierte Methoden für den Zugriff auf Datenobjekte. Das konkrete Auslesen und die Umwandlung der Json-Daten in ein Domänenobjekt geschieht als Teil des JsonDataType-Interface. Zusätzlich ist die ProcessContext-Klasse so gestaltet, dass sie sowohl aus einem JavaDelegate heraus als auch in einem External-Task-Handler verwendet werden kann. Im jeweiligen Konstruktur wird ein entsprechender Enum-Wert gesetzt um stets unterscheiden zu können aus welcher Quelle die Variablen stammen. Dies ist hilfreich z.B. in einer Übergangsphase, während sowohl Java-Delegates also auch External-Tasks innerhalb einer Prozessanwendung existieren:

Die eigentliche Geschäftslogik im External-Task-Handler ist nun auch vollständig getrennt von den technischen Details bzgl. der Deserialisierung von Daten:

 

Fazit

Der Umbau von Java-Delegates zu External-Tasks erfordert einen anderen Umgang mit komplexen Datenstrukturen. Serialisierung und Deserialisierung werden nicht mehr von der Process-Engine übernommen, da diese entkoppelt von der eigentlichen Geschäftslogik existiert. Aus diesem Grund müssen diese Schritte Client-seitig in den External-Task-Handlern übernommen werden - mit allen Konsequenzen, die das für Entwickler:innen bedeutet. Durch eine intelligente Struktur einer ProcessContext-Klasse als Wrapper um die Daten und ggf. einem gemeinsamen Interface für die Datentypen, welche als Variable gespeichert werden, lässt sich der Aufwand für die (De-)Serialisierung jedoch zentralisieren und dadurch möglichst gering halten. Sollte es bereits eine Wrapper-Klasse für den Zugriff auf Prozessvariablen geben, so lässt diese sich entsprechend erweitern bzw. umgestalten, sodass ein Umbau auf External-Tasks bzw. Json-Daten nur sehr wenig Auswirkungen auf die eigentliche Geschäftlogik hat, da die Datentypen unverändert bleiben. Ein weiterer Vorteil der Kappslung der Mechanismen für Serialisierung und Deserialisierung ist die Wiederverwenung über mehrere External-Task-Handler und Datentypen hinweg, sodass z.B. die Fehlerbehandlung in Form eines Try-catch-Konstrukts nicht in jeden Handler kopiert werden muss.

 


zurück zur Blogübersicht

Diese Beiträge könnten Sie ebenfalls interessieren

Keinen Beitrag verpassen – viadee Blog abonnieren

Jetzt Blog abonnieren!

Kommentare

Florian Runschke

Florian Runschke

Florian Runschke ist seit 2017 Berater im Bereich Business Proccess Management. Er hat bereits an unterschiedlichen Architekturen für Prozessautomatisierungsplattformen mitgearbeitet, ist Open Source-Enthusiast und sucht stets nach schlanken Lösungen um Herausforderungen effizient zu bewältigen. Für den Camunda-Modeler hat er das Tooltip-Plugin entwickelt.

Florian Runschke auf LinkedIn