Aus Linux-Magazin 02/2001

Java Message Service

Abb. 1: Erzeugen von QueueSender und QueueReceiver.

JMS (Java Message Service) ist ein standardisiertes AP-Interface für Message Oriented Middleware (MOM). Damit sind Client-Server-Anwendungen schnell erstellt, ohne dass der Entwickler die Kommunikationsschicht selbst implementieren muss.

Mit MOM-Entwicklungen lässt sich im Enterprise-Markt gegenwärtig recht viel Geld verdienen. Und wie das so üblich ist, kocht jeder Hersteller sein eigenes Süppchen. Bei Java ist natürlich alles anders. Hier gibt es mit JMS (Java Message Service) ein standardisiertes API, das einfach zu verwenden ist. Wie, das zeigen die folgenden Zeilen anhand eines einfachen Beispiels.

Warum nicht selber machen?

Eine Client-Server-Anwendung mit Java schreiben ist eine einfache Angelegenheit. Mit den beiden Klassen java.net.ServerSocket und java.net .Socket ist es kein Problem, eine TCP/IP-Verbindung zwischen zwei Programmen aufzubauen und zu nutzen. Beispiele dazu sind in jedem besseren Lehrbuch zu Java zu finden (zum Beispiel in [1], dass sich speziell mit der Netzwerk-Programmierung unter Java beschäftigt).

Steht die Verbindung, wird bei sehr vielen Client-Server-Anwendungen das gleiche Design-Pattern verwendet: Der Client schickt Daten, deren Bedeutung der Server interpretieren kann, woraufhin der Server Daten an den Client zurückschickt. Klassische Beispiele sind FTP und HTTP.

Fasst man die Daten als Nachrichten (Messages) auf, die hin- und wieder zurückgeschickt werden, ist man beim Begriff der Nachrichten-basierten Client-Server-Anwendung gelandet. Dabei ist der beschriebene Request-Response-Mechanismus nur eine von vielen Möglichkeiten. Es liegt allerdings nahe, für ein Design-Pattern eine einheitliche Lösung einzusetzen.

Hier schlägt die Stunde der so genannten Middleware, die eine Lösung für den Nachrichtentransport auf generische Art bietet. Da aber jedes zusätzliche Stück Software ausgesucht, getestet, installiert und konfiguriert werden muss, sollte sich der Aufwand auch lohnen.

Bei eigenen Programmen kann man es sich sehr einfach machen und nur minimale Informationen (etwa Integer-Werte) über die Leitung schicken, um beispielsweise Netzwerk-Bandbreite zu sparen. Trotzdem ist ein gewisser Aufwand nicht zu umgehen: Die zu übertragenden Informationen müssen in den Socket geschrieben, eventuelle Exceptions abgefangen und die Antworten des Servers interpretiert werden. Das Design effizienter Anwendungen wird zusätzlich komplizierter, wenn man nur bestimmte Ports verwenden darf oder optimale Routen wählen muss, um die Last zu verteilen.

Ein weiteres wichtiges Argument gegen eine eigene Implementation der Kommunikationsschicht ist auch das Problem von asynchronen Nachrichten. Diese sind zwar nicht für jede Anwendung, aber oft genug sinnvoll. Man denke an eine Anwendung, die auf einem mobilen Gerät läuft. Sie produziert Nachrichten, beispielsweise Buchungssätze, die abends über eine Wählverbindung an einen zentralen Server übertragen werden sollen. Wenn dabei die Middleware das Puffern übernimmt, dann erspart man sich die Implementation bei der Anwendung selbst.

Als weiteres Argument für eine unabhängige Kommunikationsschicht ist die Kommunikation von einem Sender zu vielen Empfängern zu nennen (wie beispielsweise für Newsgroups). Das geht zwar auch über Multicast-Sockets, doch ist bei denen die Anwendungs- und nicht die Transportschicht für den fehlerfreien Datenaustausch verantwortlich. Auch für dieses Problem bietet die richtige Middleware eine geeignete generische Lösung.

Architektur des JMS-API

Nach der hoffentlich motivierenden Einleitung zum Thema Middleware nun ein Blick auf das JMS-API, das in der Version 1.0.2 vorliegt. Erhältlich ist es über [2], das gezippte File ist etwa 300 KByte groß. Zusätzlich ist noch ein Paket mit Beispielcode verfügbar.

Das JMS-API ist flach. Es besteht nur aus dem Package javax.jms und enthält neben zwei Hilfsklassen, einer Reihe von Exceptions (mit Basis javax.jms.JMSException) nur noch Interfaces. Diese Interfaces, von denen einige optional sind, müssen von JMS-Providern implementiert werden.

Der JMS definiert zwei Kommunikationsmodelle: Point-to-Point (kurz PTP) und Publish/Subsribe (kurz Pub/Sub). PTP-Kommunikation erfolgt zwischen einem Sender und einem Receiver mittels einer Queue, die Pub/Sub-Kommunikation zwischen einem Publisher und einem oder mehreren Subscribern. Hier heißt die Abstraktion Topic. Im angeführten Beispiel einer Newsgroup ist der Name der Newsgroup mit dem Topic gleichzusetzen.

Welches Kommunikationsmodell verwendet wird, hängt von der Anwendung beziehungsweise dem gewählten Design ab. Bei einer Logistik-Firma mit mehreren Fahrradkurieren könnten beispielsweise die Java-fähigen Fahrräder über eine Queue regelmäßig ihre Position an die Zentrale melden. Dort wird dann anhand aller vorhandenen Informationen ein neuer Auftrag optimal einem Kurier zugeteilt. Bei diesem Design muss man in der Zentrale sehr viel Logik implementieren. Alternativ dazu könnte ein neuer Auftrag über ein Topic veröffentlicht und von allen (freien) Kurieren gelesen und angenommen werden. Dieses Design verzichtet auf die aufwändige Implementation der Logik, führt aber eventuell zu einer weniger optimalen Ressourcenauslastung.

Im JMS finden sich für jede der beschriebenen Abstraktionen entsprechende Interfaces. Diese sind sehr allgemein gehalten, so definiert das javax.jms.Topic-Interface nur die beiden Methoden getTopicName() und toString(). Das erlaubt dem JMS-Provider eine große Freiheit beim Implementieren und den existierenden MOM-Anbietern eine einfache Möglichkeit, JMS-Schnittstellen zur Verfügung zu stellen.

Abb. 1: Erzeugen von QueueSender und QueueReceiver.

Abb. 1: Erzeugen von QueueSender und QueueReceiver.

Die Sessions

Zum Verständnis der ganzen JMS-Architektur ist eine weitere Abstraktion von zentraler Bedeutung: die Session. Diese ist sozusagen die Klammer für die gesamte Kommunikation. Die Abbildung 1 zeigt, wie im PTP-Modell eine Anwendung zu einer QueueSession kommt (für das Pub/Sub-Modell sieht das ganz analog aus). Über eine QueueConnectionFactory erzeugt die Anwendung eine QueueConnection und daraus wieder eine QueueSession. Diese Klasse enthält eine Reihe von Create-Methoden, um QueueSender-, QueueReceiver- und Message-Objekte zu erzeugen. Die Create-Methoden für die Sender- und Receiver-Objekte brauchen als Argument jeweils dasselbe Queue-Objekt (dadurch wird die Verbindung hergestellt).

Eine laufende JMS-Implementation muss nun zumindest ein QueueConnectionFactory-Objekt und die definierten Queue-Objekte zur Verfügung stellen. Anwendungen suchen diese Objekte typischerweise über das Java Naming and Directory API (JNDI), das im letzten Coffee-Shop vorgestellt wurde. Dabei kann eine JMS-Implementation einen eigenen JNDI-Serviceprovider stellen oder andere Provider voraussetzen. Das kann beispielsweise ein vorhandener LDAP-Server sein.

Aufbau der Messages

Neben den Queues und Topics für die Kommunikationsmodelle spielen die definierten Messages eine zentrale Rolle innerhalb des JMS. Grundsätzlich sind bei jeder MOM die Messages zweigeteilt. Ein administrativer Message-Header enthält Informationen zur Identifikation und zum Routing, der Message-Body enthält die applikationsspezifischen Daten. Innerhalb dieses sehr allgemeinen Modells gibt es von Produkt zu Produkt große Unterschiede, die natürlich vom JMS nicht in ihrer Gesamtheit zu modellieren sind.

Das JMS-Modell konzentriert sich darauf, ein vereinheitlichtes API zu bieten, mit dem auch Messages von existierenden Nicht-JMS-MOMs zu erzeugen sind. Insbesondere soll das API ermöglichen, in Betriebssystem, Architektur und Programmiersprache heterogene Applikationen zu unterstützen. Innerhalb von Java sollen auch Java-Objekte als Inhalte von Messages einsetzbar sein.

Um diese Ziele zu erreichen, besteht eine JMS-Message aus drei Teilen: dem Header, den Properties und dem Body. Der Unterschied zwischen Properties und dem Body ist fließend. Doch sollten Properties als Erweiterung des Headers aufgefasst und verwendet werden und nicht die Applikationsdaten transportieren. Die Properties sind das Mittel, mit dem JMS-Messages auf Applikations- und Providerbedürfnisse angepasst werden können.

Properties, deren Namen mit JMSX beginnen, sind innerhalb des JMS als Standard-Properties reserviert. Hier sind zwei Properties vordefiniert ( JMSXGroupID und JMSGroupSeq), die eine Gruppierung von Messages erlauben. Properties, deren Namen mit JMS _ beginnen, sind Provider-spezifisch. Diese erlauben die Anpassung des eigenen Systems an vorhandene Gegebenheiten und die Bereitstellung eigener Funktionalitäten.

Über Properties können JMS-Clients mit Hilfe so genannter Message Selectors Nachrichten auswählen. Die Syntax dieser Selektoren ist ein Subset von SQL92. Da Selektoren nicht auf den Message-Body zugreifen können, müssen eventuell Inhalte des Bodys in den Properties wiederholt werden.

Vom Interface javax.jms.Message werden fünf Subinterfaces abgeleitet, die die verschiedenen Message-Typen definieren: ByteMessage, MapMessage, ObjectMessage, StreamMessage und TextMessage. Diese Objekte werden über entsprechende Create-Methoden der Queue- und TopicSession-Objekte erzeugt.

Wahl des JMS

Bevor ein konkretes Beispiel an die Reihe kommt, das die einzelnen Teile zusammensetzt, müssen wir allerdings noch einen JMS-Provider installieren. Hier hat man die Qual der Wahl. Von der JMS-Homepage gelangt man über den Vendor-Link zu einer Liste von Anbietern. Neben kommerziellen sind auch einige Open-Source-Projekte aufgeführt. Einige Links ([3] bis [6]) finden Sie im “Infos”-Kasten.

Der Funktionsumfang der Implementationen unterscheidet sich beträchtlich, besonders die optionalen Teile der Spezifikation. Weiterhin ist zu bedenken, dass viele JMS-Implementationen Teile eines Enterprise-Java- Beans-Servers sind. So findet sich etwa SpiderMQ im bekannten JBoss-Server wieder, Joram wird in Jonas (und damit auch in Enhydra Enterprise) verwendet und auch OpenJMS ist Teil eines noch in der Entwicklung stehenden EJB-Servers.

Ist die eigene Anwendung also Teil eines größeren Projekts, ist die Entscheidung praktisch schon gefallen. Implementationen, die sich nahe am Standard orientieren, sollten allerdings einen Austausch der JMS-Implementation – zumindest theoretisch – ermöglichen. Im Folgenden werden die OpenJMS-Implementation sowie die Lösung von SwiftMQ vorgestellt – eine Wertung ist damit nicht verbunden.

Abb. 2: Konsole OpenJMS Administration.

Abb. 2: Konsole OpenJMS Administration.

OpenJMS

Bei OpenJMS liegt momentan die Version 0.5 vor. Der Download der Source-Version umfasst 3,6 MByte. Nach dem Entpacken des Tarballs – Vorsicht, er erzeugt kein eigenes Unterverzeichnis – muss man mittels build.sh all alle JAR-Dateien erzeugen. Die dafür notwendigen Dateien ( jms.jar, xerces.jar und viele mehr) werden zum Glück mitgeliefert, so dass es keine Probleme geben sollte. Anschließend ist im Unterverzeichnis test noch die Datei startjms.sh ausführbar zu machen und in allen Konfigurationsdateien (etwa valid_ jms.xml) der Eintrag startjms.bat durch startjms.sh zu ersetzen.

Nach diesen Vorarbeiten lässt sich mit der Admin-Konsole (einer Swing-Applikation) der JMS-Server starten. Der Aufruf

admin.sh -config valid_jms.xml &

startet die Konsole, dort wird über das Actions-Menü der Server gestartet. Danach muss man ein Connect durchführen. Über Popup-Menüs erstellt man die notwendigen Queues und Topics, (siehe Abbildung 2).

Der OpenJMS-Server hat einen eingebauten JNDI-Server, der es über eine analoge Swing-Anwendung erlaubt, hierarchische Namensräume für Queues und Topics aufzubauen. Leider gibt es noch keine entsprechenden Kommandozeilen-Utilities, die für manche Zwecke sehr nützlich wären. Die mitgelieferten Admin- und Start-Skripte müssen für eine produktive Installation noch angepasst werden, denn wichtige Konfigurationsdateien haben in einem Unterverzeichnis test nichts zu suchen.

Abb. 4: Überwachung einer Queue.

Abb. 4: Überwachung einer Queue.

Abb. 3: Zugriff auf SwiftMQ-Objekte über den Navigator.

Abb. 3: Zugriff auf SwiftMQ-Objekte über den Navigator.

SwiftMQ

Das Produkt SwiftMQ der Bremer Firma IIT wird nur im Binärcode vertrieben, darf allerdings auch kommerziell kostenlos eingesetzt werden. Die Distribution (aktuell ist 1.2) umfasst 3,1 MByte, zusätzlich sind noch zwei Extensions (Bridge und Mail) verfügbar, die aber vergleichsweise klein sind (128 KByte und 40 KByte). Der Download ist nach Angabe einer E-Mail-Adresse möglich, an diese bekommt man eine Mail mit temporär gültigen Links geschickt.

Die Architektur von SwiftMQ ist modular. Sie besteht aus so genannten Swiftlets, die zusammen die Funktionalität implementieren. Auf diese Weise sind auch Erweiterungen leicht zu bewerkstelligen. Das oben angesprochene Bridge-Swiftlet zum Beispiel dient dazu, SwiftMQ mit anderen JMS-Implementationen zu verbinden. SwiftMQ implementiert ein Router-Netzwerk, an dem SwiftMQ- Clients hängen. Die interne Struktur des Netzwerks ist dabei für den Client transparent. Über ein geeignetes Netzwerk kann man somit die Lastverteilung beziehungsweise Ausfallsicherheit umsetzen.

SwiftMQ hat eine intuitiv zu bedienenden Oberfläche (siehe Abbildung 3) und ist zudem ausführlich dokumentiert. Damit sind nicht nur die üblichen administrierbaren JMS-Objekte wie Queues oder Topics zu erstellen, der Betrieb kann damit auch überwacht werden (siehe Abbildung 4). Daneben gibt es ein Kommandozeilentool mit ähnlicher Funktionalität und eine AP-Schnittstelle, über die man aus einem eigenen Java-Programm heraus die Administrationsaufgaben erfüllen kann.

Neben den beschriebenen Vorzügen hat SwiftMQ noch einen weiteren zu bieten: Es ist sehr schnell. Bei der im Folgenden beschriebenen Beispielanwendung – gestartet mit zwei Messstationen und einer Zentrale – ist SwiftMQ um den Faktor 15 schneller als OpenJMS.

Eine Beispielanwendung

Das folgende Beispiel ist bewusst einfach gehalten, es dient nur zur Erläuterung des Prinzips: Mehrere Messstationen registrieren Regentropfen innerhalb des Einheitsquadrats (das geschieht im Programm per Simulation mit Hilfe von java.util.Random). Die Koordinaten werden als Objekt in eine ObjectMessage eingepackt und über eine Queue an die Zentrale geschickt (siehe Listing 1, Zeilen 94 bis 106). Mit dieser “physikalischen” Methode kann man übrigens die Zahl Pi bestimmen: Pi/4 aller Punkte haben einen Abstand kleiner als Eins vom Nullpunkt.

Das QueueSender-Objekt wird in den Zeilen 65-86 erzeugt. Dabei wird die QueueSession als nicht transaktionsorientiert und im Auto-Acknowledge-Modus eingerichtet. Letzteres bedeutet, dass eine Message automatisch bestätigt wird, wenn ein entsprechender Receiver die Message abgeholt hat (über die dazu gehörige Receive()-Methode oder über einen MessageListener). Die Alternative dazu wäre die explizite Bestätigung der Messages durch den Receiver.

Beim QueueSender-Objekt wird der Delivery-Modus auf persistent gesetzt (Zeile 85). Das betrifft jedoch nur den Transport, wenn der Empfänger einen zu kleinen Puffer hat, können trotzdem Messages verloren gehen. Für die JMS-Implementation bedeutet der Persistenz-Modus mehr Overhead, da die Messages sicher gespeichert werden müssen. Im Beispiel ist das zwar nicht nötig, es erlaubt aber das Experimentieren mit den Persistenz-Funktionen des JMS-Providers.

Das Programm für den Empfänger sieht fast genauso aus, deshalb soll es hier nicht genau aufgeführt werden. Statt einer CreateSender()-Methode gibt es eine analoge CreateReceiver()-Methode (im Prinzip kann man in diesem Fall überall Sender durch Receiver ersetzen). Das Empfängerprogramm wartet in einer Endlosschleife auf Messages (über die Receive()-Methode des QueueReceiver-Objekts). Wenn die Koordinaten eintreffen, prüft das Programm, ob diese innerhalb des Einheitskreises liegen, und setzt einen Zähler hoch. Nach jeweils 50 Messages wird eine Schätzung für Pi ausgegeben.

Wie das Beispiel zeigt, ist die Verwendung der JMS-Infrastruktur sehr einfach – auch wenn das Programm die vielen Zusatzfunktionen von JMS nicht nutzt. Der komplette Code ist wie immer über meine Webseiten [7] verfügbar.

Listing 1: Station.java

040:   private QueueSender  iSender;
048:   private QueueSession iSession;
049:
065:   private void createSender() throws Exception {
068:     Hashtable env = new Hashtable();
069:     env.put(Context.INITIAL_CONTEXT_FACTORY, Const.JNDI_FACTORY);
070:     Context context = new InitialContext(env);
071:     if (context == null)
072:       throw new RuntimeException("Failed to get the root context");
073:     QueueConnectionFactory factory =
074:       (QueueConnectionFactory) context.lookup(Const.QC_FACTORY);
075:     if (factory == null)
076:       throw new RuntimeException("Failed to locate connection factory");
077:     QueueConnection connection = factory.createQueueConnection();
078:     connection.start();
079:     iSession =
080:       connection.createQueueSession(false,Session.AUTO_ACKNOWLEDGE);
081:     Queue queue = (Queue) context.lookup(Const.QUEUE_NAME);
082:     if (queue == null)
083:       throw new RuntimeException("Failed to locate queue " + Const.QUEUE_NAME);
084:     iSender = iSession.createSender(queue);
085:     iSender.setDeliveryMode(DeliveryMode.PERSISTENT);
086:   }
087:
094:   private void sendMessages(int count) throws Exception {
095:     Coord c = new Coord();
096:     Random r = new Random();
098:     for (int i=0; i<count; ++i) {
099:       c.x = r.nextDouble(); c.y = r.nextDouble();
100:       iSender.send(iSession.createObjectMessage(c));
103:     }
106:   }

finally{}

JMS wird von verschiedenen Herstellern sehr ernst genommen, wie man an der Liste der verfügbaren Implementationen sieht. Auch für die Enterprise Java Beans spielt JMS eine immer wichtigere Rolle. So sieht die neueste EJB-Spezifikation einen eigenen Bean-Typ dafür vor.

Wer über den Einsatz eines JMS-Systems nachdenkt, sollte sich zunächst einige Implementationen ansehen. Unter Umständen sind ja die Management-Tools wichtiger als die Basisfunktionalität. Wer Open-Source-Produkte bevorzugt, kann ebenfalls auf einschlägige Software zurückgreifen.

Außerdem sind natürlich die großen Firmen mit ihren Produkten auf dem Markt. Darunter gibt es Anbieter wie SwiftMQ mit sehr ausgefeilten Systemen für den kostenlosen Einsatz – aber leider ohne Verfügbarkeit der Quellen. ( uwo)

Infos

[1] Rusty Harold: Java Network Programming, O’Reilly 1997

[2] Die JMS-Homepage: http://java.sun.com/products/jms/

[3] Die OpenJMS-Homepage: http://openjms.exolab.org

[4] Die Homepage von ObjectCube, Inc: http://www.objectcube.com

[5] Die Joram-Homepage: http://www.objectweb.org/joram

[6] Die SwiftMQ-Homepage: http://www.swiftmq.com

[7] Download des Beispiels: http://www.bablokb.de/jms/

Der Autor

Bernhard Bablok arbeitet bei der AGIS mbH (Allianz Gesellschaft für Informatik Service mbH) als Systemprogrammierer im Systems-Management-Bereich. Wenn er nicht Musik hört, mit dem Radl oder zu Fuß unterwegs ist, beschäftigt er sich mit Themen rund um die Objektorientierung. Er ist zu erreichen unter: coffee-shop@bablokb.de

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben