Von Mule nach Java und zurück

Keine Kommentare

Da Mule weitgehend aus Java besteht, überraschen die vielfältigen Kombinationsmöglichkeiten mit Java nicht. Einige bekannte – und weniger bekannte – Varianten stelle ich in den folgenden Abschnitten vor.

Eine Kleinigkeit aber vorweg: Einige werden sich nach der Einleitung die Frage stellen, welche Sprache sonst noch im Mule-Kern vorkommt: Es ist Scala, das in einigen der Enterprise-Komponenten (DataWeave, Batch) vorkommt. Wenn Mule läuft, befindet sich die Scala-Laufzeitbibiliothek also immer auch auf dem Classpath. Wer möchte, kann daher auch Scala statt Java verwenden. Ich beschäftige mich heute aber “nur” mit Java.

Expression Language

Mule erlaubt an beinahe allen Stellen in seinen Flows statt fester Werte sogenannte MEL-Expressions. MEL steht für “Mule Expression Language”. Eine MEL-Expression wird mit #[] eingerahmt. MEL sieht aus wie viele andere Skriptsprachen, sie ist stark an C- bzw. Java-Syntax angelehnt. Die genaue Beschreibung befindet sich bei Mule: MEL-Reference.
Wenn eine MEL-Expression in einem Flow ausgewertet wird, so liegen einige Objekte bereits im Scope, z.B. message für die Nachricht oder payload für die Nutzlast der Nachricht. Mule-Kenner wissen bereits: Die Payload ist immer ein Java-Objekt (genauer: eine Instanz eines Objektes). Bei POJOs kann mit Punkt-Notation auf die Attribute zugegriffen werden, im Hintergrund ruft der MEL-Interpreter die zugehörigen Getter auf. So könnte man aus einem String mit

<set-payload value="#[payload.bytes]" doc:name="getBytes()" />

den Inhalt als Byte-Array auslesen. (Das Beispiel ist konstruiert und gefährlich: Es gibt zwar eine Methode getBytes() in String, die ist jedoch kein echter “Getter”. Außerdem verlassen wir uns hier auf das Default-Encoding, was grundsätzlich keine gute Idee ist.) Anschließend hätten wir in der Mule-Message ein Byte-Array statt eines Strings. Mit

<logger level="INFO" message="Paylod type is: #[payload.getClass()]" doc:name="getClass()" />

ließe sich das auch direkt prüfen. Nebenbei habe ich gezeigt, wie Methoden aufgerufen werden.
Man kann natürlich auch Methoden mit Parametern aufrufen, um zum Beispiel alle x zum u zu machen:

<set-payload value="#[payload.replace('x', 'u')]" doc:name="'x' to 'u'"/>

Oder – wenn man die Welt verdrehen möchte – geht das mit Apache Commons 3 und einer statischen Methode aus StringUtils:

<set-payload value="#[org.apache.commons.lang3.StringUtils.reverse(payload)]" doc:name="reverse" />

Ich habe mich hier auf Klassen beschränkt, die bereits im Classpath vorhanden sind. Natürlich darf man auch eigene Klassen benutzen, die man im Verzeichnis src/main/java implementiert.

Java-Component

Die nächste Variante sind die sogenannten Java Components. Sie existieren in zwei Geschmacksrichtungen: entweder als einfache POJOs oder als Implementierung des Mule-Interfaces org.mule.api.lifecycle.Callable.
Fangen wir mit einem POJO an:

public class SimplePojo {
  public String xToU(String text) {
    return text.replace('x', 'u');
  }
}

Den Code können wir per XML-Schnipsel aufrufen:

<component class="de.codecentric.components.SimplePojo" />

Wenn in der Klasse nur eine Public-Methode mit einem Parameter existiert, so ruft Mule diese auf. Existieren mehrere Methoden, wird es komplizierter: Dann kommen sogenannte Entry-Point-Resolver ins Spiel. Wer es genauer wissen möchte: Unter Configuring-Components in der Mule-Doku stehen die Details.
POJOs bieten die üblichen Vorteile: Sie sind extrem lose an Mule gekoppelt (genau genommen überhaupt nicht) und sie lassen sich in JUnit-Tests ohne Abhängigkeiten zu irgendwelchem Mule-Code testen. Da die Mule-XML-Konfiguration nur eine Variante einer Spring-Konfiguration darstellt, können Components übrigens auch als Spring-Beans instanziiert werden, mit den bekannten Möglichkeiten von Spring: Factories, Scopes, etc. Standardmäßig sind die Components Singletons (und müssen daher auch thread-safe implementiert sein).
Die zweite Variante der Components implementieren das Mule-Interface Callable. Über diesen Weg kommt man auch an einige Innereien von Mule heran:

public class SimpleCallable implements Callable {
  @Override
  public Object onCall(MuleEventContext eventCtx) throws Exception {
    return ((String)eventCtx.getMessage().getPayload()).replace('u', 'x');
  }
}

Wie man sich denken kann, wird aus dem Flow onCall() aufgerufen. Darin hat man Zugriff auf den MuleEventContext, die MuleMessage und den MuleContext. Über die MuleMessage lassen sich nicht nur die Payload auslesen und bearbeiten, sondern auch die Properties in den verschiedenen Mule-Scopes. Der Nachteil besteht darin, dass Tests schwieriger sind, da man sich erst einen MuleEventContext bauen muss.
Braucht man nur den MuleContext und möchte bei den “einfachen” Methoden bleiben, kann man auch – in einem POJO – MuleContextAware implementieren. Nachdem eine Instanz der Component erstellt wurde, ruft Mule die Methode setMuleContext(MuleContext ctx) auf, so dass man sich den Context in einer Member-Variablen merken kann.

Java-Transformer

Einen Java-Transformer ruft man aus der XML-Konfiguration ähnlich wie eine Component auf:

<custom-transformer class="de.codecentric.SimpleTransformer" doc:name="Java"/>

Hier haben wir wieder eine Kopplung an Mule, die Klasse muss das Interface org.mule.api.transformer.MessageTransformer implementieren. Die Implementierung muss man nicht komplett selbst erledigen. Es gibt zwei vorgefertigte abstrakte Klassen, bei denen jeweils nur eine Methode zu implementieren ist. Beginnen wir mit AbstractTransformer:

public class SimpleTransformer extends AbstractTransformer {
  @Override
  protected Object doTransform(Object src, String enc) throws TransformerException {
    return src;
  }
}

Hier bekommen wir die Payload und das gewünschte Encoding geliefert. Als Ergebnis müssen wir die neue Payload zurückgeben. (Im Beispiel gebe ich einfach das Original zurück.) Wieder haben wir eine Kopplung an Mule (wir leiten ab), können aber noch einfach testen, da der Transformer für den Test einfach instanziiert werden kann. Vom Encoding abgesehen ist der Unterschied zu der ersten Component-Variante (POJO) gering. Ich benutze diese Variante in der Praxis daher so gut wie nie, da sie gegenüber dem POJO keine Vorteile bietet.
Etwas anders sieht es beim AbstractMessageTransformer aus:

public class SimpleMessageTransformer extends AbstractMessageTransformer {
  @Override
  public Object transformMessage(MuleMessage message, String output) throws TransformerException {
    return message;
  }
}

Analog zum AbstractTransformer ist ebenfalls nur eine Methode zu implementieren, die Signatur bietet jedoch mehr Flexibilität: Statt der Payload übergibt Mule die MuleMessage als Parameter. Somit besteht auch Zugriff auf die Properties in den verschiedenen Scopes. Der Rückgabewert kann eine (transformierte) Payload oder eine Message (neu oder verändertes Original) sein.
Diese Variante ist immer die Wahl, wenn man mit der Payload und/oder anderen Bestandteilen der MuleMessage (Properties, Attachments) arbeitet.

Filter

Alle bisher genannten Varianten besitzen einen Nachteil: Sie spielen nicht gut mit DataSense im AnypointStudio zusammen. In vielen Fällen kann AnypointStudio herleiten, was für ein Typ in einer Nachricht steht und dies über den Flow hinweg verfolgen. Bei Java-Klassen hat das AnypointStudio jedoch keine Chance: Es kann nicht wissen, was für ein Datentyp aus einer Komponente herauskommt. Möchte man in der Java-Komponente die Payload überhaupt nicht beeinflussen, z.B. weil man nur einen Seiteneffekt auslösen möchte oder nur eine Property/Attachment der Nachricht ändern möchte, so ist es schade, dass die Typinformation verloren geht. Ein (zugegebenermaßen dreckiger) Trick besteht darin, einen Filter zu benutzen. So sieht das XML aus:

<custom-filter class="de.codecentric.components.SimpleCustomFilter" doc:name="SimpleCustomFilter"/>

Und so der zugehörige Java-Code:

public class SimpleCustomFilter implements Filter {
  @Override
  public boolean accept(MuleMessage message) {
    // irgendwas mit der Message machen...
    return true;
  }
}

Beim Filter geht das AnypointStudio davon aus, dass die Payload unverändert bleibt. Was daran unschön ist: Im Flow steht ein Filter, der nicht filtert, sondern etwas anderes macht. Man stiftet also Verwirrung. Ich würde diesen Hack – wenn überhaupt – nur sehr sparsam einsetzen, z.B. in Utility-Subflows.
Man kann den Filter natürlich auch bestimmungsgemäß nutzen: Gibt accept() false zurück, so bricht der Flow an der Stelle des Filters ab. In asynchronen Flows ist dann einfach die Bearbeitung beendet, in synchronen Flows geht die zu diesem Zeitpunkt in der Nachricht enthaltene Payload an den Aufrufer zurück. In den meisten Fällen ist das nicht sinnvoll, der Einsatz von Filtern ergibt eher in asynchronen Flows Sinn.

Custom-Processor

Wer noch tiefer in die Innereien von Mule einsteigen möchte, muss org.mule.api.processor.MessageProcessor implementieren und kann ihn folgendermaßen in XML konfigurieren/aufrufen:

<custom-processor class="de.codecentric.FlowCaller" doc:name="FlowCaller">	 	 
 <spring:property name="calledFlow" value="some-flow-name" />	 	<pre lang="xml"> 
</custom-processor>

Ich habe hier ein Spring-Feature genutzt: Properties mit Getter/Setter lassen sich mittels spring:property mit Werten versorgen.
Im Interface MessageProcessor befindet sich die Methode mit folgender Signatur:

public MuleEvent process(MuleEvent event) throws MuleException

Wir haben hier keine MuleMessage oder keinen MuleEventContext in der Hand, sondern ein MuleEvent. Brauchen wir das? In den meisten Fällen nicht, aber ohne Beispiel würde ich davon sicher nicht schreiben. Es ist notwendig, wenn es aus der Java-Welt zurück in die Mule-Welt gehen soll:

Mule-Flow aus Java heraus aufrufen

Wollen wir einen Flow aus Java aufrufen, benötigen wir das MuleEvent (bekommen wir im MessageProcessor) und den MuleContext (bekommen wir durch Implementierung von MuleContextAware, siehe oben). Vom MuleContext führt der Weg über die MuleRegistry zum Flow. Der lässt sich dann einfach über process() ausführen:

public MuleEvent process(MuleEvent event) throws MuleException {
  Flow flow = (Flow) context.getRegistry().lookupFlowConstruct(calledFlow);
  MuleEvent result = flow.process(event);
  return result;
}

Wozu soll das gut sein? Zugegeben, die Einsatzfälle sind selten, kommen aber vor: So bietet Mule zwar ein for-each, um über eine Collection zu iterieren, aber eine while-Schleife fehlt. Das geht in Java recht einfach, den Schleifenrumpf kann man dann wieder als Mule-Flow realisieren und die vielfältigen Möglichkeiten von Mule zur Kommunikation mit dem Rest der Welt nutzen.
Ebenso könnte man selbst ein dynamisches Dispatching auf verschiedene Flows durchführen, so wie Mule es im API-Kit-Router macht.

Fazit

Mule bietet mit seiner grafischen Darstellung der Flows (dahinter steckt eine Spring-XML-Konfigurationsdatei) zwar eine recht komfortable Umgebung für viele Integrationsaufgaben an, oft lohnt jedoch der Rückgriff auf das gute, alte Java. Mit DataWeave in der Enterprise-Edition kann man zwar oft auf Java verzichten, aber auch dort existieren Grenzen oder es wird mit der Kombination XML/DataWeave einfach unübersichtlich.
Für die verschiedenen Anforderungen (Kopplung) existieren jedoch mehrere maßgeschneiderte Wege. Aus den Beispielen sollte ersichtlich geworden sein, dass man dort ohne Boilerplate-Code schnell zum Ziel kommt. Schließlich braucht man neben den fachlichen Diskussionen in Integrationsprojekten nicht auch noch technischen Stress.

Roger Butenuth

Dr. Roger Butenuth hat in Karlsruhe Informatik studiert und anschließend in Paderborn promoviert (Kommunikation in Parallelrechnern). Er hat langjährige Erfahrung in der Projekt- und Produktentwicklung.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.