Anwendungs-Integration mit Apache Camel

Von Hendrik Jungnitsch, GEDOPLAN GmbH

Das Thema verschiedene Anwendungen miteinander zu vernetzen und untereinander kommunizieren zu lassen ist kein neues. Im Laufe der Zeit gab es eine ganze Reihe von Ansätzen und Architekturmustern um solche Anwendungslandschaften aufzubauen. ESBs (Enterprise-Service-Bus) stellen eine Möglichkeit der Integration von heterogenen verteilten Systemen dar,  sind allerdings auf Grund ihrer hohen Komplexität zunehmend in Missgunst innerhalb der Entwicklergemeinde geraten. Tatsächlich dürfte in den meisten Anwendungsszenarien ein ESB „des Guten zu viel“ sein und damit am Ende des Tages für unnötige Komplexität in den betroffenen Projekten sorgen. Dennoch ist das Thema Anwendungs-Integration nach wie vor aktuell, da es immer wieder Problemstellungen gibt bei denen es darum geht Legacy-Systeme zu integrieren, oder wo das Routing der Daten zwischen den Anwendungen entsprechende Anforderungen stellt. Hier verspricht eine Routing-Engine wie Apache Camel eine leichtgewichtige aber mächtige Alternative zu sein.

Camel ist ein freies Integration-Framework für das regelbasierte Routen und Konvertieren von Daten. Es bietet eine Java-basierte Umsetzung bekannter Enterprise-Integration-Patterns (EIP), welche mit Hilfe
verschiedener DSLs (Java, XML, Scala) deklarativ für den entsprechenden Anwendungsfall definiert werden können.
Als Apache-Projekt ist es unter der Apache License 2.0 verfügbar und wird gegenwärtig in der Version 2.14.0 angeboten.

Anwendungsintegration und Enterprise-Integration-Patterns

Anwendungsintegration spielt immer dann eine Rolle, wenn es darum geht verschiedene Anwendungen / Services miteinander zu verbinden und sie somit in eine gemeinsame Systemlandschaft einzugliedern. Dies ist z.B. erforderlich, wenn ein Fremdsystem an eine Anwendung angeschlossen werden muss, oder wenn innerhalb des Unternehmens eine Landschaft wiederverwendbarer Komponenten aufgebaut werden soll (SOA/Microservices).

Das Vernetzen von Anwendungen bringt eine ganze Reihe von Problemstellungen mit sich: Möglicherweise sprechen nicht alle teilnehmenden Services die gleichen Protokolle, was bedeutet, dass eine Vielzahl unterschiedlicher Transportwege für Daten berücksichtigt werden muss. Daten können wahrscheinlich nicht von allen Schnittstellen in der gleichen Form angenommen werden, weshalb ein Konvertieren erforderlich wird. Eventuell müssen Daten aufgeteilt oder gesammelt werden, bevor sie zum nächsten System weitergeleitet werden können. Schnittstellen mit synchroner aber auch asynchroner Kommunikation können nebeneinander vertreten sein und müssen miteinander integriert werden. Selbstverständlich sollte dieser Datenverkehr nachvollziehbar sein, also muss er entsprechend geloggt und überwacht werden können.

Da dies immer wiederkehrende Problemstellungen im Bereich der Integration sind lag es nahe mögliche Herangehensweisen für ihre Lösung in Form von Designmustern und Best-Practices festzuhalten. Genau dies haben Gregor Hohpe und Bobby Woolf in ihrem Buch „Enterprise Integration Patterns“ (2004) getan. Sie haben insgesamt 65 solche Designmuster identifiziert und dokumentiert, welche nun nach dem Namen des Buches als Enterprise-Integration-Patterns (EIP) bekannt sind.

Natürlich wird schon das bloße Wissen um diese Pattern, den Aufwand für einen Integrationsentwurf  drastisch reduzieren, da nun nicht mehr für jedes dieser Probleme eine eigene Lösung zu erarbeiten ist. Allerdings ist die Umsetzung weiterhin entsprechend komplex und wird mit jeder Menge „technischem“ Code einhergehen.  Um das Entwickeln zu vereinfachen gibt es daher Frameworks wie Apache Camel oder Spring Integration, welche eine Umsetzung der EIPs und eine entsprechende API für die Nutzung dieser bereitstellen.

Camel - Grundlegende Konzepte

Integrationsszenarios werden in Camel mit Hilfe von Routen beschrieben. Diese bestehen aus einem Start- und einem Endpunkt und definieren den genauen Datenfluss dazwischen. Die Routen werden deklarativ mit Hilfe einer DSL beschrieben.  Angesteuert wird über diese Schnittstelle das Messagingsystem von Camel, welches eine technische Umsetzung der EIPs bereitstellt.

Start-und Endpunkte oder Schnittstellen allgemein werden durch sogenannte Endpoints repräsentiert. Ein Endpoint beschreibt eine Schnittstelle mit allen dazugehörenden Informationen, wie physikalischer und logischer Adresse, Kommunikationsprotokoll und weiteren Einstellungen. Ein Endpoint könnte somit z.B. einen Webservice, einen Ordner im Dateisystem oder einen FTP-Server beschreiben. Endpoints werden in Camel in der Regel von einer Factory genannt Component erzeugt. Es gibt somit in Camel für jede Übertragungsart einen eigenen Component der dazu in der Lage ist entsprechende Endpoints zu erzeugen. Endpunkte werden in Routen in der Regel mit Hilfe von URIs angesprochen. Diese URIs bestehen aus dem vorangestellten Namen eines Components, sowie weiteren Parametern für die Konfiguration des Endpoints.

Beispiel URI für einen Ordner im Dateisystem: file://C:\\Temp\testfolder

In diesem Fall stellt file den Component dar (oder genauer gesagt wird aufgelöst zu dem Component der für diesen Namen registriert ist) und der restliche Teil der URI wird an die createEndpoint Methode  übergeben wo er genutzt wird um den Endpoint mit den entsprechenden Informationen zu konfigurieren. In diesem Fall besteht die Information nur aus dem Pfad im Dateisystem.

Mit diesem Wissen ist es nun schon möglich eine erste kleine Route zu definieren welche Dateien von einem Verzeichnis in ein anderes kopiert. Routen können in Camel mit Hilfe einer Vielzahl verschiedener DSLs (Domain-Specific-Language) deklariert werden. In diesem Artikel soll allerdings nur auf die Java-DSL eingegangen werden, welche in Form eines Builder-Patterns umgesetzt worden ist.

from(“file://C:\\Temp\import”).to(“file://C:\\Temp\export”)

Diese Routendefinition würde dafür sorgen, dass Camel das Verzeichnis import in regelmäßigem Abstand auf neue Files abfragt und diese in das export Verzeichnis kopiert. Der Beginn einer Route wird immer mit dem Schlüsselwort (Methodenaufruf) from eingeleitet und beschreibt einen sogenannten Consumer, der dazu in der Lage ist eingehende Daten entgegenzunehmen. Mit dem Schlüsselwort to wird ein Producer definiert, welcher dazu dient, Daten herauszuschreiben. Ein Endpoint ist also dazu in der Lage einen Consumer und einen Producer bereitzustellen, wobei es da auf die Implementierung des Endpoints ankommt, nicht jeder Endpoint muss beides implementieren. Der File-Endpoint aus dem Beispiel bietet jedoch beides an.

Die Informationen zwischen den Endpoints werden in Form von Messages ausgetauscht. Eine Message ist ein protokollspezifischer Wrapper um die Payload, also die tatsächlich übertragenen Daten. Eine Message enthält verschiedene Daten wie eine Message-ID, Header-Informationen und den Body (Payload). Ein Austausch von Messages wird in Camel durch ein Exchange Objekt abgebildet. In einer Exchange werden alle Daten des Austausches festgehalten, wie z.B. ein- und ausgehende Messages oder aufgetretene Exceptions.

Zwischen Endpoints können beliebig viele Schritte für das Verarbeiten und Transformieren der Message in der Route platziert werden. Diese Routenelemente sind die sogenannten Processors, welche in Camel durch ein gleichnamiges Interface repräsentiert werden. Ein Processor bietet eine Methode „process“ an, die ein Exchange-Objekt als Parameter entgegennimmt. Innerhalb der Methode kann die Exchange nach Informationen befragt aber auch manipuliert werden. Es besteht die Möglichkeit eigene Processors zu implementieren, allerdings stellt Camel bereits eine ganze Reihe Processor-Implementierungen, die jeweils ein spezifisches EIP umsetzen, zur Verfügung.

Getting Started

Um Camel in eine Anwendung einzubinden wird zunächst nur die camel-core Bibliothek benötigt, diese kann z.B. einfach als Maven-Dependency in das Projekt eingebunden werden. Enthalten sind in dem Core die grundliegende Engine, alle relevanten Interfaces, sowie Implementierung von einigen grundlegenden Components. Eine Laufzeitumgebung für Camel-Routen wird durch den CamelContext repräsentiert. Dieser führt eine Registry für Components und DataTypes und bietet Operationen an, wie das Registrieren von RouteBuildern oder das Starten und Stoppen von Routen.

Somit wäre der erste Schritt in einer Camel-Anwendung das Erzeugen des CamelContexts:

CamelContext camelContext = new DefaultCamelContext();

Um eine Route mit Hilfe der Java-DSL zu definieren wird ein RouteBuilder benötigt. Dies ist eine Klasse, die von der abstrakten Klasse RouteBuilder erbt. Beschrieben werden die Routen in der Methode configure, wo durch das Ableiten die entsprechenden Methoden für das Bauen einer Route zur Verfügung stehen.

public class MyRouteBuilder extends RouteBuilder {

    public void configure() throws Exception {

        from(“file://C:\\Temp\import”).to(“file://C:\\Temp\export”);

    }

}

Der RouteBuilder kann nun beim CamelContext registriert werden:

camelContext.addRoutes(new MyRouteBuilder());

Anschließend wird der Context gestartet, womit auch die aus dem Builder erzeugten Routen aktiv werden.

camelContext.start();

Um den Context zu beenden und alle Routen herunterzufahren muss die stop-Methode aufgerufen werden.

camelContext.stop();

Routing

Innerhalb der Routen, zwischen den Endpunkten, können eine Vielzahl verschiedener Routing-Elemente untergebracht werden. Auf diese Art ist es möglich Gebrauch von den entsprechenden EIPs zu machen. Camel bietet hier zum einen Bausteine an, um Daten zu (de-) serialisieren, zu transformieren und zu konvertieren. Aber auch Konstrukte, die das Aufsplitten, die Parallelverarbeitung, das inhaltsbasierte Weiterverteilen oder das Aggregieren von Messages ermöglichen. Mit diesen Hilfsmitteln können auf einfache Weise komplexe Datenverläufe zwischen Systemen modelliert werden.

Unterschiedliche Routen können in Camel über In-Memory-Endpoints miteinander verknüpft werden. Dafür werden die Endpunkttypen direct und seda (innerhalb eines CamelContexts), sowie vm-direct und vm (contextübergreifend innerhalb einer VM) angeboten. Solche Endpoints können über eine URI der Form typ:name nach Belieben definiert werden. Ein Beispiel für zwei verknüpfte Routen:

from(“direct:moveToExport”).to(“file://C:\\export”)

from(“file://C:\\import”).to(“direct:moveToExport”)

Hier noch ein Beispiel für eine komplexere Camel-Route:

from(“direct:veranstaltungsImport”).routeId("Veranstaltungs Import")

                .split(stax(Veranstaltung.class),new ImportAggregator())

                .parallelProcessing().to("bean-validator://x")

                    .beanRef("veranstaltungen","create")

                    .wireTap(“direct:veranstaltungImorted”).copy().end()

                .end()

                .to(“direct:processStatistic”);

Diese Route hat die Aufgabe Veranstaltungen zu importieren. Als eingehende Datenbasis wird ein XML-Format erwartet, welches beliebig viele Veranstaltungs-Elemente enthalten kann. Zuerst werden die eingehenden Daten mit STAX gesplittet und danach parallel auf Einzeldatensatz-Ebene weiter verarbeitet. Dem Splitter kann ein Aggregator mitgegeben werden, dessen Ergebnis dann am Ende der Einzeldatensatz Verarbeitung (korrespondierende end() Anweisung) als Payload weitergereicht wird. In diesem Fall würde das aggregierte Ergebnis noch an eine andere Route weitergeleitet werden, welche sich um das Verarbeiten der gesammelten statistischen Daten kümmert. Für jeden Einzeldatensatz wird in dem Beispiel zuerst einmal eine Validierung vorgenommen, anschließend wird er an eine Bean-Methode mit entsprechender Businesslogik übereicht. Mit Hilfe des Wiretaps wird eine Verzweigung aufgebaut, wo eine Kopie der Payload an eine andere Route übertragen wird. In dieser Route wäre es dann möglich für jeden Datensatz der importiert wurde, unabhängig von der Import-Route, weitere Aktionen auszuführen (z.B. Logging). Dadurch, dass die Import-Route von einem In-Memory-Ednpoint aus beginnt, wird es möglich mehrere Adapter für unterschiedliche Transportwege davorzusetzen, also z.B. Import aus dem Dateisystem oder über einen Webservice.

Components

Camel unterstützt eine ganze Reihe unterschiedlicher Transportwege, die eine Anbindung an eine Vielzahl von externen Systemen ermöglichen. Die meisten dieser Components sind nicht in der Core-Bibliothek enthalten und müssen entsprechend als Modul dazu geladen werden. Diese Vorgehensweise sorgt dafür, dass Camel sehr leichtgewichtig ist, da nur die wirklich benötigten Komponenten in die Anwendung eingebunden sind. Um eine zusätzliche Komponente in eine Camel-Anwendung zu integrieren, ist lediglich das Hinzufügen der entsprechenden Dependency erforderlich (mit derselben Version wie core).

Insgesamt stehen über 100 Komponenten zur Verfügung welche es unter anderem ermöglichen, Verbindung zu Mail- und FTP-Servern herzustellen, relationale und NoSQL Datenbanken anzusprechen, Webservices bereitzustellen und aufzurufen, JMS-Messages zu konsumieren, mit den APIs von Twitter und Facebook zu kommunizieren und noch vieles mehr.

DataFormats

Das Serialisieren bzw. Deserialisieren wird in Camel mit Hilfe von DataFormats vorgenommen. Diese kommen vorwiegend dann zum Einsatz, wenn es darum geht, eingehende Daten in Businessobjekte zu konvertieren, oder diese Objekte vor dem raussenden wieder in ein übertragbares Format zu überführen. Es gibt eine ganze Reihe von DataFormats in Camel für verschiedene Formate, wie z.B. JSON, JAXB, CSV oder ZIP.

BeanBinding

Camel bietet verschiedene Möglichkeiten für das Aufrufen von Java-Methoden aus einer Route heraus an. Beans können in den Routen entweder über einen Namen, der im CamelContext registriert sein muss, oder einen Typ angesprochen werden. Standardmäßig sollte die auszulösende Methode einen Parameter enthalten, der genutzt wird um die Payload der Message zu übergeben. Wenn die Methode einen Rückgabewert definiert, so wird dieser anschließend als neue Payload an den nächsten Schritt der Route weitergeleitet.

ErrorHandling

Für die Fehlerbehandlung innerhalb von Routen stellt Camel eine ganze Reihe von Werkzeugen bereit. Zum einen besteht die Möglichkeit, eine Art Try-Catch-Syntax in Form von Routenelementen zu verwenden. Darüber hinaus ist es aber auch möglich, einen Exception-Handler zu definieren, der global oder per Route und auch per Exception-Typ konfigurierbar ist. Eine weitere Option für den Umgang mit unvorhergesehenen Exceptions während des Routings besteht in dem Einrichten eines Deadletter-Channels.

Logging und Monitoring

Bei einer Integrationsanwendung stellt das Thema Logging eine besondere Herausforderung dar. Im System befinden sich zu jedem Zeitpunkt eine Vielzahl von Messages die in unterschiedlichen Routen unterwegs sind und von verschiedenen Processors und in vielen Threads verarbeitet werden. Um hier nicht den Überblick zu verlieren, ist es wichtig, die Log-Messages dem richtigen Kontext zuordnen zu können. Um dies zu erreichen,  unterstützt Camel das sogenannte MDC (Mapped-Diagnostic-Context)-Logging, was nach dem Prinzip funktioniert, dass es einen Speicher gibt, in dem aktuelle Context-Informationen festgehalten und dann über entsprechende Pattern als Zusatzinformationen in eine Loggerausgabe miteingebunden werden können. Im Falle von Camel wären das dem zufolge Informationen wie CamelContext-ID, Routen-ID oder Message-ID.

Standardmäßig sammelt Camel bereits jede Menge statistische Daten zu den Routen und Endpunkten, die anschließend per JMX-Beans zur Verfügung gestellt werden. Sollte dies nicht ausreichend sein, kann seit der Version 2.14.0 zusätzlich das neue Metrics-Modul eingebunden werden. Eine bequeme Möglichkeit um die gesammelten Informationen abzurufen und zu visualisieren stellt hier die webbasierte JMX-Console HawtIO dar, welche bereits über ein spezialisiertes Camel-Modul verfügt.

Setup Szenarios

Was die Laufzeitumgebung anbelangt ist Camel sehr flexibel. Es ist problemlos möglich eine Camel-Anwendung als Standalone JavaSE Applikation zu betreiben, aber auch das Deployment in einen Webcontainer oder Applicationserver stellt eine Option dar. Ebenso angesagt ist die Variante Camel in einer OSGI Runtime wie z.B. Apache Karaf zu betreiben. Dies bietet den Vorteil, dass neue Components oder Routen als OSGI-Modul zur Laufzeit hinzugefügt werden können, ohne die Anwendung zu stoppen.

Die Wahl des geeigneten Aufbaus hängt natürlich stark von den Anforderungen des Integrationsszenarios ab. Wenn es lediglich darum geht, einer Anwendung mehr Konnektivität zu verleihen, so liegt es nahe, Camel einfach in diese einzubetten, also im Falle einer Webanwendung mit dem WAR zusammen in den Container zu deployen. Soll eine Integrationsschicht aufgebaut werden, die mehrere eigene Anwendungen miteinander verbindet, dann macht es sicherlich Sinn eine eigene Integrationsanwendung dafür zu erstellen.

Fazit

Insgesamt lässt sich festhalten, dass Camel ein sehr mächtiges und flexibles Framework für Anwendungs-Integration darstellt, welches aufgrund seines modularen Aufbaus auch noch recht leichtgewichtig daherkommt. Die Möglichkeit Routen direkt typsicher in Java beschreiben zu können ist ein weiterer Pluspunkt, weil somit keine spezielle Entwicklungsumgebung erforderlich wird. Auch schön ist natürlich die Flexibilität, die Camel zeigt, wenn es um die Laufzeitumgebung geht, da man diese den eigenen Bedürfnissen entsprechend wählen kann und somit auch die Integration mit eigenem Java Beans z.B. in einem Java EE Server problemlos möglich wird.

Die Dokumentation von Camel und den verfügbaren Components fällt für ein Apache Projekt relativ umfangreich aus und eignet sich daher gut als Nachschlagewerk für die Konfigurationsmöglichkeiten von Endpoints und Routingelementen.

Aufgrund der Komplexität des Themas Integration an sich und auch auf Grund des Umfangs von Camel ist aber natürlich mit einer entsprechenden Einarbeitungszeit in das Thema zu rechnen.

Unsere Kundenzeitschrift GEDOPLAN aktuell

In dieser Rubrik stellen wir Ihnen einen Artikel aus der aktuellen Ausgabe unserer Kundenzeitschrift zum Online-Lesen zur Verfügung. Alle weiteren Ausgabe zum Download finden Sie hier.

Aktueller Kurs zum Thema:

Anwendungs-Integration mit Apache Camel 

* Nächster Termin: 05.10. - 07.10.2016 in Berlin.