Einfache und Schnelle Webservices mit Mule ESB und Apache CXF

4 Kommentare

Ich möchte Euch in diesem Beitrag zeigen, wie wir bei codecentric in unseren Projekten den Mule ESB und Apache CXF einsetzen. Ich möchte zeigen wie einfach das Erstellen von Webservices ist und was man tun kann um das doch recht langsame Standardverhalten zu verbessern.

Sollte man überhaupt einen Webservice einsetzen? Nun eine gute Frage, und wahrscheinlich sogar am relevantesten für die Performance. Webservices eignen sich hervorragend um Interfaces und Services nach außen anzubieten, oder wenn man intern auf Grund von Firewallbeschränkungen oder anderen Programmiersprachen andere Protokolle wie RMI nicht verwenden kann. Da Ihr euch mit Webservices herumschlagt, gehe ich einfach mal davon aus daß dieser Punkt nicht zur Debatte steht.

Wir benutzen den Mule Enterprise Service Bus in einigen Projekten, aber damit möchte ich nicht sagen, daß er für alle geeignet ist. Dokumentation gibt es nur nach Anmeldung und die Releasepolitik ist durchaus fragwürdig. Ich bin nicht sonderlich glücklich damit, aber er funktioniert doch ganz gut sobald mal ihn einigermaßen begriffen hat und an einigen Stellen nachgebessert hat. Um Webservices zu erzeugen kann man diese entweder von Hand schreiben, oder Apache Axis oder Apache CXF verwenden. Ich bevorzuge CXF, da ich dessen API eleganter empfinde, und der generierte Code sauberer ist. Außerdem ist das Projekt noch lebendig und wird sogar von Mule Entwicklern betreut. CXF ist Standard bei Mule und wird von uns in Verbindung mit Spring Pojo Components angewendet.

Nun schauen wir uns mal unser Beispiel Webservice Interface an. Um die Sache etwas interessanter zu gestalten nutzen wir einen nicht trivialen Service der Domänenobjekte als Parameter erwartet und zurückliefert. Ein Wort der Warnung vorweg: Große Objektbäume erzeugen viel XML 🙂

@WebService
public interface RecommendationService {
	@WebMethod
	public Products recommendProducts(
		@WebParam(name="user")
		User user,
		@WebParam(name="genre")
		Genre genre
	);
}

Natürlich gibt es auch eine Implementierung für diesen Service, welcher im nächsten Schritt in Mule eingebunden wird.
Zu aller erst müssen wir Mule dazu bewegen überhaupt Webservices zu akzeptieren. Da wir Mule in der Regel im WAR deployen benutzen wir den Servlet Connector, allerdings kann auch der Jetty Connector beim eigenständigen Einsatz genutzt werden:

<servlet:connector name="servletConnector" 
                   servletUrl="http://localhost:8080/mule/services/recommendation?wsdl" />

Als Nächstes folgt die Webservicekonfiguration des Services:

<model name="recommendationServiceModel">
	<service name="recommendation">
		<inbound>
			<cxf:inbound-endpoint address="servlet://recommendation" synchronous="true" />
		</inbound>
		<component>
			<spring-object bean="RecommendationService" />
		</component>
	</service>
</model>

Und natürlich auch der Service selbst:

<spring:bean id="RecommendationService"
             class="de.codecentric.RecommendationServiceImpl" />

Man kann die Konfigurationen in eine Datei quetschen, allerdings empfehle ich diese aufzuteilen. Entweder nach Typ (service, component, mule) oder eine Konfiguration pro Service.
Das war es auch schon mit der gesamten Mule Konfiguration.
Nun könnten wir den Service schon testen, wenn wir einen einfachen Weg hätten die Parameter zu übergeben. Bis dahin begnügen wir uns mit dem Aufruf der WSDL.

http://localhost:8080/mule/services/recommendation?wsdl

Beachtet auch eventuelle s welche auf einen ausgelagerten Teil der WSDL verweisen.

Einen Java Client für den Zugriff auf den Service zu erstellen ist ganz einfach mit Hilfe des wsdl2java Kommandos von CXF:

wsdl2java -client -d src/main/java -p de.codecentric.client 
  http://localhost:8080/mule/services/recommendation?wsdl

Wären wir nun ein externer Benutzer, so könnten und müssten wir mit den generierten Klassen arbeiten. Allerdings möchten wir insbesondere bei inkrementeller Entwicklung und den damit verbundenen häufigeren Änderungen an den Domänenobjekten diese nicht immer neu generieren. Außerdem bieten Domänenobjekte sinnvolle Businessmethoden welche nicht generiert werden, da sie nicht Teil des Webservices sind. Da CXF echt clever ist, können wir einfach folgende generierten Klassen löschen:

  • Genre
  • ObjectFactory
  • package-info
  • Products
  • User

Nun lassen sich die fehlenden Imports einfach durch die Domänenobjekte ersetzen. Außerdem muss die Referenz auf @XmlSeeAlso({ObjectFactory.class}) entfernt werden.

Danach sollten nur noch Interface und Implementierung des Services, sowie zwei Wrapper Objekte für Request und Response übrig bleiben. Führt man nun den Dummy Client mit CXF auf dem Classpath aus, so sollte der Webservice aufgerufen werden.

Intern löst der Aufruf von

RecommendationServiceImplService ss = new RecommendationServiceImplService(wsdlURL, SERVICE_NAME);
RecommendationService port = ss.getRecommendationServiceImplPort();

die Erstellung eines dynamischen Proxys mittels Reflection von der WSDL des Servers aus.

Man könnte jetzt aufhören. Wir haben erfolgreich einen Client erstellt der Domänenobjekte verwendet. Aber die Performance ist echt nicht gut.

Die WSDL wird vom Server bezogen und in eine Proxy Klasse übersetzt. Natürlich könnte man die WSDL importieren, doch dies müsste jedes mal gemacht werden wenn sich die Objekte verändern. Externe Benutzer müssen das tun, sind aber seltener Änderungen unterworfen. Trotzdem werden bei jedem Aufruf weiterhin Proxy Klassen generiert. Und das ist noch langsam genug. Ich habe die Laufzeit des gesamten Stacks gemessen und die Proxy Erzeugung übertrifft den Rest um Längen.

Was also tun? Um die Situation zu verbessern benutzen wir einen pool, mit Hilfe von commons pool GenericObjectPool.

private final GenericObjectPool recommendationServicePool;
 
RecommendationServiceFactory recommendationServiceFactory = new RecommendationServiceFactory();
recommendationServicePool = new GenericObjectPool(recommendationServiceFactory, new Config());

Der Pool benötigt also eine Factory um Instanzen zu erzeugen und eine Konfiguration. Die Konfiguration kann noch angepasst werden, aber die Defaults sind ausreichend gut. Die Implementierung der Factory sieht so aus:

public class RecommendationServiceFactory implements PoolableObjectFactory  {
public Object makeObject() throws Exception {
  RecommendationServiceImplService service = new RecommendationServiceImplService();
  RecommendationService port = service.getRecommendationServiceImplPort();
  return port;
}
public boolean validateObject(Object arg0) {
  // consider all controllers valid objects
  return true;
}
public void destroyObject(Object arg0) throws Exception {}
public void activateObject(Object arg0) throws Exception {}
public void passivateObject(Object arg0) throws Exception {}
}

Nun können wir unseren Service wie folgt aufrufen:

RecommendationService port = (RecommendationService) recommendationServicePool.borrowObject();
try {
  Products products = port.recommendProducts(user, genre);
} finally {
  recommendationServicePool.returnObject(port);
}

Auf keinen Fall sollte man vergessen geliehene Objekte wieder in den Pool zurückzugeben. Anders als in einer Bücherei bekommt man keine Mahnung 🙂

Zusammenfassung
Wir benutzen Mule ESB um Spring Komponenten basierte Webservices, welche Domänenobjekte verwenden, zu konfigurieren und zu deployen. Die Webservices werden mit Hilfe von Apace CXF bereitgestellt und in einem damit generiertem Client angesprochen. Durch die Benutzung eines Pools verhindern wir die unnötige Erzeugung von Proxyklassen.

Ich erwähnte bereits, daß der Pool eine erhebliche Verbesserung brachte. Dennoch sollte man nie einem Blogbeitrag trauen und immer selber messen. Dies ist sogar ganz einfach möglich indem man die Zeit des Aufrufs ohne Pool und des mehrfachen Aufrufs mit pool misst. Der erste Aufruf im Pool ist auch langsam, da das Objekt erzeugt wird. Auf meiner Maschine hat der ganze Stack 360ms benötigt. Alle folgenden Aufrufe benötigten 4-6ms. Das ist fast eine Verbesserung um den Faktor 100. Für die komplexen Aufgaben die der ESB ausführt sind 4ms sogar recht schnell. Muss doch ein HTTP request gemacht und innerhalb eines WARs im JBoss empfangen werden, die Daten ein und wieder ausgepackt werden, herausgefunden werden welcher Service mit welcher Methode aufgerufen werden soll etc.

Auf jeden Fall sollte man nicht zu vorschnell Webservices verurteilen. Mit den richtigen Hilfsmittel kann man sie relativ gut benutzen, und sie können auch recht schnell sein.

Fabian Lange ist Lead Agent Engineer bei Instana und bei der codecentric als Performance Geek bekannt. Er baut leidenschaftlich gerne schnelle Software und hilft anderen dabei, das Gleiche zu tun.
Er kennt die Java Virtual Machine bis in die letzte Ecke und beherrscht verschiedenste Tools, die JVM, den JIT oder den GC zu verstehen.
Er ist beliebter Vortragender auf zahlreichen Konferenzen und wurde unter anderem mit dem JavaOne Rockstar Award ausgezeichnet.

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

Kommentare

  • Atul Kash

    Commercial ESB products can often be quite expensive, particularly when adapters are sold separately at very high prices, that’s why I prefer Mule ESB which is an open source alternative. Great Article!

  • Andrew Perepeplytsya

    Hi Fabian,

    Regarding the release policy, it’s pretty simple: quarterly bugfix releases for subscribers, with hotfixes and patches on-demand for individual customers if they request it.

    The CE version doesn’t have a strict schedule, milestones and major releases are pushed out ‚when it’s ready‘.

    HTH,
    Andrew

  • dragosage

    11. Januar 2011 von dragosage

    Unsere Erfahrung bisher: die Performance ist nahe an Mangelhaft. Wir haben die Anforderung, ca. 100-400 XML Messages die Sekunde zu übermitteln und im Moment liegen wir bei 6 🙁

    Gerade stellen wir uns die Frage, ob ESB die richtige Entscheidung ist…

    Ciao DA

  • jörn

    Hi,

    kannst du freundlicherweise ein sample-projekt zur verfügung stellen?

    besten dank!
    jörn

Kommentieren

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