• +49-(0)721-402485-12
Ihre Experten für XML, XQuery und XML-Datenbanken

XML-RPC

XML-RPC ist eigentlich eine Abart von RPC, was für remote procedure calls (Prozedur-Aufrufe auf entfernten Rechnern) steht. Wenn Sie erst neuerdings programmieren oder Java bisher nur kurz benutzt haben, werden Remote Procedure Calls wahrscheinlich neu für Sie sein; jemand, der sich damit nicht explizit beschäftigt, ist vielleicht in letzter Zeit RPC betreffend ein wenig eingerostet, da es in den letzten Jahren doch aus dem Fokus verschwunden ist. In diesem Kapitel wird beleuchtet, wie die drei kleinen Buchstaben vor dem RPC etwas revolutionieren, was auf dem besten Wege war, ein Computing-Dinosaurier zu werden, und wie man XML-RPC von Java aus benutzt. Am Ende dieses Kapitels wird an einigen praxistauglichen Applikationen nicht nur gezeigt, wie man diese Technologie einsetzt, sondern auch wann.

Sind Sie auf der Flutwelle der objektorientierten Entwicklung mitgeschwommen, die in den letzten drei oder vier Jahren vorbeirollte, könnte Ihnen das Wort »Prozedur« allein schon einen Schauer über den Rücken jagen. Prozedurale Sprachen wie PL/SQL oder ANSI-C sind wegen einer langen Liste guter Gründe nicht populär. Vielleicht sind Sie schon beschimpft worden, weil Sie eine Java-Methode als Funktion bezeichnet haben, und sicherlich können Sie mehr, als »Spaghetti-Code« zu schreiben. Das ist Code, der Methode auf Methode in einer endlosen Folge hintereinander enthält.

Genau wie diese Techniken und Sprachen ist auch RPC am Wegesrand liegengeblieben. Es gibt neue, objektorientierte Möglichkeiten, um dieselben Resultate oft mit besserem Design und höherer Performance zu erreichen. Überraschenderweise hat jedoch der Aufstieg von XML den Aufstieg und die Beachtung von extra für XML-RPC entworfenen APIs und einen Trend zur Benutzung von XML-RPC in speziellen Situationen trotz der damit verbundenen Implikationen nach sich gezogen.

Bevor Sie sich mit diesen APIs beschäftigen, sollten Sie zunächst einen Blick auf die Fähigkeiten von RPC werfen und RPC mit anderen, ähnlichen Java-Technologien, besonders mit der remote method invocation (RMI; deutsch: Methoden-Aufrufe auf entfernten Rechnern), vergleichen. Falls Sie sich dafür entscheiden, XML-RPC in eigenen Applikationen einzusetzen (und das wollen Sie früher oder später sicherlich), sollten Sie sich darüber im klaren sein, daß Sie diese Wahl gegenüber anderen Entwicklern rechtfertigen müssen, speziell gegenüber solchen, die gerade Bücher über EJB oder RMI gelesen haben.

Natürlich gibt es Szenarien zur Anwendung all dieser Technologien. Das Wissen um die korrekte Anwendung jeder dieser Technologien bestimmt über den eigenen Erfolg – nicht nur als Entwickler, sondern auch als Team-Mitglied und Mentor. Um Ihnen also die Konzepte hinter diesen Remote-Methodiken verständlich zu machen, werfen wir nun einen Blick auf die zwei wichtigsten Verfahren zum Zugriff auf Objekte über ein Netzwerk: RPC und RMI.

RPC versus RMI

Wenn Sie die letzten Jahre nicht gerade als Einsiedler zugebracht haben, werden Sie bemerkt haben, daß EJB und RMI die Java-Welt im Sturm erobert haben. Die gesamte EJB-(Enterprise JavaBeans-)Spezifikation basiert auf RMI-Prinzipien, und Sie würden es sehr schwer haben, eine Three-Tier-Application ohne RMI zu entwickeln. Mit anderen Worten: Sie sollten ein wenig Zeit investieren, um beispielsweise einen Blick in die Bücher Java Enterprise in a Nutshell von David Flanagan, Jim Farley, William Crawford und Kris Magnusson oder in Java Distributed Computing von Jim Farley (beide von O’Reilly & Associates) zu werfen, falls Sie RMI noch nicht kennen sollten.

Was ist RMI?

RMI ist die englische Abkürzung für Methoden-Aufrufe auf entfernten Rechnern. RMI erlaubt es einem Programm, Methoden von Objekten aufzurufen, wenn die Objekte nicht auf demselben Rechner wie das Programm liegen. Das ist das Herz des verteilten Rechnens in der Java-Welt und das Rückgrat von EJB wie auch vieler Imple-mentierungen von Enterprise-Anwendungen. Ohne zu sehr ins Detail zu gehen, benutzt RMI Client-Stubs (Stub engl. Stumpf) zur Beschreibung der Methoden, die ein entferntes Objekt zum Aufrufen zur Verfügung stellt. Der Client arbeitet mit diesen Stubs (die Java-Interfaces sind), und RMI ist für die »Magie« des Übersetzens der Anfragen an einen Stub in einen Netzwerk-Aufruf verantwortlich. Dieser ruft die Methode auf dem Rechner, der das entsprechende Objekt beherbergt, auf und sendet die Resultate über das Netzwerk zurück. Schließlich gibt der Stub das Resultat an den Client zurück, der den eigentlichen Aufruf initiierte, und dieser arbeitet weiter.

Die Hauptidee dahinter ist die, daß sich der Client nicht um RMI- und Netzwerk-Details kümmern muß; er benutzt den Stub, als wäre er ein reales Objekt mit den entsprechenden implementierten Methoden. RMI (unter Benutzung von JRMP™, dem Remote Protocol von Java) läßt alle Kommunikation hinter dem Vorhang ablaufen und erlaubt es damit dem Client, sich mehr Zeit für die Behandlung der Geschäftsregeln und die Anwendungslogik zu gönnen und nur auf eine generische Exception (java.rmi.RemoteException) achten zu müssen. RMI kann außerdem verschiedene Protokolle, wie zum Beispiel das Internet Inter-ORB Protocol (IIOP), benutzen und erlaubt damit die Kommunikation zwischen Java- und CORBA-Objekten, die oft in anderen Sprachen, wie C oder C++, geschrieben wurden.

RMI bringt jedoch auch Kosten mit sich. Zunächst ist RMI ressourcenintensiv. JRMP hat eine sehr schlechte Performance. Ein eigenes Remote-Protokoll zu schreiben ist wiederum keine einfache Sache. Wenn ein Client RMI-Aufrufe anstößt, müssen Sockets geöffnet und verwaltet werden, deren Anzahl die System-Performance beeinflussen kann, speziell, wenn über ein Netzwerk auf das System zugegriffen werden kann (was dazu führt, daß mehr Sockets für den HTTP-Zugriff geöffnet werden müssen).

RMI benötigt außerdem einen Server oder Provider, an den die Objekte gebunden werden. Bis ein Objekt nicht bei einem solchen Provider an einen Namen gebunden ist, kann man auch nicht auf das Objekt zugreifen. Dazu müssen Sie eine RMI-Registry, einen Lightweight Directory Access Protocol-(LDAP-)Directory-Server oder einen anderen aus der Vielfalt der Java Naming and Directory Interface-(JNDI-)Services benutzen. Schließlich ist es möglich, daß RMI trotz all der hilfreichen RMI-Klassen im JDK eine Menge an Programmieraufwand erfordert; ein Remote-Interface, das die verfügbaren Methoden beschreibt, muß geschrieben werden (wie auch einige andere Interfaces, wenn man EJBs benutzt). Das bedeutet aber auch, daß das Hinzufügen einer Methode zur Server-Klasse eine Änderung des Interfaces der Client-Stubs und eine Neukompilierung nach sich zieht, was oft unerwünscht und manchmal unmöglich ist.

Was ist RPC?

RPC bedeutet Prozedur-Aufruf auf einem entfernten Rechner. Während es RMI ermöglicht, direkt mit einem Java-Objekt zu arbeiten, ist RPC mehr in Richtung »Verteilung« ausgerichtet. Anstatt mit Objekten zu arbeiten, bietet RPC den Zugriff auf Stand-alone-Methoden über das Netzwerk. Obwohl das die Interaktivität einschränkt, sorgt es für eine etwas einfachere Schnittstelle zum Client. Sie können sich RPC als ein Verfahren zur Benutzung von »Services« auf entfernten Rechnern vorstellen, während RMI der Benutzung von »Servern« auf entfernten Rechnern entspricht.

Der feine Unterschied ist, daß RMI mehr vom Client getrieben wird. Das Auslösen von Events folgt auf den Aufruf von Methoden durch den Client. RPC ist oft mehr eine Klasse oder eine Menge von Klassen, die Aufgaben mit oder ohne Intervention seitens des Clients abarbeiten; dennoch bedienen diese Klassen zu bestimmten Zeiten Anfragen von Clients und führen »Mini«-Aufgaben für sie aus. In Kürze werden einige Beispiele folgen, die diese Definitionen erhellen.

Obwohl RPC nicht so eine interaktive Umgebung darstellt wie RMI, hat es doch einige deutliche Vorteile. RPC ermöglicht es, grundsätzlich verschiedene Systeme zusammenarbeiten zu lassen. Während RMI es über IIOP ermöglicht, Java mit CORBA-Servern und Clients zu verbinden, stellt RPC wirklich jede Art von Anwendungskommunikation zur Verfügung, da als Protokoll HTTP verwendet werden kann. Weil heutzutage eigentlich jede Sprache irgendeine Möglichkeit hat, mittels HTTP zu kommunizieren, ist RPC besonders für Programme interessant, die mit Legacy-Systemen zusammenarbeiten sollen.

RPC ist außerdem typischerweise ressourcenschonender als RMI (besonders wenn XML zur Codierung zum Einsatz kommt, was als nächstes beleuchtet wird); während RMI oft ganze Java-Klassen über das Netzwerk laden muß (wie Code für Applets oder selbstgeschriebene Helper-Klassen für EJB), muß RPC nur die Request-Parameter und die resultierende Antwort, die in der Regel als Text vorliegt, über das Netz befördern. RPC paßt auch gut ins API-Modell, indem es Systemen, die nicht Teil Ihrer eigenen Anwendung sind, erlaubt, dennoch Informationen mit ihr auszutauschen. Das bedeutet, daß Änderungen des Servers keine Änderungen am Code anderer Clients nach sich ziehen; mit Requests und Datentransfer in Textform können neue Methoden ohne Neukompilation der Clients hinzugefügt werden. Es sind nur kleine Änderungen an Clients nötig, um neue Methoden zu nutzen.

Das Problem bei RPC war in der Vergangenheit die Codierung der zu übertragenden Daten. Ein Beispiel ist die Aufgabe, eine Java-Hashtable oder einen Vector leichtgewichtig in Textform zu repräsentieren. Wenn Sie in Betracht ziehen, daß diese Datenstrukturen wiederum andere Java-Objekte enthalten können, wird die Datenrepräsentation schnell schwierig zu schreiben; außerdem muß das Format weiterhin für alle verschiedenen Programmiersprachen verständlich bleiben, da andernfalls die Vorteile von RPC schwinden.

Bis vor kurzem existierte eine umgekehrte Proportionalität zwischen der Qualität und Benutzbarkeit der Codierung und ihrer Einfachheit; anders gesagt: Je einfacher es wurde, komplexe Objekte zu repräsentieren, desto schwieriger wurde ihre Benutzung in anderen Programmiersprachen, ohne proprietäre Erweiterungen und Code zu benutzen. Ausgefeilte textuelle Repräsentationen von Daten waren nicht standardisiert und erforderten neue Implementierungen in jeder Sprache, um benutzbar zu sein. Sie sehen schon, wo das hinführt.

XML-RPC

Das größte Hindernis bei der Benutzung von RPC war in der Vergangenheit seine Codierung. Aber dann tauchte XML mit einer Lösung auf. XML stellt nicht nur eine sehr einfache textliche Repräsentation, sondern auch einen Standard für die Struktur von Daten dar. Bedenken wegen proprietärer Lösungen wurden hinweggewischt, als das W3C die XML 1.0-Spezifikation verabschiedete, die RPC-Programmierern versicherte, daß XML nicht irgendwann abdriftet. Außerdem bot SAX ein leichtgewichtiges, standardisiertes Verfahren, auf XML zuzugreifen, was es wesentlich einfacher machte, RPC-Bibliotheken zu implementieren.

Damit mußten XML-RPC-Implementierer lediglich für die Übertragung via HTTP sorgen (etwas, das Menschen schon viele Jahre lang taten) und die spezifischen APIs zum Codieren und Decodieren schreiben. Nach einigen Beta-Implementierungen von XML-RPC-Bibliotheken wurde klar, daß XML außerdem eine sehr schnelle und leichtgewichtige Art der Codierung darstellt, was zu einer besseren Performance der XML-RPC-Bibliotheken führte, als eigentlich erwartet worden war. Heute ist XML-RPC eine überlebensfähige und stabile Lösung für Prozedur-Aufrufe auf entfernten Rechnern.

Für den Java-Entwickler stellt XML-RPC eine Möglichkeit dar, einfache »Hooks« in die eigene Anwendung und ihre Services einzufügen, die die Anwendung selbst, aber auch Clients in anderen Abteilungen oder Unternehmen nutzen können. Es entkoppelt diese APIs auch von Java, wenn Clients die Sprache Java nicht direkt nutzen können. Schließlich macht XML-RPC es überflüssig, die Technologie RMI zu erlernen, um verteilte Services zu benutzen (zumindest anfangs).

Dieses Kapitel wird sich mit der Implementierung eines XML-RPC-Servers und eines Clients beschäftigen, und es wird zeigen, wie ein Server unabhängig von Clients arbeiten kann und dennoch XML-RPC-Schnittstellen zur Zusammenarbeit und Abfrage von Daten zur Verfügung stellt. Obwohl hier RMI nicht tiefergehend betrachtet wird, wird in diesem Kapitel die XML-RPC-Lösung kontinuierlich mit der entsprechenden RMI-Variante verglichen und darauf hingewiesen, warum XML-RPC ein besserer Weg zur Lösung verschiedener Aufgaben ist.

Sag »Hallo!«

Sie möchten wahrscheinlich wissen, ob XML-RPC die richtige Lösung für einige Ihrer Programmierprobleme ist. Um Ihr Verständnis für XML-RPC zu schärfen, werden wir nun ein wenig Java-Code unter Benutzung von XML-RPC schreiben. Einer großen Tradition der Programmierung folgend, macht dabei ein simples »Hello World«-Programm den Anfang. Dabei wird gezeigt, wie man einen XML-RPC-Server definiert und einen Handler bei diesem Server registriert. Dieser Handler nimmt einen Java-String als Parameter, der den Benutzernamen darstellt, und gibt »Hello«, gefolgt vom Namen zurück; diese Rückgabe könnte also zum Beispiel »Hello Shirley« heißen. Dann muß der Handler noch für XML-RPC-Clients verfügbar gemacht werden. Schließlich wird ein einfacher Client generiert, der zum Server Kontakt aufnimmt und die Ausführung der Methode anfordert.

In einer praktischen Anwendung befänden sich der Server und der Handler vermutlich auf einem Rechner, vielleicht einem dedizierten Server, und der Client würde von einem anderen Rechner aus die Prozeduraufrufe initiieren. Falls Ihnen jedoch nicht mehrere Rechner zur Verfügung stehen, können Sie die Beispiele auch lokal ausprobieren. Obwohl das viel schneller gehen wird, als das bei realen Clients und Servern der Fall wäre, können Sie dennoch sehen, wie die Teile zusammenarbeiten, und einen Eindruck von XML-RPC bekommen.

XML-RPC-Bibliotheken

Eine Menge Arbeit ist bereits in RPC und kürzlich in XML-RPC investiert worden. Wie bei der Benutzung von SAX, DOM und JDOM für die Arbeit mit XML existiert auch hier kein Grund, das Rad neu zu erfinden, wenn es gute und sogar ausgezeichnete Java-Packages für den entsprechenden Anwendungszweck gibt. Das Zentrum für Informationen über XML-RPC und Links zu Bibliotheken für Java wie auch für viele andere Sprachen ist http://www.xmlrpc.com. Durch Userland (http://www.userland.com) gesponsert, bietet diese Site eine öffentlich zugängliche Spezifikation von XML-RPC, Informationen über die unterstützten Datentypen und einige Tutorials zur Benutzung von XML-RPC. Am wichtigsten aber ist, daß sie zu dem XML-RPC-Package für Java führt. Wenn Sie dem Link auf der Hauptseite folgen, kommen Sie zu Hannes Wallnofers Site unter http://classic.helma.at/hannes/xmlrpc/.

Auf Hannes’ Site gibt es eine Beschreibung der Klassen in seinem XML-RPC-Package und Anweisungen dazu. Sie müssen das Archiv herunterladen und in ein entsprechendes Verzeichnis entpacken, wo sich bereits andere Entwicklungstools und -Packages befinden, beziehungsweise in die IDE laden. Anschließend sollten Sie in der Lage sein, die Klassen zu kompilieren; es ist ein Servlet-Beispiel dabei, für das Sie die Servlet-Klassen (servlet.jar für die Servlet API 2.2) benötigen. Sie können diese Klassen mit Tomcats Servlet-Engine von http://jakarta.apache.org holen. Die Servlet-Klassen werden für die Beispiele in diesem Kapitel nicht benötigt. Wenn Sie dieses Servlet-Beispiel nicht ausprobieren möchten, brauchen Sie sie also nicht herunterzuladen.

Der Kern der Distribution (der die Applet- oder RegExp-Beispiele in dem heruntergeladenen Archiv nicht beinhaltet) besteht aus dreizehn Klassen, die alle Teil des Packages helma.xmlrpc sind. Diese befinden sich in der Datei lib/xmlrpc.jar der Distribution und können direkt verwendet werden. Die Klassen der Distribution werden in Tabelle 11-1 kurz beschrieben.

Tabelle 11-1: Die XML-RPC-Klassen
KlasseVerwendungszweck
XmlRpcKernklasse, die Methodenaufrufe an Handlern durch XML-RPC-Server erlaubt
XmlRpcClientEine Client-Klasse für die RPC-Kommunikation über HTTP, einschließlich Poxy- und Cookie-Unterstützung
XmlRpcClientLiteClient-Klasse, wenn ein schlankerer HTTP-Client ausreicht (keine Cookies, keine Proxy-Unterstützung)
XmlRpcServerKlasse für Server zum Empfang von RPC-Aufrufen
XmlRpcServletFunktionalität der Klasse XmlRpcServer als Servlet
XmlRpcProxyServletEin XML-RPC-Servlet-Proxy
XmlRpcHandlerGrundlegende Schnittstelle zur Steuerung von XML-RPC-Interaktionen von Handlern
AuthenticatedXmlRpcHandlerWie XmlRpcHandler, erlaubt Authentifikation
Base64Codiert und decodiert zwischen Bytes und Base 64-Encoding
BenchmarkMißt die Zeit für XML-RPC-Interaktionen für einen spezifischen SAX-Treiber
WebServerEin schlanker HTTP-Server zur Benutzung von XML-RPC-Servern

Die SAX-Klassen (aus früheren Kapiteln) und ein SAX-Treiber sind nicht Bestandteil der Distribution, sie werden jedoch zur Arbeit damit benötigt. Sie brauchen eine vollständige XML-Parser-Implementierung, die SAX 1.0 unterstützt. Ich benutze weiterhin den Apache Xerces in den Beispielen, obwohl die Bibliotheken jeden SAX 1.0-kompatiblen Browser unterstützen.

Nachdem Sie alle Quelldateien kompiliert haben, müssen Sie sicherstellen, daß alle XML-RPC-Klassen, alle SAX-Klassen und alle Klassen des XML-Parsers im Klassenpfad gefunden werden können. Damit sind Sie in der Lage, eigenen Code zu schreiben und den Prozeß des »Hallo-Sagens« zu beginnen. Sie sollten den Quellcode der XML-RPC-Klassen vorliegen haben, da ein Blick dort hinein für das Verständnis der Beispiele hilfreich sein kann.

Schreiben des Handlers

Zuerst müssen Sie die Klasse mit der Methode schreiben, die Sie von Ferne aufrufen möchten. Eine solche Klasse wird normalerweise Handler genannt. Vorsicht ist angebracht, da der Mechanismus des XML-RPC-Servers, der die Anfragen verteilt, ebenfalls oft Handler genannt wird; und wieder zeigt die Mehrdeutigkeit von Namen ihr häßliches Gesicht.

Eine klarere Unterscheidung kann wie folgt formuliert werden: Ein XML-RPC-Handler ist eine Methode oder eine Menge von Methoden, die einen XML-RPC-Request nehmen, seinen Inhalt decodieren und ihn an eine Klasse und Methode weiterleiten. Ein Response Handler (Antwort-Handler) oder einfach Handler ist eine Methode, die durch einen XML-RPC-Handler aufgerufen werden kann. Mit den XML-RPC-Bibliotheken für Java brauchen Sie keinen XML-RPC-Handler zu schreiben, da bereits einer in der Klasse helma.xmlrpc.XmlRpcServer implementiert ist. Sie müssen lediglich eine Klasse mit einer oder mehreren Methoden schreiben und sie beim Server registrieren.

Es wird Sie vielleicht überraschen, daß es nicht nötig ist, von irgendeiner bestimmten Klasse abzuleiten oder andere spezielle Mechanismen in den Code einzubauen, wenn Sie einen Antwort-Handler schreiben wollen. Jede Methode kann via XML-RPC aufgerufen werden, solange die Typen ihrer Parameter und ihres Rückgabewertes von XML-RPC unterstützt werden (d.h. codiert werden können). Tabelle 11-2 führt alle momentan unterstützten Java-Typen auf, die in Signaturen von XML-RPC-Methoden benutzt werden können.

Tabelle 11-2: Unterstützte Java-Typen in XML-RPC
XML-RPC-DatentypJava-Typ
intint
booleanboolean
stringString
doubledouble
dateTime.iso8601Date
structHashtable
arrayVector
base64byte[]
nilnull

Obwohl diese Liste nur eine kleine Anzahl an Typen enthält, reichen sie für die meisten der über ein Netzwerk gesendeten XML-RPC-Requests. Die Methode in diesem Beispiel braucht nur einen String (den Namen der Person, zu der »hello« gesagt werden soll) und gibt auch nur einen String zurück. Sie erfüllt damit die genannten Bedingungen. Das reicht an Wissen, um eine einfache Handler-Klasse zu schreiben, wie sie in Beispiel 11-1 zu sehen ist.

Beispiel 11-1: Handler-Klasse mit der Methode sayHello

package javaxml2;

public class HelloHandler {

	public String sayHello(String name) {
		return "Hello " + name;
	}
}

Das ist genauso einfach, wie es aussieht. Die Signatur der Methode erwartet als Parameter legale XML-RPC-Parameter und liefert solche auch als Rückgabewert. Damit kann sie sicher bei dem (noch zu erstellenden) XML-RPC-Server registriert werden, und Sie können sicher sein, daß sie mittels XML-RPC aufrufbar sein wird.

Schreiben des Servers

Ist der Handler fertiggestellt, müssen Sie noch ein Programm schreiben, das einen XML-RPC-Server startet, auf Requests lauscht und diese Requests an den Handler weiterleitet. Für dieses Beispiel wird die Klasse helma.xmlrpc.WebServer als Request-Handler benutzt. Sie könnten auch ein Servlet benutzen, allerdings können Sie sich durch die Benutzung dieser leichtgewichtigen Implementierung den Start einer Servlet-Engine auf dem XML-RPC-Server sparen.

Am Ende dieses Kapitels wird ausführlicher auf das Servlet im Zusammenhang mit dem XML-RPC-Server eingegangen. In dem folgenden Beispiel wird ein Port für den Server angegeben, an dem er dann auf Requests lauscht, bis er beendet wird. Nun müssen Sie die gerade erzeugte Klasse noch beim Server registrieren und dem Server noch etwaige anwendungsspezifische Parameter übergeben.

Zunächst wird das in Beispiel 11-2 gezeigte Skelett eingegeben; Sie müssen die Klasse WebServer importieren und sicherstellen, daß dem Programm von der Kommandozeile eine Portnummer übergeben wird, wenn der Server gestartet wird.

Beispiel 11-2: Skelett für den XML-RPC-Server

package javaxml2;

import helma.xmlrpc.WebServer;

public class HelloServer {

    public static void main(String[] args) {
        if (args.length < 1) {
            System.out.println(
                "Benutzung: java javaxml2.HelloServer [port]");
            System.exit(-1);
        }
    
        // Starten des Servers am angegebenen Port
    }
}

Bevor der Server gestartet wird, muß noch der SAX-Treiber, der für das Parsen und Codieren von XML verwendet werden soll, angegeben werden. Der voreingestellte SAX-Treiber für diese Bibliotheken ist James Clarks XP-Parser, der unter http://www.jclark.com verfügbar ist. In diesem Beispiel wird statt dessen der Apache Xerces-Parser durch Angabe der Klasse SAX Parser1 bei der XML-RPC-Engine angefordert. Das geschieht durch die Methode setDriver( ). Das ist eine statische Methode der Klasse XmlRpc. Diese Klasse dient als Fundament für die Klasse WebServer, muß aber importiert und direkt benutzt werden, um diese Änderung in SAX-Treibern vorzunehmen.

Die Exception ClassNotFoundException kann von dieser Methode ausgelöst werden und muß demzufolge auch abgefangen werden, sollte die Treiber-Klasse nicht zur Laufzeit im Klassenpfad gefunden werden. Nun fügen Sie die notwendigen import-Anweisungen und Methoden zu der Klasse HelloServer hinzu:

package javaxml2;

import helma.xmlrpc.WebServer;
import helma.xmlrpc.XmlRpc;

public class HelloServer {

    public static void main(String[] args) {
        if (args.length < 1) {
            System.out.println(
                "Benutzung: java javaxml2.HelloServer [port]");
            System.exit(-1);
        }
    
        try {            // Benutzen des Apache Xerces-SAX-Treibers            XmlRpc.setDriver("org.apache.xerces.parsers.SAXParser");            // Starten des Servers        } catch (ClassNotFoundException e) {            System.out.println("Konnte den SAX-Treiber nicht finden");        }                    
    }
}

An diesem Punkt können Sie nun den Hauptteil des Codes hinzufügen, der den HTTP-Listener erzeugt, welcher die XML-RPC-Requests bedient und dann die Handler-Klassen registriert, die für Remote-Aufrufe verfügbar sein sollen. Das Erzeugen des Listeners ist sehr einfach: Die bereits erwähnte Helper-Klasse WebServer kann mit einer Portnummer instantiiert werden, an der gelauscht werden soll. Ebenso einfach bedient der Server XML-RPC-Requests.

Obwohl noch keine Klassen verfügbar sind, die man aufrufen kann, haben Sie jetzt bereits einen voll funktionsfähigen XML-RPC-Server. Jetzt wird die Zeile eingefügt, die den Server startet, und eine Statuszeile für Ausgabezwecke. Außerdem gehören noch eine import-Anweisung und eine Exception-Behandlung für eine java.io. IOException dazu. Da der Server an einem Port gestartet werden muß, kann eine IOException ausgelöst werden, wenn auf den Port nicht zugegriffen werden kann oder sonst Probleme beim Starten des Servers auftreten. Das geänderte Code-Fragment sieht wie folgt aus:

package javaxml2;

import java.io.IOException;

import helma.xmlrpc.WebServer;
import helma.xmlrpc.XmlRpc;

public class HelloServer {

    public static void main(String[] args) {
        if (args.length < 1) {
            System.out.println(
                "Benutzung: java javaxml2.HelloServer [port]");
            System.exit(-1);
        }
    
        try {
            // Benutzen des Xerces SAX-Treibers
            XmlRpc.setDriver("org.apache.xerces.parsers.SAXParser");

            // Starten des Servers            System.out.println("Starte den XML-RPC-Server...");            WebServer server = new WebServer(Integer.parseInt(args[0]));            
            
        } catch (ClassNotFoundException e) {
            System.out.println("Konnte den SAX-Treiber nicht finden");
        } catch (IOException e) {            System.out.println("Konnte den Server nicht starten: " +                 e.getMessage(  ));        }                                
    }
}

Kompilieren Sie die Klasse und probieren Sie sie aus. Sie funktioniert vollständig, gibt die Statuszeile aus und wartet dann auf Requests. Jetzt muß noch die Handler-Klasse hinzugefügt werden, so daß der Server Requests bearbeiten kann.

Einer der deutlichsten Unterschiede zwischen RPC und RMI ist die Art und Weise, wie Methoden zugänglich gemacht werden. Bei RMI enthält ein sogenanntes Remote-Interface die Signaturen aller Remote-Methoden. Ist eine neue Methode zum Server, ihre Signatur jedoch nicht dem Remote-Interface hinzugefügt worden, kann kein RMI-Client darauf zugreifen. Das sorgt für eine große Menge Code-Modifikationen und Neukompilierungen bei der Entwicklung von RMI-Klassen.

Dieser Prozeß wird bei der Benutzung von RPC generell als leichter und flexibler eingestuft. Wenn ein Request beim RPC-Server ankommt, enthält dieser eine Menge von Parametern und einen textuellen Wert, normalerweise in der Form »Klassenname.Methodenname«. Das bedeutet für den RPC-Server, daß die Methode »Methodenname« der Klasse »Klassenname« aufgerufen werden soll. Der RPC-Server sucht dann nach einer passenden Klasse und Methode, die Parametertypen erwartet, die denen im Request entsprechen. Wird eine Übereinstimmung gefunden, wird diese Methode aufgerufen und das Ergebnis codiert und zurückgeschickt.

Daher wird die angeforderte Methode nie explizit im XML-RPC-Server definiert, sondern im Request vom Client. Es wird nur eine Klasseninstanz beim XML-RPC-Server registriert. Sie können Methoden zu der Klasse hinzufügen, den Server (ohne Codeänderungen) neu starten und haben dann sofort vom Client aus Zugriff auf die neuen Methoden. Wenn es möglich ist, die korrekten Parameter zu ermitteln und an den Server zu senden, stehen die neuen Methoden sofort zur Verfügung. Das ist einer der Vorteile von XML-RPC gegenüber RMI: Es kann eine API kompakter repräsentieren. Es müssen keine Client-Stubs, Code-Skelette oder Interfaces aktualisiert werden. Wenn eine Methode hinzugefügt wird, kann der Methodenname der Client-Gemeinde bekanntgegeben werden, die sie dann sofort benutzen kann.

Nun, da theoretisch erklärt wurde, wie einfach es ist, einen RPC-Handler zu benutzen, zeige ich im Beispiel HelloHandler, wie ein solcher registriert wird. Die Klasse WebServer erlaubt das Hinzufügen eines Handlers mittels der Methode addHandler( ). Diese Methode erwartet einen Namen als Eingabe, unter dem der Handler registriert wird, und eine Instanz des Handlers selbst. Diese wird typischerweise durch die Instantiierung der Klasse mittels eines Konstruktors (über das Schlüsselwort new) erzeugt. Es gibt jedoch auch andere Möglichkeiten, wenn eine Instanz von mehreren Clients gemeinsam genutzt und nicht von jedem Client extra erzeugt werden soll. Diese Möglichkeiten werden im nächsten Abschnitt vorgestellt. Im vorliegenden Beispiel ist die Instantiierung einer Klasse eine ausreichende Lösung. Die Klasse HelloHandler wird unter dem Namen »hello« registriert. Es kommen dann noch Statusmeldungen dazu, um zu zeigen, was im Server abläuft, während der Handler hinzugefügt wird:

        try {
            // Benutze den Apache Xerces SAX-Treiber
            XmlRpc.setDriver("org.apache.xerces.parsers.SAXParser");

            // Starten des Servers
            System.out.println("Start des XML-RPC-Servers...");
            WebServer server = new WebServer(Integer.parseInt(args[0]));            
            
            // Registrieren der Handler-Klasse            server.addHandler("hello", new HelloHandler(  ));            System.out.println(                "HelloHandler-Klassa an \"hello\" gebunden");                        System.out.println("Warte auf Requests...");                                   
            
        } catch (ClassNotFoundException e) {
            System.out.println("Konnte SAX-Treiber nicht finden");
        } catch (IOException e) {
            System.out.println("Konnte den Server nicht starten: " + 
                e.getMessage(  ));
        }      

Nun wird diese Quelldatei neu kompiliert und der Server gestartet. Die Ausgabe sollte wie in Beispiel 11-3 aussehen.2

Beispiel 11-3: Starten des Servers

$ java javaxml2.HelloServer 8585
Start des XML-RPC-Servers...
HelloHandler-Klasse an "hello" gebunden
Warte auf Requests...

Ist das einfach! Sie können nun einen Client für den Server schreiben und die Netzwerkkommunikation unter Benutzung von XML-RPC testen. Das ist ein weiterer Vorteil von XML-RPC: Die Schwelle zum Beginn der Programmierung von Servern und Clients ist, verglichen mit der Komplexität bei RMI, niedrig. Lesen Sie weiter, und Sie werden sehen, daß die Schaffung eines Clients genauso einfach ist.

Schreiben des Clients

Nun, da der Server läuft und auf Requests wartet, haben Sie den schwersten Teil der Programmierung einer XML-RPC-Anwendung hinter sich (Sie können ruhig glauben – das war der schwere Teil!). Nun müssen Sie nur noch einen simplen Client stricken, der die Methode sayHello( ) aufruft. Das erreichen Sie einfach, indem Sie die Klasse helma.xmlrpc.XmlRpcClient benutzen.

Diese Klasse kümmert sich um eine ganze Menge an Details auf der Seite des Clients, um die sich die Entsprechungen XmlRpcServer und WebServer auf der Seite des Servers kümmern. Um den Client zu schreiben, brauchen Sie diese Klasse wie auch die Klasse XmlRpc. Diese Klasse muß sich um das Encoding des Requests kümmern, daher müssen Sie auch hier wieder den SAX-Treiber mit der Methode setDriver( ) angeben. Fangen Sie Ihren Client-Code mit den benötigten import-Anweisungen an, holen Sie sich das Argument für die Methode sayHello( ) von der Kommandozeile, und fügen Sie die benötigte Exception-Behandlung hinzu. Speichern Sie den Java-Quellcode aus Beispiel 11-4 unter HelloClient.java.

Beispiel 11-4: Ein Client für den XML-RPC-Server

package javaxml2;

import helma.xmlrpc.XmlRpc;
import helma.xmlrpc.XmlRpcClient;

public class HelloClient {
 
    public static void main(String args[]) {
        if (args.length < 1) {
            System.out.println(
                "Benutzung: java HelloClient [Ihr Name]");
            System.exit(-1);
        }
        
        try {
            // Benutze den Apache Xerces-SAX-Treiber
            XmlRpc.setDriver("org.apache.xerces.parsers.SAXParser");

            // Angeben des Servers

            // Erzeugen des Requests

            // Abschicken des Requests und Ausgabe der Resultate
          
        } catch (ClassNotFoundException e) {
            System.out.println("Konnte den SAX-Treiber nicht finden");
        }        
    } 
}

Wie der andere Code in diesem Kapitel ist auch der hier einfach und unkompliziert. Um einen XML-RPC-Client zu erzeugen, müssen Sie die Klasse XmlRpcClient instantiieren. Dazu brauchen Sie den Hostnamen des Servers, auf dem der XML-RPC-Server läuft, zu dem Sie eine Verbindung herstellen möchten. Das sollte eine komplette URL mit dem Protokoll-Präfix http:// sein. Beim Kreieren des Clients kann die Exception java.net.MalformedURLException ausgelöst werden, wenn das Format der URL nicht akzeptiert wird. Dann müssen Sie diese Klasse zur Liste der importierten Klassen hinzufügen, den Client instantiieren und den erforderlichen Exception-Handler einfügen:

package javaxml2;
import java.net.MalformedURLException;

import helma.xmlrpc.XmlRpc;
import helma.xmlrpc.XmlRpcClient;

public class HelloClient {
 
    public static void main(String args[]) {
        if (args.length < 1) {
            System.out.println(
                "Benutzung: java HelloClient [Ihr Name]");
            System.exit(-1);
        }
        
        try {
            // Benutze den Apache Xerces-SAX-Treiber
            XmlRpc.setDriver("org.apache.xerces.parsers.SAXParser");
            
            // Angeben des Servers            XmlRpcClient client =                 new XmlRpcClient("http://localhost:8585/");  

            // Erzeugen des Requests

            // Abschicken des Requests und Ausgabe der Resultate          
          
        } catch (ClassNotFoundException e) {
            System.out.println("Konnte den SAX-Treiber nicht finden");
        } catch (MalformedURLException e) {            System.out.println(                "Falsche URL für XML-RPC-Server: " +                 e.getMessage(  ));        }        
    } 
}

Obwohl keine wirklichen RPC-Aufrufe ausgeführt werden, haben Sie damit eine voll funktionsfähige Client-Anwendung. Diese Anwendung kann kompiliert und gestartet werden, Sie werden jedoch keine Aktivität bemerken, da keine Verbindung geöffnet wird, solange kein Request abgesetzt wird.

Die Portnummer im Quellcode muß diejenige sein, an der der Server lauscht. Das ist natürlich eine schlechte Variante der Implementierung von Connectivity zwischen Server und Client; das Ändern der Portnummer des Servers zieht Änderungen am Code des Clients und eine Neukompilierung nach sich! In eigenen Anwendungen sollten Sie das über eine benutzerdefinierte Variable festlegen. Für das Beispiel sollte die Einfachheit gewahrt bleiben.

Die Leichtigkeit, mit der dieser Client und unser Server zusammenkommen, ist eindrucksvoll. Jedoch ist das Programm immer noch ziemlich nutzlos, solange es keinen Request stellt, beziehungsweise keine Antwort empfängt. Um den Request zu codieren, wird die Methode execute( ) der Instanz der Klasse XmlRpcClient aufgerufen. Diese Methode benötigt zwei Parameter. Das sind ein String und ein Vector, der die Parameter enthält, die der spezifizierten Methode übergeben werden sollen.

Der Klassen-Identifier ist der Name, unter dem der HelloHandler beim XML-RPC-Server registriert wurde; dieser Name kann der Klassenname sein, oft wird jedoch etwas Verständlicheres gewählt. Im Beispiel war das »hello«. Der Name der aufzurufenden Methode wird daran durch einen Punkt getrennt angehängt, so daß sich folgende Form ergibt: [classidentifier].[method name]. Die Parameter müssen als Java-Vector vorliegen, der alle Parameterobjekte enthält, die diese Methode benötigt. Die einfache Methode sayHello( ) erwartet einen String mit dem Namen des Benutzers, der auf der Kommandozeile angegeben werden sollte.

Wenn der XML-RPC-Client diesen Request codiert, sendet er ihn an den XML-RPC-Server. Der Server sucht die Klasse, die dem Klassen-Identifier des Requests entspricht, und einen passenden Methodennamen in dieser Klasse. Wenn ein solcher gefunden wird, werden die Parametertypen der Methode mit denen im Request verglichen. Stimmen sie überein, wird die Methode ausgeführt. Wenn mehrere Methoden mit dem Namen gefunden werden, bestimmen die Parametertypen im Request, welche davon ausgeführt wird. Dadurch funktioniert das normale Überladen in den Handler-Klassen.

Das Resultat der Ausführung der Methoden wird vom XML-RPC-Server codiert und als Java-Object (das könnte wieder ein Vector von Objecten sein!) zum Client zurückgeschickt. Dieses Resultat kann zum erwarteten Typ gecastet und dann vom Client normal verwendet werden. Ist es nicht möglich, eine Klasse mit einer entsprechend genannten Methode mit passender Signatur zu finden, wird eine XmlRpcException zum Client zurückgeschickt. Das verhindert, daß der Client eine Methode oder einen Handler aufruft, die bzw. der nicht existiert, oder falsche Parameter sendet.

All das geschieht durch ein paar zusätzliche Zeilen Java-Code. Die Klasse XmlRpcException muß genau wie die Klasse java.io.IOException importiert werden; die letztere wird ausgelöst, wenn bei der Kommunikation zwischen Server und Client Fehler auftreten. Dann wird die Klasse Vector hinzugefügt und instantiiert, wobei ein einzelner String angehängt wird. Anschließend können Sie die Methode execute( ) mit dem Namen des Handlers, dem der Methode und ihren Parametern aufrufen. Das Resultat des Aufrufs wird zu String gecastet, und dieser wird auf dem Bildschirm ausgegeben. In diesem Beispiel läuft der XML-RPC-Server auf dem lokalen Rechner an Port 8585:

package javaxml2;

import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Vector;

import helma.xmlrpc.XmlRpc;
import helma.xmlrpc.XmlRpcClient;
import helma.xmlrpc.XmlRpcException;

public class HelloClient {
 
    public static void main(String args[]) {
        if (args.length < 1) {
            System.out.println(
                "Benutzung: java HelloClient [Ihr Name]");
            System.exit(-1);
        }
        
        try {
            // Benutze den Apache Xerces-SAX-Treiber
            XmlRpc.setDriver("org.apache.xerces.parsers.SAXParser");
            
            // Angeben des Servers
            XmlRpcClient client = 
                new XmlRpcClient("http://localhost:8585/");  
                
            // Erzeugen des Requests            Vector params = new Vector(  );                        params.addElement(args[0]);            // Ausführen des Requests und Ausgeben des Resultats            String result =                (String)client.execute("hello.sayHello", params);            System.out.println("Antwort vom Server: " + result);                          
          
        } catch (ClassNotFoundException e) {
            System.out.println("Konnte den SAX-Treiber nicht finden");
        } catch (MalformedURLException e) {
            System.out.println(
                "Falsche URL für den XML-RPC-Server: " + 
                e.getMessage(  ));
        } catch (XmlRpcException e) {            System.out.println("XML-RPC Exception: " + e.getMessage(  ));        } catch (IOException e) {            System.out.println("IO Exception: " + e.getMessage(  ));        }        
    } 
}

Das reicht aus, um den Code in unserem Beispiel arbeiten zu lassen. Nun können Sie es kompilieren und eine Shell öffnen, in der das Programm abläuft.

Sprich mit mir

Die XML-RPC-Klassen und die Klassen des Beispiels müssen sich im Klassenpfad Ihrer Umgebung befinden. Außerdem muß sich der Apache Xerces oder welcher SAX-Treiber auch benutzt wird, ebenfalls im Klassenpfad befinden. Da die Beispiele diese Klassen für das Parsen benutzen, müssen sie darauf zugreifen können. Wenn das alles zutrifft, können Sie die Klasse HelloServer unter Angabe einer Portnummer starten. Unter Windows sollte das Kommando start benutzt werden, um den Server in einem separaten Prozeß zu starten:

c:\javaxml2\build>start java javaxml2.HelloServer 8585
Starten des XML-RPC Servers...
HelloHandler-Klasse an "hello" gebunden
Warte auf Requests...

Unter Unix benutzen Sie das Hintergrundverarbeitungskommando (&), um den Client in derselben Shell starten zu können (oder öffnen Sie eine andere Shell mit der gleichen Umgebung für den Start des Clients):

$ java javaxml2.HelloServer &
Starten des XML-RPC Servers...
HelloHandler-Klasse an "hello" gebunden
Warte auf Requests...

Nun können Sie den Client starten, indem Sie Ihren Namen als Kommandozeilen-Parameter angeben. Sie sollten schnell eine Antwort sehen (ähnlich der in Beispiel 11-5 gezeigten), wenn HelloServer den Request des Clients empfängt und das Resultat der Methode sayHello( ) zurückliefert, das der Client dann ausgibt.

Beispiel 11-5: Ausführen der Klasse HelloClient

$ java javaxml2.HelloClient Leigh
Antwort vom Server: Hello Leigh

Sie haben nun XML-RPC in Aktion gesehen. Das ist sicherlich kein auch nur annähernd nützliches Beispiel. Es sollte Ihnen aber einen Eindruck von den Grundlagen vermittelt und die Einfachheit der Programmierung von XML-RPC-Servern und -Clients unter Java verdeutlich haben. Mit diesen Grundlagen können wir uns nun einem etwas realistischeren Beispiel zuwenden. Im nächsten Abschnitt wird ein leistungsfähigerer Server aufgebaut und gezeigt, wie XML-RPC-Handler oft aussehen. Anschließend zeige ich Ihnen, wie man einen Client (ähnlich HelloClient) erzeugt, der zum Test des Servers benutzt wird.

Dem Server die Last aufbürden

Das »hello«-Beispiel war für die Demonstration der Benutzung von XML-RPC von Java so nützlich, wie es unrealistisch war. Zusätzlich zu der generellen Einfachheit ist der Server nicht sehr flexibel, und der Handler zeigt auch nicht, wie ein praktischer XML-RPC-Handler operieren könnte. Hier versuche ich, Ihnen ein Beispiel für die Benutzung von XML-RPC in einer Produktionsumgebung zu geben. Dazu werden wir die Nützlichkeit des Handlers erweitern und die Benutzbarkeit des Servers verbessern. Wenn es auch kein Code ist, den Sie zu eigenen Projekten hinzufügen können, zeigt dieses Beispiel doch, wie nützlich XML-RPC sein kann und wie man Anwendungen baut, die von XML-RPC profitieren können, dadurch aber nicht eingeschränkt werden.

Ein Shared-Handler

Die Klasse HelloHandler war einfach, aber in einer praktischen Anwendung nutzlos. Bei den meisten XML-RPC Anwendungsfällen sieht das Szenario so aus, daß Events auf einem Server auftreten, der mehr für die Durchführung komplexer Aufgaben geeignet ist, während ein schlanker Client bei ihm die Ausführung gewisser Prozeduren anfordert und die zurückgegebenen Resultate benutzt. Zusätzlich kann es so sein, daß ein Teil oder sogar alle Berechnungen, die zur Erfüllung eines Requests durchgeführt werden müssen, im voraus ausgeführt werden können. Mit anderen Worten: Die Handler-Klasse kann Aufgaben ausführen und sicherstellen, daß die Resultate verfügbar sind, wenn ein Methodenaufruf erfolgt. Einem Java-Programmierer sollten jetzt Threads und Shared-Instance-Daten einfallen. Hier werden diese Prinzipien an einer sehr einfachen Scheduler-Klasse demonstriert.

Der Scheduler soll es Clients erlauben, Events hinzuzufügen und zu entfernen. Clients können dann beim Server eine Liste aller Events in der Warteschlange erfragen. Um das praktikabler zu gestalten (und um eine Aufgabe zu haben, die der Server später ausführen kann), liefert die Abfrage der Events diese in der Reihenfolge, in der sie zeitlich aufgetreten sind. Ein Event ist in diesem Beispiel einfach ein String, der den Namen des Events darstellt, und ein Zeitstempel für den Event (als java.util.Date). Obwohl das keine vollständige Scheduler-Implementierung ist, kann es doch demonstrieren, wie der Server im Hintergrund Arbeit für die Clients leistet.

Als erstes müssen Sie die Methoden addEvent( ) und removeEvent( ) schreiben. Da beide vom Client getriggerte Events darstellen, ist an beiden nichts Bemerkenswertes. Sie müssen sich jedoch Gedanken über die Art und Weise der Speicherung der Events in der Klasse Scheduler machen. Obwohl der XML-RPC-Server diese Klasse instantiieren wird und diese Instanz dann für die Beantwortung aller XML-RPC-Aufrufe auf dem Server benutzt wird, ist es möglich und sogar wahrscheinlich, daß andere Klassen oder sogar XML-RPC-Server mit dem Scheduler interagieren. Wenn der Scheduler eine Liste von Events als Membervariable verwaltet, können mehrere Instanzen nicht untereinander auf ihre jeweiligen Daten zugreifen und sie teilen.

Um dieses Problem in diesem Beispiel zu lösen, wird der Speicher für die Events als static deklariert, so daß alle Instanzen der Klasse Scheduler darauf zugreifen können. Um einen Namen und einen Wert immer zusammenhängend zu speichern, scheint eine Hashtable geeignet zu sein, da sie die Speicherung von Schlüssel/Wert-Paaren gestattet. Zusätzlich zu der Hashtable speichert die Klasse die Namen noch in einem Vector. Obwohl das zusätzlichen Speicherplatz verbraucht (und Speicher in der JVM), kann der Vector sortieren und muß sich nicht mit der Sortierung einer Hashtable abplagen; der Vorteil ist, daß man zwei Namen im Vector einfach tauschen kann (ein Tausch) und die Zeitstempel in der Hashtable nicht auch tauschen muß (zweimal tauschen). Mit dieser Information sind Sie nun in der Lage, das Skelett der Klasse zu programmieren und diese zwei Methoden zum Hinzufügen und Löschen von Events einzufügen. Die Variablen zum Speichern von Events werden ebenfalls jetzt angelegt, die Mechanismen zum Sortieren und Auslesen von Events dagegen folgen später. Beispiel 11-6 zeigt das Listing für den Handler.

Beispiel 11-6: Die Klasse Scheduler

package javaxml2;

import java.util.Date;
import java.util.Hashtable;
import java.util.Vector;

public class Scheduler {
    
    /** Liste der Event-Namen (für die Sortierung) */
    private static Vector events = new Vector(  );
    
    /** Event-Details (Name, Zeitstempel) */
    private static Hashtable eventDetails = new Hashtable(  );    

    public Scheduler(  ) {
    }

    public boolean addEvent(String eventName, Date eventTime) {      
        // Füge dieses Event zu der Liste hinzu
        if (!events.contains(eventName)) {
            events.addElement(eventName);
            eventDetails.put(eventName, eventTime);
        }
      
        return true;
    }
    
    public synchronized boolean removeEvent(String eventName) {
        events.remove(eventName);
        eventDetails.remove(eventName);
      
        return true;
    }
}

Die Methode addEvent( ) fügt den Namen eines Events zu beiden Speicherobjekten hinzu und zusätzlich die Zeit zu der Hashtable. Die Methode removeEvent( ) tut das Entgegengesetzte. Beide Methoden liefern einen Wert vom Typ boolean zurück. Obwohl in diesem Beispiel dieser Wert immer true ist, könnte man diesen Wert in komplexeren Implementierungen zur Anzeige aufgetretener Fehler benutzen.

Da Sie nun in der Lage sind, Events hinzuzufügen und zu löschen, brauchen Sie nun eine Methode, die eine Liste der Events liefert. Diese Methode liefert alle Events in der Liste, gleich von welchem Client oder von welcher Applikation sie hinzugefügt wurden. Das könnten also auch Events sein, die von anderen XML-RPC-Clients, einem anderen XML-RPC-Server, einer anderen Anwendung oder einer Stand-alone-Implementierung des Schedulers hinzugefügt wurden. Da diese Methode ein einziges Object als Resultat zurückliefern muß, könnte das ein Vector formatierter String-Werte sein, die den Namen eines Events und die Zeit seines Auftretens enthalten. In einer nützlicheren Variante könnte der Vector mit den Events oder eine andere Form der Events in einem typisierten Format (mit dem Datum als Date-Objekt etc.) zurückgegeben werden.

Diese Methode liefert eine Sicht auf die Daten, ohne dem Client die Möglichkeit zu geben, etwas an ihnen zu ändern. Um die Liste der Events zurückzugeben, benutzt die Methode das Event »store« und die Klasse java.text.SimpleDateFormat, die es erlaubt, Objekte der Klasse Date als Text zu formatieren. Indem über alle Events iteriert wird, wird eine String-Repräsentation jedes Events mit Namen und Zeitstempel erstellt und an den Vector angehängt, der das Resultat darstellt. Dieser wird schließlich an den Client zurückgegeben. Dazu werden die benötigte import-Anweisung und der zur Rückgabe der gespeicherten Events benötigte Code an den Code des Schedulers angehängt:

package javaxml2;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Hashtable;
import java.util.Vector;

public class Scheduler {

    // bereits existierende Methoden

    public Vector getListOfEvents(  ) {             Vector list = new Vector(  );        // Erzeugen einer Formatter-Instanz für Date        SimpleDateFormat fmt =             new SimpleDateFormat("hh:mm a MM/dd/yyyy");                // Alle Elemente zur Liste hinzufügen        for (int i=0; i<events.size(  ); i++) {            String eventName = (String)events.elementAt(i);            list.addElement("Event \"" + eventName +                             "\" fällig: " +                                                       fmt.format(                                (Date)eventDetails.get(eventName)));        }        return list;    }
}

Zu diesem Zeitpunkt könnten Sie diese Klasse ohne Probleme als XML-RPC-Handler benutzen. Jedoch war der Zweck dieses Beispiels zu zeigen, wie Arbeit vom Server erledigt wird, während der Client andere Aufgaben löst. Die Methode getListOfEvents( ) nimmt an, daß die Liste der Events (die Variable events vom Typ Vector) korrekt sortiert ist, wenn diese Methode aufgerufen wird und die Sortierung also bereits durchgeführt wurde. Bisher ist jedoch kein Code zum Sortieren geschrieben worden und, was noch wichtiger ist: Es gibt auch keinen Code, der die Sortierung auslöst. Weiterhin wird die Sortierung zeitaufwendiger, wenn mehr und mehr Events hinzugefügt werden. Der Client sollte nicht darauf warten müssen, daß die Sortierung abgeschlossen wird.

Es macht Sinn, eine Methode hinzuzufügen, die es der Klasse erlaubt, die Liste der Events zu sortieren. Der Einfachheit halber wird ein Bubblesort benutzt. Die Beschreibung von Sortieralgorithmen liegt außerhalb des Anspruchs dieses Buches, daher ist der Code hier ohne Erklärung abgedruckt. Am Ende der Methode ist der Vectorevents jedenfalls nach den Zeitstempeln der Events sortiert. Weitere Informationen zu diesem und anderen Sortieralgorithmen können Sie zum Beispiel im Buch Algorithms in Java von Robert Sedgewick und Tim Lindholm (Addison Wesley) finden. Der Algorithmus und die Methode zur Sortierung der Events sind hier dargestellt und müssen in den Code integriert werden:

package javaxml2;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

public class Scheduler {
    
    /** Liste der Event-Namen (für die Sortierung) */
    private static Vector events = new Vector(  );
    
    /** Event-Details (Name, Zeitstempel) */
    private static Hashtable eventDetails = new Hashtable(  ); 
    
    /** Flag zeigt an, ob Listeneinträge schon sortiert sind */    private static boolean eventsSorted;
    // bereits vorhandene Methoden

    private synchronized void sortEvents(  ) {                if (eventsSorted) {            return;        }        // Erzeugen eines Arrays der Events, wie sie vorliegen (unsortiert)        String[] eventNames = new String[events.size(  )];        events.copyInto(eventNames);        // Bubblesort des Arrays        String tmpName;        Date date1, date2;        for (int i=0; i<eventNames.length - 1; i++) {            for (int j=0; j<eventNames.length - i - 1; j++) {                // Vergleich der Zeitstempel zweier Events                 date1 = (Date)eventDetails.get(eventNames[j]);                date2 = (Date)eventDetails.get(eventNames[j+1]);                if (date1.compareTo(date2) > 0) {                    // Tauschen, wenn nötig                    tmpName = eventNames[j];                    eventNames[j] = eventNames[j+1];                    eventNames[j+1] = tmpName;                }            }        }        // Schreiben in einen neuen Vector (sortiert)        Vector sortedEvents = new Vector(  );        for (int i=0; i<eventNames.length; i++) {            sortedEvents.addElement(eventNames[i]);        }        // Aktualisieren der statischen Klassenvariable        events = sortedEvents;        eventsSorted = true;    }
}

Zusätzlich zu dem eigentlichen Algorithmus wird die Klasse java.util.Enumeration importiert und eine Member-Variable eventsSorted vom Typ boolean eingeführt. Dieses Flag erlaubt es, die Ausführung der Sortierung abzukürzen, wenn die Events bereits sortiert sind. Es ist zwar bisher kein Code geschrieben worden, der diese Variable aktualisiert, aber das ist einfach. Die Methode zur Sortierung zeigt bereits an ihrem Ende an, daß die Events sortiert sind. Der Konstruktor sollte diesen Wert anfangs auf true setzen und damit anzeigen, daß die Liste der Events sortiert ist. Nur wenn neue Events hinzugefügt werden, ist es möglich, daß die Liste in Unordnung gerät.

Deshalb muß in der Methode addEvents( ) dieses Flag auf den Wert false gesetzt werden. Das sagt der Klasse Scheduler, daß die Sortierung neu gestartet werden muß. Wenn die Methode getListOfEvents( ) aufgerufen wird, werden die Events sortiert sein und können dann abgefragt werden. Sie müssen nun im Konstruktor und in der Methode zum Hinzufügen von Events noch Code hinzufügen, der den Wert des Flags entsprechend ändert:

package javaxml2;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

public class Scheduler {

    public Scheduler(  ) {
        eventsSorted = true;
    }

    public boolean addEvent(String eventName, Date eventTime) {      
        // Füge dieses Event zu der Liste hinzu
        if (!events.contains(eventName)) {
            events.addElement(eventName);
            eventDetails.put(eventName, eventTime);
            eventsSorted = false;            
        }
      
        return true;
    }

    // Andere Methoden
}

Sie müssen keine Änderungen an der Methode removeEvent( ) vornehmen, da das Entfernen eines Eintrags die Sortierung der Elemente nicht beeinträchtigt. Wenn Sie den Client zugunsten anderer Aufgaben entlasten möchten, ist ein Thread, der die Events sortiert, der ideale Mechanismus für die serverseitige Verarbeitung. Wird solch ein Thread innerhalb der JVM gestartet, kann der Client weiterarbeiten, ohne darauf warten zu müssen, daß der Thread beendet wird. Das ist besonders in einer Multithreaded-Umgebung wichtig, in der Synchronisation und Threads, die auf bestimmte Objekt-Locks warten, genutzt werden.

Im vorliegenden Fall wurden solche Threading-Issues ausgeklammert, es sollte aber relativ einfach sein, entsprechenden Code nachzurüsten. Es wird zunächst eine innere Klasse, die von Thread abgeleitet wird, hinzugefügt. Diese tut nichts weiter, als die Methode sortEvents( ) aufzurufen. Dann wird zur Methode addEvents( ) Code hinzugefügt, der den Thread erzeugt und startet, wenn Events hinzugefügt werden. Nun löst jedes hinzugefügte Event eine Neusortierung der Events aus. Der Client jedoch kann weiterarbeiten (und zum Beispiel neue Events hinzufügen, was wiederum neue Threads zur Sortierung starten würde). Wenn der Client eine Liste der Events anfordert, sollte er eine sortierte Liste als Antwort erhalten. Das alles geschieht, ohne daß der Client darauf warten oder selbst Rechenzeit dafür aufwenden muß. Das Hinzufügen sowohl einer inneren Klasse zur Sortierung als auch des Codes, der diesen Thread in der Methode addEvents( ) startet, runden die Klasse Scheduler ab und sind hier zu sehen:

package javaxml2;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

public class Scheduler {

    // Bereits vorhandene Variablen und Methoden

    public boolean addEvent(String eventName, Date eventTime) {      
        // Füge dieses Event zu der Liste hinzu
        if (!events.contains(eventName)) {
            events.addElement(eventName);
            eventDetails.put(eventName, eventTime);
            eventsSorted = false;
            
            // Starten des Sortier-Threads auf dem Server            SortEventsThread sorter = new SortEventsThread(  );            sorter.start(  );                        
        }
      
        return true;
    }

    class SortEventsThread extends Thread {        public void run(  ) {            sortEvents(  );        }        }
}

Wenn Sie den modifizierten Sourcecode kompilieren, erhalten Sie einen mit Threads arbeitenden Scheduler, der die arbeitsaufwendige Aufgabe des Sortierens auf dem Server erledigt und es Clients erlaubt, ungestört weiterzuarbeiten, während im Hintergrund sortiert wird. Das ist immer noch ein einfaches Beispiel für die richtige Benutzung einer Handler-Klasse, aber es führt die Konzepte der Verteilung von Ressourcen und der möglichst häufigen Übertragung von Arbeiten auf den Server ein. Um diese etwas fortgeschrittenere Handler-Klasse zu ergänzen, werde ich als nächstes die Entwicklung eines etwas fortgeschritteneren XML-RPC-Servers zeigen.

Ein konfigurierbarer Server

Die XML-RPC-Server-Klasse braucht immer noch ein wenig Bearbeitung. Die jetzige Version erfordert immer noch das spezifische Hinzufügen von Handler-Klassen im Code. Das bedeutet, daß das Hinzufügen neuer Handler-Klassen Programmierung und das Neukompilieren der Klasse nach sich zieht. Das ist nicht nur unter dem Gesichtspunkt des Software-Engineerings (Änderungskontrolle) unerwünscht, sondern außerdem noch langweilig und zeitaufwendig. Das Holen der neuesten Version aus einem Versionsmanagement-System, das Einfügen der Änderungen in den Quelltext und das Testen der geänderten Klasse, nur um ein oder zwei neue Handler hinzuzufügen, ist unpraktisch. Sie werden dadurch keine Freunde im Management gewinnen. Vorzuziehen wäre in diesem Fall ein robuster Server, der entsprechende Informationen aus einer Konfigurationsdatei lesen kann. Wir werden jetzt einen schlanken Server bauen, der das beherrscht.

Um zu beginnen, erstellen wir zunächst eine neue Server-Klasse. Sie können dazu entweder wieder von neuem beginnen oder einige Teile aus der früher in diesem Kapitel vorgestellten Klasse HelloServer kopieren. Beginnend mit dem Framework, fügen Sie die entsprechenden import-Anweisungen hinzu und instantiieren den Server ähnlich wie in dem Beispiel früher in diesem Kapitel. Diesmal lassen Sie jedoch Code zum Hinzufügen etwaiger Handler-Klassen weg, da es eine Helper-Methode geben wird, die die benötigten Informationen aus einer Datei liest. Eine Änderung im Vergleich zur früheren Version besteht darin, daß diese Klasse einen zusätzlichen Kommandozeilen-Parameter benötigt, der den Namen der entsprechenden Datei darstellt. Der Server wird diese Datei mittels der Methoden lesen, die ich später vorstellen werde, und fügt Handler zum Server hinzu. Die Klasse LightweightXmlRPcServer, die weiterhin die Helper-Klasse WebServer verwendet, können Sie mit dem in Beispiel 11-7 gezeigten Quellcode schreiben.

Beispiel 11-7: Ein wiederverwendbarer XML-RPC-Server

package javaxml2;

import java.io.IOException;

import helma.xmlrpc.XmlRpc;
import helma.xmlrpc.WebServer;

public class LightweightXmlRpcServer {
  
    /** Die Utility-Klasse für den XML-RPC-Server  */
    private WebServer server;
    
    /** Port-Nummer zum Lauschen */
    private int port;
    
    /** Zu benutzende Konfigurationsdatei */
    private String configFile;

    public LightweightXmlRpcServer(int port, String configFile) {
        this.port = port;
        this.configFile = configFile;
    }

    public void start(  ) throws IOException {
        try {
            // Benutze den Apache Xerces-SAX-Parser
            XmlRpc.setDriver("org.apache.xerces.parsers.SAXParser");

            System.out.println("Start des XML-RPC-Servers...");
            server = new WebServer(port);                      
            
            // Registrieren der Handler
            
        } catch (ClassNotFoundException e) {
            throw new IOException("Fehler beim Laden des SAX-Parsers: " + 
                e.getMessage(  ));
        }         
    }   

    public static void main(String[] args) {
      
        if (args.length < 2) {
            System.out.println(
                "Benutzung: " +
                "java com.oreilly.xml.LightweightXmlRpcServer " +
                "[port] [configFile]");
            System.exit(-1);
        }
        
        LightweightXmlRpcServer server =
            new LightweightXmlRpcServer(Integer.parseInt(args[0]),
                                        args[1]);   

        try {
            // Starten des Servers
            server.start(  );
        } catch (IOException e) {
            System.out.println(e.getMessage(  ));
        }                               
    }
}

Hier geschieht nichts Bemerkenswertes. Der Code sorgt dafür, daß die benötigten Parameter übergeben werden, und startet dann den Server am angegebenen Port. Jetzt ist die Zeit gekommen, Methoden hinzuzufügen, die die Handler aus der Datei lesen und sie beim Server registrieren.

Da jeder Handler einen Namen und eine zugeordnete Klasse benötigt, können Sie eine Konfigurationsdatei erzeugen, die genau diese Informationen enthält. Unter Java ist es einfach, eine Klasse mit ihrem Package- und Klassennamen zu laden und zu instantiieren. Damit kann ein neuer Handler einfach durch zwei Textwerte repräsentiert werden. In der Datei können Sie sowohl die ursprüngliche Klasse HelloHandler als auch die neue Klasse Scheduler hinzufügen. Da Sie den Parser ebenfalls schreiben, ist es sicher, einfach zu entscheiden, daß Kommas Einträge abgrenzen und Hash-Zeichen (#) Kommentare einleiten. Eigentlich können Sie auch jedes andere Format benutzen, solange sichergestellt ist, daß Sie Code schreiben, der die entsprechenden Konventionen beim Parsen der Datei benutzt.

Vielleicht sind Sie überrascht, daß hier kein XML-Dateiformat benutzt wird. Dafür gibt es mehrere Gründe. Erstens werde ich mich im nächsten Kapitel mit SOAP beschäftigen, das durch und durch XML benutzt. Die Benutzung eines anderen Formats an dieser Stelle unterstreicht die Unterschiede zwischen den beiden Methodiken. Zweitens sind Sie sicher an diesem Punkt darauf vorbereitet, Ihren eigenen XML-Parser zu schreiben, daher ist diese Aufgabe eine gute Übung. Drittens bin ich Realist; Sie werden erstaunt sein, wie oft es vorkommt, daß »XML-Frameworks« und »XML-Anwendungen« Nicht-XML-Formate benutzen. Gewöhnen Sie sich daran, es wird immer mal wieder geschehen.

Erzeugen Sie nun die in Beispiel 11-8 gezeigte Konfigurationsdatei, die die Klasse HelloHandler unter dem Identifier »hello« und die Klasse Scheduler unter dem Identifier »scheduler« registriert, und speichern Sie sie unter xmlrpc.conf.

Beispiel 11-8: XML-RPC-Konfigurationsdatei

# Hello Handler: sayHello(  )
hello,javaxml2.HelloHandler

# Scheduler: addEvent(), removeEvent(), getEvents(  )
scheduler,javaxml2.Scheduler

Zu Dokumentationszwecken habe ich die für jeden Handler verfügbaren Methoden als Kommentare beigefügt. Damit wissen andere, die später Änderungen an der Datei vornehmen wollen, welche Methoden für jeden Handler verfügbar sind.

Die I/O-Klassen von Java machen das Laden einer Datei und das Lesen ihres Inhaltes einfach. Es ist simpel, eine Helper-Methode zu schreiben, die den Inhalt einer solchen Datei liest und die Wertepaare in eine Hashtable schreibt. Diese Tabelle kann dann an eine weitere Methode übergeben werden, die die Handler lädt und registriert. Die Methode in diesem Beispiel verzichtet auf eine ausführliche Fehlerbehandlung, wie sie zum Beispiel in einem realen Server erfolgen müßte, und ignoriert einfach jede Zeile ohne ein Paar durch Komma getrennte Werte.

Es ist leicht, Code zur Fehlerbehandlung hinzuzufügen, wenn das vorgestellte Fragment in eigenen Anwendungen benutzt werden soll. Wird eine Zeile mit einem Wertepaar gefunden, wird diese aufgebrochen, und der Identifier und der Klassenname werden als ein Eintrag in der Hashtable gespeichert. Fügen Sie nun die neuen import-Anweisungen für die benötigten Utility-Klassen und die zwei neuen Methoden getHandlers( ) zu der Klasse LightweightServer hinzu:

package javaxml2;

import java.io.BufferedReader;import java.io.FileReader;
import java.io.IOException;
import java.util.Hashtable;

import helma.xmlrpc.XmlRpc;
import helma.xmlrpc.WebServer;

public class LightweightXmlRpcServer {

    // Bereits vorhandene Methoden

    private Hashtable getHandlers(  ) throws IOException {        Hashtable handlers = new Hashtable(  );        BufferedReader reader =             new BufferedReader(new FileReader(configFile));        String line = null;        while ((line = reader.readLine(  )) != null) {            // Die Syntax ist "handlerName, handlerClass"            int comma;            // Überspringe Kommentare            if (line.startsWith("#")) {                continue;            }            // Überspringe leere oder unsinnige Zeilen                        if ((comma = line.indexOf(",")) < 2) {                continue;            }            // Hinzufügen von Klasse und Name des Handlers            handlers.put(line.substring(0, comma),                          line.substring(comma+1));        }        return handlers;            }
}

Anstatt hier Code einzufügen, der das Ergebnis der Methode sichert, können Sie das Resultat als Eingabe für eine andere Methode verwenden, die über die Hashtable iteriert und jeden Handler beim Server registriert. Der Code dafür ist nicht kompliziert; die einzige bemerkenswerte Sache dabei ist, daß die Methode addHandler( ) des WebServers eine Instanz einer Klasse als Parameter verlangt. Daher muß der Code den Namen der zu registrierenden Klasse aus der Hashtable holen, sie mittels Class.forName( ) in die JVM laden und dann mittels newInstance( ) instantiieren.

Dies ist die Vorgehensweise in Klassenladern und anderen dynamischen Anwendungen in Java. Sie ist Ihnen eventuell nicht vertraut, wenn Sie erst seit kurzem mit Java arbeiten oder noch nie Klassen dynamisch über ihren Namen instantiieren mußten. Wenn die Klasse auf diese Weise angelegt wurde, wird die Methode addHandler( ) mit dem Identifier und der entstandenen Instanz als Parameter aufgerufen, und es wird mit der Iteration fortgefahren. Sobald alle Inhalte der Hashtable geladen sind, ist der Server zur Arbeit bereit. Ich habe die Klasse Enumeration benutzt, um durch die Schlüssel der Hashtable zu iterieren, daher muß auch diese import-Anweisung in den Quellcode eingefügt werden:

package javaxml2;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Hashtable;

import helma.xmlrpc.XmlRpc;
import helma.xmlrpc.WebServer;

public class LightweightXmlRpcServer {

    // Bereits existierende Methoden

    private void registerHandlers(Hashtable handlers) {                Enumeration handlerNames = handlers.keys(  );        // Schleife über alle angeforderten Handler        while (handlerNames.hasMoreElements(  )) {            String handlerName = (String)handlerNames.nextElement(  );            String handlerClass = (String)handlers.get(handlerName);            // Hinzufügen eines Handlers zum Server            try {                server.addHandler(handlerName,                     Class.forName(handlerClass).newInstance(  ));                System.out.println("Registriere Handler " + handlerName +                                   " als Klasse " + handlerClass);            } catch (Exception e) {                System.out.println("Fehlgeschlagen: Registrierung von Handler " +                                    handlerName + " als Klasse " +                                    handlerClass);            }        }    }
}

Das ist einfach eine Ergänzung zu der Methode getHandlers( ); tatsächlich nimmt sie das Resultat dieser Methode als Eingabe entgegen. Sie benutzt die Strings in der Hashtable und registriert sie. Nun läuft der Server und hat alle Handler, die in der Konfigurationsdatei standen, geladen und für Aufrufe verfügbar.

Sie könnten diese Methoden natürlich auch zu einer umfangreicheren Methode verschmelzen. Der Zweck, den die Methoden erfüllen, ist jedoch deutlich unterschiedlich: Während die eine, getHandlers( ), das Parsen einer Datei erledigt, ist die andere, registerHandlers( ), mit der Registrierung von Handlern beschäftigt, über die entsprechende Informationen vorliegen. Mit dieser Aufteilung können Sie die Methode des Parsens der Konfigurationsdatei ändern (oder die entsprechenden Informationen aus einer Datenbank oder einem noch anders gearteten Medium extrahieren), ohne sich Gedanken über die Art und Weise der Registrierung der Handler machen zu müssen.

Wenn Sie diese beiden Methoden hinzugefügt haben, müssen Sie sie nur noch in der Methode start( ) der Server-Klasse aufrufen:

    public void start(  ) throws IOException {
        try {
            // Benutze den Apache Xerces-SAX-Parser
            XmlRpc.setDriver("org.apache.xerces.parsers.SAXParser");

            System.out.println("Starten des XML-RPC-Servers...");
            server = new WebServer(port);                      
            
            // Registrieren der Handler            registerHandlers(getHandlers(  ));
            
        } catch (ClassNotFoundException e) {
            throw new IOException("Fehler beim Laden des SAX-Parsers: " + 
                e.getMessage(  ));
        }         
    }

Nach dem Kompilieren und der Erstellung der Konfigurationsdatei ist der Server einsatzbereit.

Ein nützlicher Client

Der neue Client beinhaltet keine neuen Techniken oder Konzepte; genau so einfach, wie die Klasse HelloClient war, ist auch die Klasse SchedulerClient. Sie benötigt den Start eines XML-RPC-Clients, ruft einige Handler-Methoden auf und gibt die Resultate dieser Aufrufe auf dem Bildschirm aus. Der komplette Code für diesen Client ist hier gezeigt. Kommentare erklären, was passiert, und da alles hier bereits bekannt ist, können Sie den Code in Beispiel 11-9 einfach mittels eines Editors eingeben, speichern und anschließend kompilieren.

Beispiel 11-9: Die Klasse SchedulerClient

package javaxml2;

import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import helma.xmlrpc.XmlRpc;
import helma.xmlrpc.XmlRpcClient;
import helma.xmlrpc.XmlRpcException;

public class SchedulerClient {

    public static void addEvents(XmlRpcClient client) 
        throws XmlRpcException, IOException {
          
        System.out.println("\nHinzufügen von Events...\n");          
          
        // Parameter für Events
        Vector params = new Vector(  );
        
        // Hinzufügen eines Events für den nächsten Monat
        params.addElement("Korrekturlesen der Endfassung");
            
        Calendar cal = Calendar.getInstance(  );
        cal.add(Calendar.MONTH, 1);
        params.addElement(cal.getTime(  ));
            
        // Hinzufügen des Events
        if (((Boolean)client.execute("scheduler.addEvent", params))
                            .booleanValue(  )) {
            System.out.println("Event hinzugefügt.");
        } else {
            System.out.println("Konnte das Event nicht hinzufügen.");
        }        
            
        // Hinzufügen eines Events für morgen
        params.clear(  );
        params.addElement("Enfassung abschicken");
            
        cal = Calendar.getInstance(  );
        cal.add(Calendar.DAY_OF_MONTH, 1);
        params.addElement(cal.getTime(  ));
        
        // Hinzufügen des Events
        if (((Boolean)client.execute("scheduler.addEvent", params))
                            .booleanValue(  )) {
            System.out.println("Event hinzugefügt.");
        } else {
            System.out.println("Konnte Event nicht hinzufügen.");
        }        
            
    }

    public static void listEvents(XmlRpcClient client) 
        throws XmlRpcException, IOException {  
          
        System.out.println("\nListe der Events...\n");                

        // Holen des Events vom Scheduler
        Vector params = new Vector(  );
        Vector events = 
            (Vector)client.execute("scheduler.getListOfEvents", params);
        for (int i=0; i<events.size(  ); i++) {
            System.out.println((String)events.elementAt(i));
        }
    }

    public static void main(String args[]) {
        
        try {
            // Benutze den Apache Xerces-SAX-Parser
            XmlRpc.setDriver("org.apache.xerces.parsers.SAXParser");
          
            // Verbinden zum Server
            XmlRpcClient client = 
                new XmlRpcClient("http://localhost:8585/");                                         
              
            // Ein paar Events hinzufügen
            addEvents(client);                        
            
            // Events auflisten
            listEvents(client);            
        
        } catch (Exception e) {
            System.out.println(e.getMessage(  ));
        }        
    }   
}

Wenn Sie den Code eingeben, werden Sie bemerken, daß die Events in umgekehrter Reihenfolge ihres Auftretens in die Liste eingefügt werden. Der Server sortiert sie anschließend mit der Methode sortEvents( ), um die korrekte Sortierung der Ergebnisse zu erleichtern, wenn die Methode getListOfEvents( ) aufgerufen wird. Der Server kümmert sich als nächstes um die Sortierung.

Sprich mit mir (noch einmal)

Wenn Sie den Code für Handler, Server und Client eingegeben haben, kompilieren Sie alle Quelldateien. Sie müssen ebenfalls eine Konfigurationsdatei erstellen, die diejenigen Handler auflistet, die bei dem Server registriert werden sollen, der im Abschnitt »Ein konfigurierbarer Server« früher in diesem Kapitel vorgestellt wurde. Zuerst müssen Sie den XML-RPC-Server als separaten Prozeß starten:

c:\javaxml2\build>start java javaxml2.LightweightXmlRpcServer 8585
                        c:\javaxml2\ch11\conf\xmlrpc.conf

Unter Unix schreiben Sie:

$ java javaxml2.LightweightServer 8585 conf/xmlrpc.conf &

Der Server sollte nun melden, daß die Handler der Konfigurationsdatei unter den angegebenen Namen registriert wurden:

Starten des XML-RPC-Servers...
Registriere Handler scheduler als Klasse javaxml2.Scheduler
Registriere Handler hello als Klasse javaxml2.HelloHandler

Wenn Sie den vorigen XML-RPC-Server HelloServer nicht beendet haben, werden Sie eine Fehlermeldung sehen, die besagt, daß es nicht möglich ist, einen Server an einem bereits belegten Port zu starten. Stellen Sie daher sicher, daß der HelloServer beendet ist, bevor Sie den LightweightXmlRpcServer starten.

Zum Schluß starten Sie den Client und können die Resultate begutachten:

$ java javaxml2.SchedulerClient

Hinzufügen von Events...

Event hinzugefügt.
Event hinzugefügt.

Liste der Events...

Event "Endfassung abschicken" fällig: 10:55 AM 05/09/2001
Event "Korrekturlesen der Endfassung" fällig: 10:55 AM 06/08/2001

Sie sollten keine auffallende Pause zwischen dem Hinzufügen von Events und der Ausgabe der Liste bemerken können, und doch sortiert der Server die Events in einem separaten Thread in der JVM (übrigens ist Bubblesort nicht der schnellste Sortieralgorithmus). Sie haben nun Ihre erste nützliche XML-RPC-Anwendung geschrieben!

Die wirkliche Welt

Ich werde dieses Kapitel mit einem kurzen Ausblick auf wichtige Details der Benutzung von XML-RPC in der Realität beschließen. Das wird mein Konzept weiterführen, XML nicht deshalb zu benutzen, weil es die neueste und coolste Technologie ist, sondern weil sie am besten geeignet ist, manche Probleme zu lösen. All das Wissen in diesem Buch, in den XML-Spezifikationen und in anderen Büchern zu diesem Thema wird nicht dafür sorgen, daß Ihre Anwendung so gut arbeitet, wie es möglich wäre, wenn Sie nicht wissen, wann und wie man XML und XML-RPC richtig einsetzt! Dieser Abschnitt beleuchtet einige der Gesichtspunkte, die bei der Benutzung von XML-RPC in den Blickpunkt rücken.

Wo ist XML in XML-RPC?

Nachdem Sie dieses Kapitel bis hierher durchgearbeitet haben, werden Sie sicher erstaunt sein, daß Sie nie irgendwelchen SAX-, DOM- oder JDOM-Code schreiben mußten. Tatsächlich wurde so gut wie kein XML direkt benutzt. Der Grund dafür ist, daß die XML-RPC-Bibliotheken für die Codierung und Decodierung der zwischen Client und Server verschickten Requests verantwortlich sind. Obwohl hier kein Code erzeugt wurde, der XML direkt manipuliert, wird hier dennoch XML benutzt. Der einfache Request an die Methode sayHello( ) wurde tatsächlich in einen HTTP-Aufruf übersetzt, der wie Beispiel 11-10 aussieht.

Beispiel 11-10: XML für einen XML-RPC-Request

POST /RPC2 HTTP/1.1
User-Agent: Tomcat Web Server/3.1 Beta (Sun Solaris 2.6)
Host: newInstance.com
Content-Type: text/xml
Content-length: 234

<?xml version="1.0"?>
<methodCall>
  <methodName>hello.sayHello</methodName>
  <params>
    <param>
      <value><string>Brett</string></value>
    </param>
  </params>
</methodCall>

Die XML-RPC-Bibliotheken auf dem Server empfangen und decodieren es und versuchen, eine geeignete Handler-Methode zu finden. Anschließend wird die benötigte Java-Methode aufgerufen, und der Server codiert das Ergebnis wieder wie in Beispiel 11-11 dargestellt mittels XML.

Beispiel 11-11: Einer XML-RPC-Antwort zugrundeliegendes XML

HTTP/1.1 200 OK
Connection: close
Content-Type: text/xml
Content-Length: 149
Date: Mon, 11 Apr 2000 03:32:19 CST
Server: Tomcat Web Server/3.1 Beta-Sun Solaris 2.6

<?xml version="1.0"?>
<methodResponse>
  <params>
    <param>
      <value><string>Hello Brett</string></value>
    </param>
  </params>
</methodResponse>

All diese Kommunikation erfolgt, ohne daß Sie sich darüber Gedanken machen müssen.

Gemeinsam genutzte Instanzen

In diesen Beispielen habe ich statische Datenobjekte benutzt, um von mehreren Instanzen einer Klasse auf die Daten zugreifen zu können (Daten zu »sharen«). Jedoch gibt es auch Fälle, wo eine Instanz selbst so (shared) benutzt wird. Das muß nicht aus der Benutzung von XML-RPC resultieren, sondern kann auch passieren, weil die Klasse auf dem Server anderweitig benutzt wird. Zum Beispiel sagt das Design-Pattern Singleton in Java aus, daß nur eine Instanz einer Klasse erzeugt werden darf und diese dann von allen Anwendungen benutzt wird. Das wird normalerweise durch eine statische Methode getInstance( ) erreicht, anstatt ein Objekt zu instantiieren:

Scheduler scheduler;

// Holen der durch die Klasse Scheduler verwalteten einzigen Instanz
scheduler = Scheduler.getInstance(  );

// Hinzufügen eines Events für den aktuellen Zeitpunkt
scheduler.addEvent("Picnic", new Date(  ));

Um sicherzustellen, daß keine Klasse direkt eine Instanz der Klasse Scheduler erzeugt, werden die Konstruktoren als private oder protected deklariert. Während diese Vorgehensweise Clients zwingt, diesen Code zu benutzen, um Zugriff auf eine Instanz zu erlangen, kann es doch Verwirrung stiften, wenn die Klasse als XML-RPC-Handler benutzt wird. Denken Sie daran, daß die Registrierung eines Handlers immer die Instantiierung der Handler-Klasse vorausgesetzt hat. Jedoch benötigt die Klasse WebServer lediglich eine gültige Instanz als Parameter und nicht notwendigerweise eine neue Instanz. Der folgende Code ist ein Beispiel für eine völlig legitime Art, einen Handler hinzuzufügen:

WebServer server = new WebServer(8585);

// Erzeugen der Handler-Klasse
HelloHandler hello = new HelloHandler(  );
server.addHandler("hello", hello);

Der Server unterscheidet nicht zwischen beiden Methoden, solange die Handler-Klasse instantiiert ist, wenn sie der Methode addHandler( ) übergeben wird. Also können Sie eine kleine Änderung am Code vornehmen, wenn Sie eine Instanz der bereits beschriebenen Singleton-Klasse Scheduler registrieren möchten:

WebServer server = new WebServer(8585);

// Übergeben der Singleton-Instanz
server.addHandler("scheduler", Scheduler.getInstance(  ));

Dadurch wird die gemeinsam genutzte Instanz genau so übergeben, als ob die Klasse mit dem Schlüsselwort new instantiiert worden wäre, und bewahrt alle Informationen der Singleton-Klasse. Viele Klassen, die in Services wie XML-RPC benutzt werden, sind als Singletons ausgeführt, um statische Datenobjekte zu vermeiden, da es eine gemeinsam genutzte Instanz erlaubt, die Daten in normalen Member-Variablen zu speichern; eine einzige Instanz arbeitet dann mit diesen Member-Variablen für alle Client-Requests.

Servlet oder kein Servlet?

In letzter Zeit ist die Benutzung von Servlets als XML-RPC-Server immer populärer geworden. Einzelheiten über Servlets können Sie in Jason Hunters Java Servlet-Programmierung (O’Reilly Verlag) finden. Tatsächlich enthalten die XML-RPC-Java-Klassen, die Sie heruntergeladen haben, ein Servlet. Es ist sowohl erlaubt als auch verbreitet, ein Servlet in dieser Art und Weise zu benutzen, da es nichts anderes tut, als XML-RPC-Requests entgegenzunehmen. Es ist jedoch nicht immer die beste Idee.

Wenn Sie einen Rechner haben, der auch andere HTTP-Requests für Java-Tasks bearbeitet, ist eine Servlet-Engine eine gute Wahl, um die Details dieser Requests zu behandeln. In einem solchen Fall ist es eine gute Idee, den XML-RPC-Server als Servlet auszulegen. Ein Vorteil von XML-RPC ist aber die Möglichkeit, Handler-Klassen mit kompliziertem Code, der viel Rechenzeit benötigt, von anderen Teilen der Anwendung zu separieren. Die Klasse Scheduler könnte auf einem Server mit Klassen kombiniert werden, die komplizierte Indizierungen, algorithmische Modellierungen und möglicherweise graphische Transformationen ausführen.

Alle diese Funktionen sind für Client-Anwendungen sehr aufwendig. Das Hinzufügen einer Servlet-Engine, die auch andere Anwendungs-Requests außer den mit XML-RPC verbundenen akzeptiert, würde die für die Handler-Klassen zur Verfügung stehende Rechenzeit beträchtlich herabsetzen. In einem solchen Szenario sollten die einzigen Requests an den Server diejenigen sein, die für diese Handler-Klassen bestimmt sind.

In dem Fall, daß nur XML-RPC-Requests akzeptiert werden (wie oben beschrieben), ist es keine gute Idee, ein Servlet als XML-RPC-Server zu verwenden. Die benutzte Klasse WebServer ist klein, schlank und speziell für die Behandlung von XML-RPC-Requests über HTTP geschrieben. Eine Servlet-Engine ist so entwickelt, daß sie alle möglichen HTTP-Requests akzeptiert, und nicht im geringsten für die Behandlung von XML-RPC-Requests optimiert. Über einen gewissen Zeitraum hinweg betrachtet, werden Sie Performance-Einbußen der Servlet-Lösung gegenüber der Lösung mit der Klasse WebServer beobachten können. Wenn Sie keine gewichtigen Gründe dafür haben, ein Servlet für andere als XML-RPC betreffende Aufgaben zu benutzen, sollten Sie den schlanken XML-RPC-Server einsetzen, der genau für die Aufgaben im Umfeld von XML-RPC entworfen wurde.

Und was kommt jetzt?

Nachdem Sie jetzt einen Einblick in RPC und XML-RPC gewonnen haben, ist es Zeit für den nächsten logischen Schritt. Dieser Schritt heißt SOAP, das Simple Object Access Protocol (engl. für: einfaches Objektzugriffsprotokoll). SOAP baut auf XML-RPC auf und bietet Unterstützung für benutzerdefinierte Objekttypen, eine bessere Fehlerbehandlung und mehr Features. Außerdem ist es gerade sehr »in«. Im nächsten Kapitel werde ich die Grundlagen vorstellen – seien Sie also gewappnet.

1)
Zur Zeit unterstützt diese XML-RPC-Bibliothek SAX 2.0 nicht und implementiert auch nicht das Interface XMLReader. Da die Klasse SAXParser des Apache Xerces das Interface Parser aus SAX 1.0 und das Interface XMLReader aus SAX 2.0 implementiert, muß kein Code in den Beispielen geändert werden, wenn die Bibliotheken auf die Version SAX 2.0 aktualisiert werden. Werden jedoch Parser anderer Hersteller verwendet, kann es durchaus nötig werden, eine SAX 2.0-Klasse zu spezifizieren, wenn die XML-RPC-Bibliotheken auf die Benutzung von SAX 2.0 umgestellt werden.
2)
Auf einem Unix-Rechner müssen Sie als root eingeloggt sein, wenn Sie einen Service an einer Portnummer kleiner als 1024 starten möchten. Um diese Probleme zu umgehen, können Sie einen Port mit einer höheren Nummer benutzen, wie Beispiel 11-3 zeigt.