Problembeschreibung

Gerade bei Geschäftsprozessen, die abteilungs- oder organisationsübergreifend sind, müssen dabei verschiedene Systeme integriert werden. Hierbei werden oftmals Webservices auf Basis von SOAP verwendet, mit denen plattformunabhängig und standardisiert Systemfunktionen angeboten werden können. Hierzu werden alle Daten als XML codiert und versendet.

Viele der implementierten Prozesse weisen ähnliche Eigenschaften bzw. technische Herausforderungen auf, die außerhalb der eigentlichen BPMN- Modellierung liegen:

Ein Teil dieser Probleme und noch viele andere werden transparent durch das BPMS gelöst. Verbreitete BPMS unterstützen die Ausführung von Prozessinstanzen, die zu verschiedenen Modellversionen gehören. Allerdings ist es nur sehr schwer bis praktisch unmöglich bestehende Prozessinstanzen auf das neueste Prozessmodell zu migrieren. Hierbei haben viele BPMS starke Restriktionen bzgl. der erlaubten Änderungen, die meistens dazu führen, dass viel manuelle Arbeit nötig wäre, um die Instanzen zu migrieren. Daher werden in der Praxis oftmals die bestehenden Prozesse zu Ende ausgeführt und nur neue Prozessinstanzen nutzen das neue Prozessmodell.

Dadurch ist es aber nun schwerer möglich, auf Änderungen in der Umgebung zu reagieren. Wird zum Beispiel ein benutzter Service in einer neuen Version zur Verfügung gestellt, kann zwar ein neues Prozessmodell deployed werden, allerdings kann es sein, dass noch einige Wochen später eine alte Prozessinstanz versucht, auf die bereits abgeschaltete, alte Serviceversion zuzugreifen.

Neben dem Problem der sich ändernden umgebenden Systemlandschaft führt eine weitere, eigentlich sehr nützliche Eigenschaft von BPMS zu Problemen: Damit Prozessinstanzen auch im Falle eines Serverabsturzes nicht verloren gehen, werden sie mit allen Instanzdaten persistiert. Werden nun aber große Datenmengen durch den Prozess geroutet, wie z.B. PDF-Dateien für Anträge, liegen große Datenmengen in der Datenbank und verlangsamen so zusehends das System.

Um diesen beiden Probleme zu begegnen, wird eine Pufferschicht um das Prozessmodell und die Prozessinstanzen benötigt. Wie diese Schicht einfach realisiert werden kann und wie eine sinnvolle Architektur dafür aussieht, wollen wir im Laufe dieses Artikels erklären.

BPMN vs. Camel

An dieser Stelle kommt das Integrationsframework Apache Camel ins Spiel. Apache Camel ist eine Routing- und Transformation-Engine für Nachrichten jeglicher Art. Es ermöglicht eigene Routing-Regeln zu erstellen. Dabei wird definiert, von welchem externen Provider die Nachrichten abgerufen werden, wie sie zu verarbeiten sind und zu welchen Zielen diese Nachrichten dann weitergeleitet werden.

Die Übernahme einer Nachricht zum Routing kann dabei von vielen unterschiedlichen Schnittstellen erfolgen. Um nur einige aus einer großen Vielzahl von Schnittstellenmöglichkeiten zu nennen, kann eine Nachricht z.B. eine eingelesene Datei aus dem Filesystem sein, von einem Message Broker oder auch direkt von einem SOAP-Webservice-Endpoint stammen.

Neben der großen Vielzahl an Schnittstellen besteht eine weitere Stärke von Camel (auch gegenüber anderen Integrationsframeworks) darin , dass die Routing-Konfiguration auf eine sehr einfache und leicht verstehbare Weise mit einer Fluent API in Java-Code erstellt werden kann.

Im Unterschied zu einem BPMS zählt die Geschäftsprozessorientierung aber nicht zum Selbstverständnis von Apache Camel. Dazu würde die Unterstützung von langläufigen Prozessen und das Recovery von Prozessen im Falle von Systemabstürzen/-fehlern gehören. Geschäftsprozesse werden zudem über grafische Modelle implementiert. Für Camel gibt es zwar auch verschiedene Möglichkeiten grafisch Routing-Regeln zu definieren; die Verwendung dieser grafischen Tools ist aber eher Geschmacksache. Die Java Fluent API ist für den gewünschten Einsatzzweck bereits hervorragend geeignet.

Durch diese unterschiedliche Zielsetzung ergeben BPMN und Apache Camel ein ideales Paar: Die Verwaltung der Prozessinstanzen wird vom BPMS übernommen, während aufwändige Datentransformationen in Apache Camel durchgeführt werden können.

Zielarchitektur

Um den Prozess von seiner Umgebung zu isolieren, sehen wir in unserer Architektur eine Schicht von Adaptern vor, durch die Daten von und zu der jeweiligen Prozessinstanz fließen. Dies erlaubt uns die Services aus Sicht des Prozesses in einer Version konstant zu halten und transparent zu externen Serviceversionen zu transformieren. Ebenso können wir Datenmengen filtern, so dass nur wirklich benutzte Daten den Prozess erreichen und dort persistiert werden.

Die prinzipielle Struktur ist in Abbildung 1 gezeigt: Für jeden von einem Prozessmodell angebotenen Service existieren potentiell Adapter, die die Services nach außen in den benötigten Versionen anbieten. Nach innen konvertieren diese Adapter die SOAP-Nachrichten in das Schema der jeweiligen Prozessinstanz. In diesem Schritt können auch größere Daten im Serviceaufruf, wie z.B. die oben genannten PDF-Dateien abgespalten und in einem separaten Datenbereich abgelegt werden.

Abbildung 1: Zielarchitektur mit Pufferschicht zur Kapselung des Prozesses

Die von einem Prozess aufgerufenen Services sollten eine Geschäftsfunktion präsentieren. Jeder Prozess kann potentiell einen Adapter benötigen um SOAP- Nachrichten zu konvertieren. Dies wird insbesondere dann notwendig, wenn sich einer der benutzten Backend-Services unabhängig vom eigenen Projekt ändern kann.

In diesem Adapter für das Backend können auch die, dem BPMS vorenthaltenen, Daten in die Nachrichten wieder eingebettet werden. So ist es möglich die großen Datenmengen, die lediglich durch die Prozesse geroutet werden würden, am BPMS vorbei zu transportieren, um dessen Persistenz zu entlasten. Der Speicherplatz der Adapterschicht kann dann automatisch oder manuell aufgeräumt werden, ohne die Protokolle der Prozessausführung zu beeinträchtigen.

Umsetzung mit Camel

Um die Umsetzung mit Apache Camel zu demonstrieren, haben wir ein Beispiel entwickelt, das die erwähnten Requirements erfüllt. Das komplette funktionierende Beispiel steht in github [1] zur Verfügung.

Zur besseren Verständlichkeit ist das Beispiel in zwei Teile aufgeteilt. Der erste Teil demonstriert die Schema-Konversion für SOAP-Service-Calls, die in das BPMS eingehen und vom BPMS ausgehen. Der zweite Teil zeigt das Vorbeischleusen von großen Dateien am BPMS.

Schritt 1: Schema-Transformation des Webservices

Für das Beispiel wurden zwei WSDLs erzeugt, die jeweils unterschiedliche, versionierte Schemas importieren. Um diese WSDLs nun als Endpoint in Camel anbieten zu können, bedient man sich des integrierten CXF-Frameworks. Dazu kann in der Kombination mit Spring die notwendige Konfiguration geschaffen werden (Listing 1).

<cxf:cxfEndpoint id="insuranceEndpoint-v1"
    address="http://localhost:9000/process1"
    wsdlURL="/wsdl/insuranceV1.wsdl">
    <cxf:properties>
        <entry key="dataFormat" value="MESSAGE" />
        <entry key="publishedEndpointUrl"
            value="http://localhost:9080/insuranceservice" />
    </cxf:properties>
</cxf:cxfEndpoint>
<cxf:cxfEndpoint id="insuranceEndpoint-v2"
    address="http://localhost:9000/process2"
    wsdlURL="/wsdl/insuranceV2.wsdl">
    <cxf:properties>
        <entry key="dataFormat" value="MESSAGE" />
        <entry key="publishedEndpointUrl"
            value="http://localhost:9080/insuranceservice" />
    </cxf:properties>
</cxf:cxfEndpoint>
Listing 1

Die Endpunkt-URI wird durch die Konfiguration des CXF-Endpoint-Properties “publishedEndpointUrl” mit der Adresse des Servlets überschrieben. Dieses Servlet dient im Beispiel als zentrale Anlaufstelle.

Diese Konfigurationen von Webservice-Endpoints können dann direkt in der Camel-Nachrichten-Konfiguration, oder auch kurz Route genannt, verwendet werden. Dazu benötigt man in der Camel-Route lediglich den Namen der Bean, der im Attribut “id” der Spring-Bean definiert wurde.

In Listing 2 ist die erste Konfiguration der Camel-Route zu sehen. Ein Route definiert einen Nachrichteneingang mit dem einleitenden Schlüsselwort “from” der Java-Fluent-API. Dies bestimmt, von welcher Schnittstelle die Nachrichten dieser Route abzuholen sind.

public class SchemaEvaluationRouter extends RouteBuilder {

    @Override
    public void configure() throws Exception {

        // ------------ Namespace-Definition ----------------
        Namespaces ns_v1 = new Namespaces("c1",
            "http://bpel.innoq.com/insurance/v1/types");
        Namespaces ns_v2 = new Namespaces("c2",
            "http://bpel.innoq.com/insurance/v2/types");

        from("jetty:http://0.0.0.0:9080/insuranceservice?minThreads=5")
            .process(new ContentPrinter())
            .process(new PrintHeaderProcessor())
            .choice()
            .when().xpath("//c1:CarInsuranceFindProcess", ns_v1)
                    .setHeader("schemaVersion", constant("1"))
                    .setHeader("targetVersion")
                    .xpath("//c1:targetVersion", String.class, ns_v1)
                    .to("seda:evaluateV1Scheme")
            .when().xpath("//c2:CarInsuranceFindProcess", ns_v2)
                    .setHeader("schemaVersion", constant("2"))
                    .setHeader("targetVersion")
                    .xpath("//c2:targetVersion", String.class, ns_v2)
                    .to("seda:evaluateV2Scheme")
            .otherwise()
                    .to("seda:fault");

        from("seda:evaluateV1Scheme")
            .choice()
            .when(header("targetVersion").isEqualTo("2"))
                .pipeline("direct:transformV1V2")
                .to("cxf:bean:insuranceEndpoint-v2")
            .otherwise()
                .to("cxf:bean:insuranceEndpoint-v1");
Listing 2

In den Schnittstellenpunkten in “from” und “to” werden die Endpunkte ebenso in URI-Form definiert. Hier wird durch die Definition eines Protokolls, Location und - wenn notwendig - Parameter bestimmt, wie eine Nachricht in die Apache-Camel-Route gelangt und diese wieder verlässt.

Eine Camel-Message Exchange besteht immer aus einem Body (Nutzdaten) und aus Meta-Informationen (header). Der Body mit den Daten wird aus den Objekten gebildet, mit welchen der jeweilige Endpunkt arbeitet.

Bei einem File-Endpoint wird ein File-Object in den Body gelegt. Bei einem CXF-Endpoint kann man das zu erzeugende Format konfigurieren: Message, Payload oder Pojo als Property angegeben, gibt den Body des Webservice- Request in unterschiedlichen Format in den Exchange.

Die Route in Listing 2 beinhaltet verschiedenste Verarbeitungsschritte, die mit einem Processor und “process” eingebunden sind. Diese Prozessoren dienen hier lediglich dem Logging, demonstrieren aber, wie man eigene Logik in die Verarbeitung einer Nachricht einbringen kann.

Das Ziel der Nachricht wird hier durch ein Content Based Routing - ein Routing entsprechend dem Nachrichteninhalt - mit dem Wort “choice” definiert. Das Routing der Nachricht entscheidet sich anhand des Vorhandensein des Tags “CarInsuranceFindProcess” im Body entweder im Schema V1 oder im Schema V2. Die dazu notwendige Analyse der XML-Nachricht erfolgt über eine einfache XPath-Abfrage.

Mit dem Schlüsselwort “to” wird dann das Ziel der Nachricht bestimmt. Hier ist es eine weitere Camel-Route, die über eine asynchrone In-Memory Queue gefüttert und über “seda:” definiert wird. Der Name nach dem “seda:” benennt diese Queue dann eindeutig. Während der Verarbeitung einer Nachricht wird vor der Weiterleitung an diese Queue noch Meta-Information als Nachrichten-Header mit setHeader gespeichert.

Diese Meta-Informationen stehen in allen weiteren Routen zur Verfügung und können damit z.B. als Verarbeitungsanweisungen in den folgenden Routen benutzt werden.

Dort wird dann die Schema-Transformation (V2->V1;V1->V2) vorgenommen, wenn diese notwendig ist. Danach wird die Nachrichtenverarbeitung durch die Weitergabe an den Webservice-Endpoint im BPMS abgeschlossen. Webservice- Endpoints werden in Camel sowohl für den Gebrauch als Consumer (Client) oder Provider (Server) auf dieselbe Art und Weise definiert. Dies ist entsprechend aus dem Beispiel in [1] nachvollziehbar.

Für die Feinheiten und weiteren Möglichkeiten von und mit Camel sei an dieser Stelle auf die Dokumentation im Web [2] verwiesen.

Schritt 2: Vorbeischleusen von großen Dateien am BPMS

Das Vorbeischleusen startet ebenso mit einem Endpoint, der sich wieder einen, in Spring konfigurierten, CXF-Endpoint zunutze macht.

Der CXF-Endpoint wird als Einsprungspunkt definiert um den Webservice des BPMS zu kapseln.

public class FileEntrePot extends RouteBuilder {

    @Override
    public void configure() throws Exception {

        Namespaces ns_external = new Namespaces("ext",
            "http://www.example.org/external/");

        from("cxf:bean:externalSendSignedDocument")
            .process(new ContentPrinter())
            .setHeader("processid")
            .xpath("//ext:sendSignedDocument/processId/text()",
                ns_external)
            .setHeader("CamelFileName",
                simple("Process_${in.header.processid}.pdf.txt"))
            .multicast()
            .to("seda:handlefile", "seda:forwardmessage")
            .bean(ExternalResponse.class, "createSSDResponse");  

        from("seda:handlefile")
            .transform()
            .xpath("//ext:sendSignedDocument/pdf/text()", ns_external)
            .to("file:target/output");

        from("seda:forwardmessage")
            .process(new ExternalToInternalConverter())
            .to("cxf:bean:internalSendSignedDocument");
Listing 3

So können Requests auf den BPMS abgefangen, die Datei extrahiert und durch eine ID oder den Dateipfad ersetzt werden. Der so umgeformte SOAP-Request wird mittels einer weiteren Camel-Route “seda:forwardmessage” an den Webservice-Endpoint des BPMS weitergeleitet. Die Datei selbst wird an die Route “seda:handlefile” geschickt.

Dies wird durch

.multicast().to("seda:handlefile", "seda:forwardmessage")

ausgelöst.

Dieser Abzweigungspunkt funktioniert in der gezeigten Standardkonfiguration wie eine Pipeline: Erst das “seda:handlefile” abarbeiten und danach das “seda:forwardmessage”. Beide Schritte arbeiten mit einer (flachen) Kopie der Nachricht. Die Route “forwardmessage” entfernt die Datei aus dem Message Exchange und gibt nur noch den Pfad zur Datei weiter. Je nachdem, in welcher Form die Datei abgelegt werden soll, kann auch ein beliebiger Key zum Zugriff auf eine Datenbank weitergegeben werden.

Im Processor ExternalToInternalConverter() wird mit den Daten lediglich das Objekt erzeugt, das per JAXB für den richtigen Message Content am internen BPMS-WebserviceEndpoint sorgt.

Das Herausnehmen der Datei war noch eine relativ leichte Übung. Das Zurückholen der Datei ist dagegen eine größere Herausforderung. Hierfür muss das Content Enricher Pattern angewendet werden.

Das bedeutet, dass während des Exchanges Daten - in unserem Fall eine Datei

Wir beschränken uns an dieser Stelle auf die Beschreibung der Camel-Route (Listing 4) und den relevanten Teil des Processors “ReAquireProcessor”, der in dieser Route verwendet wird (Listing 5).

from("cxf:bean:internalMakeApplication??
    headerFilterStrategy=#dropAllMessageHeadersStrategy")
.process(new ReAcquireProcessor())
.to("cxf:bean:externalMakeApplication");
Listing 4

Listing 4 zeigt, wie unser Processor in den Webservice-Aufruf eingebunden wird. Der Aufruf wird dann direkt an den externen Webservice-Endpoint, der auch als Spring Bean konfiguriert sein muss, weitergeleitet.

Der spannendere Teil findet in dem ReAquireProcessor statt und zwar in der privaten Methode pickUpFileFromEntrePot (Listing 5)

Diese Methode sorgt für das Anreichern des Contents, indem sie sich eines Camel-File-Endpoints bedient, um die Datei abzuholen. Durchgeführt wird dies mit einem ConsumerTemplate, das vom Camel-Framework zur Verfügung gestellt wird.

Das Template ermöglicht es, Objekte, z.B. eingelesene Dateien, von einem Camel-Endpoint zu holen und direkt im Java-Code zu verwenden.

Damit rufen wir auf einem bequemen Weg eine Datei ab und nutzen dafür die vorgefertigten Mechanismen von Camel, ohne dieses Feature manuell von Hand programmieren zu müssen.

Die Datei wird dann im Rahmen einer Schema-Konversion in den Body des Webservice-Aufrufs eingefügt. Vorher musste im Processor aus dem Body der Dateinamen ermittelt werden.

Die restliche Aufgabe des Processors ist nun, einen neuen Body mit dem Schema für den externen Webservice aufzubauen und die Datei dabei zu beinhalten.

Aus Platzgründen können nicht alle Teile des Beispiels wiedergegeben werden. Daher hier nochmal der Hinweis auf die Publikation des Codes im Internet [1]

public class ReAcquireProcessor implements Processor {

    ConsumerTemplate consumer;
    private static final Namespace ns_ext = Namespace.getNamespace("ext",
            "http://www.example.org/external/");
    private static final Namespace ns_int = Namespace.getNamespace("ext",
            "http://www.example.org/internal/");

    org.jdom.output.XMLOutputter out = new XMLOutputter();

    private static final String EXCEPTION_MESSAGE = "No Document Found!";

    @Override
    @SuppressWarnings({ "rawtypes" })
    public void process(Exchange exch) throws Exception {
        ...
        pickUpFileFromEntrePot("doc.pdf");
        ...

    }

    private String pickUpFileFromEntrePot(String text) throws SoapFault {
        String fileEndpoint = "file:target/output?delete=true&amp;
            initialDelay=0&amp;fileName=" + text;

        String filebytes = consumer.receiveBody(fileEndpoint, 5000,
                String.class);

        if (filebytes != null) {
            return filebytes;
        } else  {
            throw new SoapFault(EXCEPTION_MESSAGE,
                SoapFault.FAULT_CODE_CLIENT);
        }
    }
}
Listing 5

Camel vs. ESB

Oftmals wird in Büchern und Artikeln für die hier beschriebenen Probleme der Einsatz von einem Enterprise-Services-Bus-Produkt (ESB) empfohlen. Letztendlich ist Apache Camel eine Art „ESB light“: So bildet es auch die Grundlage von Apache Service Mix – einem kompletten ESB.

Es gibt aber auch einige Unterschiede: ESBs sind in der Regel komplexe Serverprodukte, die sich erst richtig lohnen, wenn sie zentral in einem Unternehmen eingesetzt werden. Dann können Spezialisten die Konfiguration, Wartung und Überwachung vornehmen. Damit wird aber die Kontrolle aus dem eigentlichen Entwicklungsprojekt in die Infrastruktur des Unternehmens verlagert. Dies kann für unternehmensweite SOA-Strukturen sinnvoll sein (z.B. um Location Transparency herzustellen, d.h. den konkreten Endpunkt eines Services vor den Konsumten zu verstecken), aber ist für private Transformationsregeln zu schwergewichtig, weil es die nötige Kommunikation für das Projekt erhöht und mehr Abteilungen in das Projekt einbindet. Über Apache Camel hat das Projekt die Chance, unter eigener Verantwortung leicht und einfach seine Transformationsregeln zu implementieren und in demselben Deployment-Zyklus wie die Anwendung produktiv zu schalten.

Fazit

Mit ausführbaren Geschäftsprozessen, die in BPMN modelliert sind, können einfach Integrationsanwendungen erstellt und zentral ausgeführt werden. Diese Integrationsaufgaben erfordern jedoch auch Lösungen, um sowohl Änderungen im Prozess von der Umwelt abzukapseln als auch Änderungen in der Umwelt vom Prozess transparent zu machen. Die dazu nötige Pufferschicht kann mit Apache Camel komfortabel erstellt werden und sichert so die langfristige Wartbarkeit und Einsatzfähigkeit der Prozesse. Hierbei ergänzen sich sehr gut die Stärken der verschiedenen Produkte: BPMS managen die Ausführung der Prozessmodelle, die Persistenz etc. und bieten dabei vielfältige Funktionen, diese Prozesse auch aus Business-Sicht auszuwerten. Apache Camel bietet dagegen einen leichtgewichtigen Ansatz, um transiente Nachrichtentransformationen durchzuführen. Dabei können auch weitere Aufgaben von Apache Camel wahrgenommen werden, wie z.B. das Logging der SOAP-Nachrichten in ein zentrales Protokoll, die Messung von diversen technischen Statistiken o.Ä.

Wir hoffen, Ihnen mit diesem Artikel einen guten Überblick über die Nachrichtentransformation mit Apache Camel und dessen sinnvolle Integration in Geschäftsprozessautomatisierungsprojekten aufgezeigt zu haben.

Referenzen:

  1. https://github.com/Waterback/BPELCam  ↩

  2. http://camel.apache.org  ↩