![]() |
|
|||||
Parameterübergabe bei getrenntem SpeicherDoch der wirkliche Unterschied zwischen lokalen und entfernten Methoden ist das Fehlen des gemeinsamen Kontexts. Die involvierten Rechner führen ihr eigenes Leben mit ihren eigenen Speicherbereichen. Stehen auf einer Maschine zum Beispiel statische Variablen jedem zur Verfügung, so ist dies bei entfernten Maschinen nicht der Fall. Ebenso gilt dies für Objekte, die von mehreren Partnern geteilt werden. Die Daten auf einer Maschine müssen also erst übertragen werden, und somit arbeitet der Server mit einer Kopie der Daten. Bei primitiven Daten ist das kein Thema, schwierig wird es erst bei Objektreferenzen. Mit der Referenz auf ein Objekt kann der andere Partner nichts anfangen. Aber mit der Übertragung der Objekte fangen wir uns zwei weitere Probleme ein.
Wenn die Daten übertragen werden, müssen sich die Partner zudem über das Austauschformat geeinigt haben. Die Daten müssen von beiden verstanden werden. Traditionell bieten sich zwei Verfahren an.
19.2 Nutzen von RMI bei Middleware-Lösungen
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 1. | Der Server stellt das entfernte Objekt mit der Funktion bereit. Die Funktion läuft im eigenen Adressraum, und der Server leitet Anfragen an diese Funktion weiter. |
Um entfernte Objekte mit ihren Methoden in Java-Programmen zu nutzen, müssen wir einige Schritte machen, die im Folgenden kurz skizziert werden. An den Schritten spiegelt sich der Programmieraufwand wieder:
| 1. | Wir geben eine entfernte Schnittstelle an, die die Methode(n) definiert. |
Vergleichen wir entfernte Objekte und ihre Methode, so fallen Gemeinsamkeiten ins Auge. Die Referenzen auf entfernte Objekte lassen sich wie gewohnt übertragen. Sie können als Parameter oder als Rückgabewert angegeben werden. Dabei ist es egal, ob die Methode mit den Parametern oder Rückgabewerten lokal oder entfernt sind. Die Unterschiede zu lokalen Objekten sind aber deutlicher. Da ein Client immer über eine entfernte Schnittstelle das Objekt repräsentiert, hat es nichts mit der tatsächlichen Implementierung zu tun und daher ist auch eine Typumwandlung unmöglich. Die einzige Umwandlung von einer entfernten Schnittstelle ist in Remote und noch eventuellen Obertypen von Remote. Damit ist auch deutlich, dass instanceof auch nur testen kann, ob das Objekt entfernt ist oder nicht; die echte Vererbung auf der Server-Seite bleibt verborgen.
Neben der reinen Java-Lösung RMI gibt es auf dem großen Land der Standards das komplexe CORBA. Im Gegensatz zu RMI definiert CORBA ein großes Framework für unterschiedliche Programmiersprachen. Die Definition von CORBA geht in das Jahr 1991, also vor RMI. Die OMG (Object Management Group) hat bei der reinen Java-Implementierung Sun vorgeworfen, einen zweiten Standard zu schaffen. Die Frage nach dem Sinn von RMI ist also erlaubt. Die Antwort liegt jedoch in der Einfachheit und Integration von RMI. In den letzen Jahren hat Sun jedoch RMI an den defacto Standard CORBA angepasst. Die Stellvertreterobjekte sprechen mittlerweile nicht nur das eigene Protokoll, sondern können sich auch mit dem Inter-ORB Protocol (IIOP) von CORBA unterhalten. Diese Lösung heißt RMI/IIOP (»RMI über IIOP«). Damit lässt sich auch über RMI eine Verbindung zwischen Java-Programmen und nicht Java-Programmen herstellen. Wollten wir auf IIOP verzichten, müsste die Übermittlung mit CORBA oder einem eigenen Protokoll erfolgen. Dann können wir aber nicht von der einfachen Nutzung profitieren und müssen uns mit Fragen wie Bit-Anzahl der Datentypen oder Byte-Ordnungen (Big-Endian/Litte-Endian) herumschlagen.
Damit der Client eine entfernte Methode nutzen kann, muss er ein Stellvertreterobjekt befragen. Dieses packt die Daten ein und übermittelt sie. Wir haben gesagt, dass diese Hilfsfunktionen automatisch generiert werden. Damit der Generator korrekten Quellcode für die Übertragung erstellen kann, ist eine Beschreibung nötig. Die Definition muss die Signatur eindeutig spezifizieren, und damit weiß der Client, wie die Funktion aussieht, die er aufrufen kann und der Server kann die Methode dann beschreiben. Normalerweise gibt es für die Spezifikation der entfernten Funktionen spezielle Beschreibungssprachen und auch CORBA verfolgt diesen Weg, doch bei RMI reicht es, ein Interface anzugeben, da in Java die Schnittstelle alles Wesentliche erfasst.
Listing 19.1 Adder.javaimport java.rmi.*;
public interface Adder extends Remote
{
public int add(int x, int y) throws RemoteException;
}
An diesem Beispiel können wir mehrere wichtige Eigenschaften der Schnittstelle ablesen:
| Die entfernte Schnittstelle ist öffentlich. Wenn sie nur paketsichtbar oder eingeschränkter ist, kann der Client die entfernte Methode nicht finden, wenn er danach verlangt. |
| Die eigene Schnittstelle erweitert die Schnittstelle Remote. Nur die Klassen, die Remote implementieren, können entfernte Methoden anbieten. Remote ist allerdings leer und damit eine Markierungsschnittstelle. |
| Die angebotenen Methoden können nicht beabsichtigte Fehler auslösen, zum Beispiel, wenn das Transportsystem zusammenbricht. Für diesen Fall muss jede Methode RemoteException in einer throws-Anweisung aufführen. |
| Eine entfernte Funktion darf Parameter besitzen. Sind dies primitive Werte, so werden diese einfach übertragen. Handelt es sich um Objekte, so müssen diese serialisierbar sein. |
Die Methoden des Servers werden letztendlich vom Client über die Stellvertreter genutzt. Der Server muss unterschiedliche Vorgaben erfüllen: Er muss eine spezielle Klasse erweitern, einen Konstruktor anbieten und die entfernte Schnittstelle implementieren. Dann kann ein Server-Objekt angemeldet werden. Das Serverobjekt für die Schnittstelle sieht dann wie folgt aus:
Listing 19.2 AdderImpl.javaimport java.rmi.*;
import java.rmi.server.*;
public class AdderImpl extends UnicastRemoteObject implements Adder
{
public AdderImpl() throws RemoteException
{
}
public int add( int x, int y ) throws RemoteException
{
return x + y;
}
}
Da die Klasse eine Implementierung der Schnittstelle ist, geben wie ihr die Endung Impl.
Zuerst fällt in der Implementierung auf, dass wir die Klasse UnicastRemoteObject erweitern. Sie liegt im Paket java.rmi.server und zeigt so die grobe Richtung für die Verwendung an.
public class AdderImpl extends UnicastRemoteObject implements Adder
{
...
}
Diese Klasse bietet Hilfe bei der Übertagung der Daten mittels Standard-TCP-Sockets an. Der Server kann so auf eingehende Anfragen reagieren und diese bearbeiten. Weiterhin zeigt UnicastRemoteObject an, dass ein Exemplar (nicht repliziert) unserer Klasse existieren soll.
Für den Konstruktor eines entfernten Objekts gelten zwei Eigenschaften: Wir müssen einen Standard-Konstruktor anbieten und dieser muss ebenso wie die Methoden RemoteException anzeigen.
public AdderImpl() throws RemoteException
{
}
Der Standard-Konstruktor ist notwendig, da eine Unterklasse genau diesen aufrufen möchte. Unser Konstruktor muss nichts machen. Er ruft aber automatisch den Konstruktor der Oberklasse auf, also den von UnicastRemoteObject. Wir haben schon beschrieben, dass er bei der Übertragung hilft. Wenn ein entferntes Objekt nun konstruiert wird, dann bindet er diesen Dienst an einen anonymen Port und horcht an einkommende Aufrufe. Wollten wir einen speziellen Port nutzen, müssten wir im Konstruktor unserer Unterklasse einen parametrisierten Konstruktor von UnicastRemoteObject aufrufen, der einen Port annimmt.
class java.rmi.server.UnicastRemoteObject extends RemoteServer |
| protected UnicastRemoteObject() throws RemoteException Erzeugt und exportiert ein neues UnicastRemoteObject und bindet es an einen unbekannten Port. Konnte es nicht exportiert werden, löst der Konstruktor eine RemoteException aus. |
| UnicastRemoteObject( int port ) throws RemoteException Erzeugt ein UnicastRemoteObject und bindet es an den angegeben Port. Ist dieser Null, so wird er vom System zugewiesen. |
Es kann passieren, dass eine entfernte Klasse schon von einer anderen Klasse erbt und wir wegen der fehlenden Mehrfachvererbung ein Problem bekommen. Wenn wir uns dafür entscheiden, keine Objekte über UnicastRemoteObject anzubieten, aber eine existierende Klasse trotzdem Anbieter sein möchte, so muss das Objekt im Konstruktor ein entferntes Objekt, welches Remote implementiert, mit UnicastRemoteObject.exportObject(Remote) anmelden.
class java.rmi.server.UnicastRemoteObject extends RemoteServer |
| static RemoteStub exportObject( Remote obj ) Exportiert das entfernte Objekt und macht es empfänglich für einkommende Aufrufe. Es wird ein willkürlicher Port verwendet. |
| static Remote exportObject( Remote obj, int port ) Wie exportObject(Remote), nur wird der angegebene Port und nicht der Standard Port 1099 verwendet. |
Beispiel Der Server implementiert die Schnittstelle Adder und registriert sich selbst über exportObject().
import java.rmi.*; |
Beispiel Die Erweiterung von UnicastRemoteObject ist nur eine Abkürzung für den Weg über exportObject(). Die Methode wird aber sowieso genommen, wie der Ausschnitt aus dem Quellcode für den Konstruktor zeigt.
protected UnicastRemoteObject() throws RemoteException { this(0); } protected UnicastRemoteObject(int port) throws RemoteException { this.port = port; exportObject((Remote)this, port); } |
Im nächsten Schritt müssen die Methoden der Schnittstelle implementiert werden. Es steht frei, andere Methoden anzugeben, die nicht in der Schnittstelle vorgegeben sind, doch diese sind dann natürlich nicht nach außen sichtbar.
public int add( int x, int y ) throws RemoteException { return x + y; }
Die Argumente und Rückgabewerte können von jedem beliebigen Datentyp sein. Bei primitiven Datentypen werden spezielle read()- und write()-Folgen generiert. Objekte müssen die Schnittstelle Serializable implementieren. Dann werden die lokalen Objekte als Kopie übertragen. Über die Serialisierung werden alle nicht statischen und nicht transienten Attribute übermittelt. Ist der Parameter wiederum instanceof Remote, dann wird dieser Verweis als einfache Referenz übergeben. In Wirklichkeit ist sie ein Verweis auf den Stellvertreter.
Entfernte Objekte erweitern oft die Klasse UnicastRemoteObjekt. Sie selbst ist jedoch eine Unterklasse von java.rmi.server.RemoteServer, eine abstrakte Klasse, die wiederum die abstrakte Klasse java.rmi.server.RemoteObject beerbt.
RemoteObject ist nichts anderes als ein verteiltes Objekt, welches die Schnittstellen Remote und Serializable implementiert. Da beides aber nur Markierungsschnittstellen sind, taucht keine ausprogrammierte Methode auf. RemoteObject ersetzt die Klasse Object für verteilte Objekte. Die Unterscheidung tritt bei den Methoden hashCode(), equals() und toString() auf. Ebenso implementiert RemoteObject die Methoden writeObject() und read Object(), damit die verteilten Objekte serialisiert werden können.
abstract class java.rmi.server RemoteObject implements Remote, Serializable |
| protected RemoteObject() Erzeugt remote-Objekt. |
| protected RemoteObject(RemoteRef newref) { Erzeugt remote-Objekt, welches mit angegebener remote-Referenz initialisiert ist. |
| RemoteRef getRef() Liefert remote-Referenz für das Objekt. |
| public static Remote toStub( Remote obj ) throws NoSuchObjectException Liefert den Stub für das remote-Objekt obj. Die Operation kann nur durchgeführt werden, wenn das Objekt schon exportiert wurde. Sonst wird eine NoSuchObjectException ausgelöst. |
| int hashCode() Liefert den Hashcode. Zwei entfernte Stubs, die auf dasselbe Objekt verweisen, sollten auch den gleichen Hashwert liefert. Die Methode ruft auf entfernten RemoteRef-Objekten die Methode remoteHashCode() auf und auf lokale Objekte einfach hashCode(). |
| boolean equals( Object obj ) Vergleicht, ob zwei entfernte Objekte gleich sind. Ist obj kein entferntes Objekt, so wird ein normaler Vergleich mit equals() gemacht. Der Vergleich mit dem entfernten Objekt wird mit remoteEquals() auf dem RemoteRef-Objekt vorgenommen. |
| String toString() Liefert eine String-Repräsentation. |
| private void writeObject( ObjectOutputStream out ) throws IOException, ClassNotFoundException Bereitet ein serialisiertes Objekt vor, in dem es den Namen des Objektes in UTF-8 schreibt. Dann wird das Objekt serialisiert. |
| private void readObject( ObjectInputStream in ) throws IOException, ClassNotFound Exception Liest das serialisierte Objekt wieder ein. |
RemoteServer erweitert RemoteObject und fügt die interessante Methoden getClientHost() hinzu. Damit kann der Server den Hostnamen vom Client als String erfahren. Damit der Server jedoch Informationen an den Client zurückschicken kann, muss der Client selbst als Server auftreten. Die Kommunikation ist bei RMI in der Regel immer einseitig.
abstract class java.rmi.server.RemoteServer extends RemoteObject |
| protected RemoteServer() Erzeugt für die Unterklassen ein RemoteServer()-Objekt. |
| protected RemoteServer( RemoteRef ref ) Erzeugt für die Unterklassen ein RemoteServer()-Objekt mit der Referenz ref. |
| static String getClientHost() throws ServerNotActiveException Liefert den Hostnamen des aktuellen Clients. Die Ausnahme ServerNotActiveException wird ausgelöst, wenn der Aufruf außerhalb eines Servers stattfindet, der RMI anbietet. |
Die Stellvertreter sind Methoden auf der Client- und Server-Seite, die die tatsächliche Kommunikation betreiben. Sie müssen für jede Methode oder jede Parameteränderung neu angegeben werden. Da die Implementierung per Hand zu aufwendig und inflexibel wäre, erstellt ein Hilfsprogramm diese Klassen. Das Dienstprogramm heißt für Java »rmic«. Der Compiler generiert selbstständig aus einer Methodenbeschreibung die Stellvertreter, die »Stubs« und »Skeleton« heißen. Ein Stub ist ein Stellvertreter (client-seitiger Proxy) für das entfernte Objekt auf der Client-Seite, der die RMI-Anfragen an den Skeleton (server-seitig) weitergibt. Der Skeleton richtet die Client-Anfrage an die wirkliche Methodenimplementierung und schickt das Ergebnis wieder zurück.
Bevor rmic zum Zuge kommt, müssen die entfernten Klassen und Schnittstellen übersetzt sein. Danach schreiben wir
$rmic AdderImpl
Die erzeugten Klassen werden standardmäßig im aktuellen Verzeichnis platziert. Mit der Option -D lässt sich der Zielort ändern.
RMI gibt es mittlerweile in unterschiedlichen Version. Mit dem Schalter -vXXX bzw. -iiop lässt sich dies genauer angeben.
| -v1.1 Erzeugt Stub und Skeleton für das Protokoll unter JDK 1.1. |
| -v1.2 Erzeugt den Stub für das JDK 1.2. Skeletons werden nicht benötigt. |
| -y Das Sandardprotokoll unter dem JDK 1.2. Es ist kompatibel mit dem neuen 1.2 Stub-Protokoll und dem älteren von 1.1. |
| -iiop Erstellt für CORBA die passenden Bausteine. |
Mit der Option -idl kann zusätzlich für CORBA eine Spezifikationsdatei erstellt werden. Möchten wir zu den generierten Klassen den Quellcode sehen, so müssen wir -keep angeben.
| Tipp Obwohl mit der Zeile alles in Ordnung aussieht, muss unter einigen Systemen der CLASSPATH angepasst werden – er muss auf das aktuelle Verzeichnis zeigen. Andernfalls produziert das Programm einen Fehler, nämlich, dass die Klassen nicht gefunden werden, obwohl sie im Pfad stehen. |
|
Mit dem Namensdienst können die Server ihre entfernten Objekte mit einem Namen anmelden. Er hilft außer den Clients, die entfernten Objekte zu finden. Für den Namensdienst können unterschiedliche Programme eingesetzt werden; beim JDK ist ein einfaches Programm dabei. Der beigefügte Namensdienst ist ein vereinfachter Object Request Broker (ORB) bei CORBA. Unter Windows starten wir den Dienst im Hintergrund mit folgender Zeile:
$start rmiregistry
$rmiregistry &
| Tipp Da rmiregisty selbst ein RMI-Client ist, muss dieser Zugriff auf die Server-Klassen haben; andernfalls könnte der Server seine Objekte von einem fremden Typ gar nicht anmelden. Daher sollte im CLASSPATH ein Verweis auf die Server-Klassen stehen. |
Der Namensdienst läuft standardmäßig auf dem Port 1099 auf. Für Dienste hinter einer Firewall ist es bedeutend, dass dieser Port auch anders lauten kann. Eine andere Portnummer lässt sich einfach als Parameter angeben:
$start rmiregistry 2001
Der angegebene Port dient nur der Vermittlung vom Client zum Namensdienst. Die Kommunikation von Client und Server läuft über einen anderen Port.
An dieser Stelle haben wir schon fast alles zusammen. Der Namensdienst läuft und wartet auf den Server und den Client. Beginnen wir mit dem Server. Er ist ein normales Java-Programm ohne Einschränkungen. Er muss weder etwas mit Remote noch mit Serializable zu schaffen haben. Seine einzige Aufgabe ist es, ein entferntes Objekt anzulegen und beim Namensdienst einzutragen. Dazu wird die Methode rebind() oder bind() benutzt.
Listing 19.3 AdderServer.javaimport java.net.*;
import java.rmi.*;
import java.rmi.server.*;
import java.rmi.registry.*;
public class AdderServer
{
public static void main( String args[] ) throws Exception
{
AdderImpl adder = new AdderImpl();
Naming.rebind( "Adder", adder );
System.out.println( "Adder bound" ); } }
An diesem Programm ist abzulesen, dass das Eintragen sehr einfach ist. Es ist wie eine assoziative Datenstruktur zu verstehen, die einen Objektnamen mit einem entfernten Objekt assoziiert. Die Notation für das Objekt ist wie bei einer URL:
Wenn ein alternativer Port für den Namensdienst gewählt wurde, stellen wie diesen mit Doppelpunkt wie üblich hinten an. Optional kann auch das Protokoll RMI vorangestellt werden. Da aber sowieso nichts anderes unterstützt wird, ist es echt optional.
Zum Binden der Informationen bietet der Namensdienst zwei unterschiedliche Funktionen an. bind() trägt den Dienst im Namensdienst ein, aber wenn schon ein anderer Dienst unter dem gleichen Namen läuft, wird eine AlreadyBoundException ausgelöst. rebind() dagegen fügt abhängig vom Namensdienst einen neuen Eintrag mit dem gleichen Namen hinzu oder überschreibt den alten.
Ist der Dienst nicht mehr gewünscht, so lässt er sich mit unbind() wieder abmelden, solange der Namensdienst läuft. Aus Sicherheitsgründen lässt der Namensdienst Objekte nur von dem Server entbinden, der auch das Objekt angemeldet hat. Einen zusätzlichen Namen müssen wir daher nicht angeben.
final class java.rmi.Naming |
| static void bind( String name, Remote obj ) throws AlreadyBoundException, MalformedURLException, RemoteException Bindet das Objekt ref, welches in der Regel der Stub ist, an den Namen name und trägt es so in der Registrierung ein. Eine AlreadyBoundException zeigt an, dass der Name schon vergeben ist. Die MalformedURLException informiert, wenn der Name ungültig gebunden ist. Eine RemoteException wird ausgelöst, wenn der Namensdienst nicht erreicht werden konnte. Fehlende Rechte führen zu einer AccessException. |
| static void rebind( String name, Remote obj ) Wie bind(), nur dass Objekte ersetzt werden, falls sie schon angemeldet sind. |
| static void unbind( String name ) Entfernt das Objekt aus der Registierung. Ist das Objekt nicht gebunden, so folgt eine NotBoundException. Die anderen Fehler sind wie bei bind(). |
Bisher haben wir ein entferntes Objekt erzeugt und angemeldet, sodass später das Objekt schon da ist, wenn es angesprochen wird. Wir haben das durch UnicastRemoteObjekt realisiert, dessen Arbeitsweise darin besteht, das Objekt einmal anzumelden. Sollten auf einem Objekt-Server mehrere Dienste vor sich hindämmern, ist das natürlich nicht sonderlich effektiv und kostet unnötig Ressourcen. Daher unterstützt die Bibliothek neben UnicastRemoteObjekt eine weitere Klasse, die das automatische Hochstarten eines Dienstes erlaubt. Wir leiten unser Objekt dann von der Klasse Activatable ab, und dann werden die Objekte bis zu ihrer Aktivierung in einem Dämmerzustand gehalten. Kommt dann der erste Zustand, entfaltet das System dieses Objekt, sodass es Anfragen entgegennehmen kann. Wird das Objekt nach seiner Tat wiederum nicht verwendet, kann es wieder eingefroren werden. Die Daten bleiben dabei stabil. Activatable ist eine abstrakte Klasse, die von RemoteServer abgeleitet ist. Die unterschiedlichen Klassen zum Aktivieren bei Bedarf liegen alle im Paket java.rmi.activation.
Ebenso wie der Server ist der Client ein normales Java-Programm, welches weder etwas mit Remote noch mit Serializable zu tun hat. Um nun die entfernte Methode zu nutzen, muss ein entferntes Objekt gesucht und angesprochen werden. Dazu fragen wir den Namensdienst. Der Name für das Objekt setzt sich zusammen aus der URL und dem Namen des Dienstes. Bei Portangaben dürfen wir nicht vergessen, diesen wieder hinter einem Doppelpunkt anzugeben.
Listing 19.4 AdderClient.javaimport java.rmi.*;
import java.rmi.registry.*;
import java.rmi.server.*;
public class AdderClient
{
public static void main( String args[] )
{
try
{
Adder a = (Adder)Naming.lookup("Adder"); int sum = a.add( 2, 2 ); System.out.println( sum ); } catch ( Exception e ) { System.out.println( e ); } } }
Damit ist das letzte Puzzelstück zusammen und das RMI-Beispiel komplett. Naming.lookup() liefert zu einer URL ein Stub-Objekt, welches die gewünschte Schnittstelle Adder implementiert. Der Rückgabetyp ist Remote. Das ist jetzt wirklich einfach und wir sehen, dass sich ein lokaler Funktionsaufruf nicht mehr von einem entfernten unterscheidet.
final class java.rmi.Naming |
| static Remote lookup( String name ) throws NotBoundException, MalformedURLException, RemoteException Liefert eine Referenz auf den Stub, der mit dem entfernten Objekt name verbunden ist. War kein Dienst unter dem Namen verfügbar, kommt es zu einer NotBoundException. Ist der Namensdienst nicht erreichbar, folgt eine RemoteException. MalformedURLException kann durch eine falsch gebildete URL folgen. |
| static String[] list( String name ) Liefert ein Feld mit angemeldeten Diensten. Der angegebene Name gibt die URL des Namensdienstes an. Ist die URL falsch konstruiert, so folgt eine MalformedURLException; ist die Registry nicht erreichbar, folgt eine RemoteException. |
Um die Aktivität von RMI verfolgen zu können, haben die Entwickler einen einfachen Loggin-Mechanismus eingebaut. Er gibt Auskunft über die Objekte und entfernte Referenzen. Hier erfahren wir auch, ob alle gewünschten Objekte korrekt gefunden wurden. Das Logging lässt sich mit der Eigenschaft java.rmi.server.logClass einschalten, wenn der Wert auf true gesetzt ist. Dann erscheinen Ausgaben auf dem System.err-Fehlerkanal. Erweitern wir die Klasse RemoteServer, so erben wir zudem eine statische Funktion setLog(OutputStream), mit dem sich der Fehlerausgabestrom individuell setzen lässt. Mittels getLog(), der einen PrintStream liefert und keinen OutputStream, gelangen wir wieder an den Fehlerkanal.
abstract class java.rmi.server.RemoteServer extends RemoteObject |
| static void setLog( OutputStream out ) Loggt RMI-Aufrufe, in dem sie in den Ausgabestrom out geschrieben werden. Ist out=null, wird das Logging beendet. |
| static PrintStream getLog() Liefert den Ausgabestrom für das RMI-Logging. |
Im verteilten Fall reicht der normale GC nicht, und das Konzept muss um einen verteilten GC (engl. distributed GC, kurz DGC) erweitert werden. Im lokalen Fall weiß die lokale Maschine immer, ob ein Objekt referenziert wird, im verteilten Fall kann auf dem Server ein Objekt existieren, für das sich kein Mensch mehr interessiert. Damit im verteilten Fall auch der GC nicht mehr benutzte Objekte auf der Server-Seite freiräumen kann, verschickt die Maschine beim Nutzen und Lösen von Verbindungen referenced bzw. dereferenced Meldungen. Ist die Verbindung dann gelöst, bleibt die Klasse jedoch noch einige Zeit auf dem Server und wird nicht sofort gelöst. Aussagen über die Verweildauer gibt die Lease an, die sich über eine Eigenschaft verändern lässt.
Beispiel Setze die Verweildauer für Objekte auf eine halbe Stunde hoch
java -Djava.rmi.dgc.leaseValue=30 |
In unserem bisherigen Beispiel haben wir zwei Ganzzahlwerte übergeben. Die Implementierung der Stellvertreter ist nun so, dass eine Socket-Verbindung die Daten überträgt. Da keine Objekte transportiert werden, muss keine Serialisierung die Daten in Reihe liefern. Wir wollen uns nun damit beschäftigen, was mit Objekten passiert, die übertragen werden. Wir können verschiedene Klassen unterscheiden:
| Klassen, die auf beiden Seiten vorliegen, weil es zum Beispiel Klassen aus dem Standard-API sind. |
| Klassen, die nur auf der Server-Seite vorliegen und dem Client nicht bekannt sind. |
| Klassen, die selbst wieder Remote implementieren. |
Liegt die Klasse auf beiden Seiten als Klassenbeschreibung vor, da sie etwa eine Standard-Klasse ist oder in beiden Pfaden eingetragen ist, müssen wir mit keinen Problemen rechnen. Die übertragenen Daten müssen jedoch von Klassen stammen, die serialisierbar sind.
Schwierig wird die Lage erst dann, wenn der Server Klassen benötigt, die beim Client liegen. Es könnte etwa eine entfernte Methode
int max( Vector v );
geben, die das Maximum der Elemente aus dem Vector bildet. Die Elemente sind jedoch Objekte, die der Server vorher nicht gesehen hat, etwa Unterklassen von Konto im Vector, und der Server kennt nur Konto und die Methoden davon, bindet aber dynamisch an die Methode der Unterklasse.
Wir kommen also dazu, dass der Klassenlader Klassen nachladen muss, die für den verteilten Aufruf auf der Client- und Server-Seite nötig sind. Das erinnert an einen Applet-Klassenlader, der Gleiches machen muss. Für RMI-Aufrufe kommt der RMI-Klassenlader (java.rmi.RMIClassLoader) zum Zuge. Dieser Lader lädt jetzt die Stellvertreterobjekte sowie die benötigten Klassen in die lokale virtuelle Maschine. Woher die Klassen kommen, ist dem Lader egal. Sie können in CLASSPATH stehen, im aktuellen Verzeichnis oder auf einem Webserver. Im letzten Fall steuert die Eigenschaft java.rmi.server.codebase den Ort.
Beispiel Setzen der Codebase auf einen Webserver, damit die RMI-Programme die benötigten Klassen aus http://server/classimlp laden können.
java -Djava.rmi.codebase=http://server/classimlp |
Sollten die Klassen nur vom Server geladen werden und aus anderen, vielleicht dunklen Stellen, so ist die Eigenschaft java.rmi.useCodebaseOnly auf true zu setzen.
Damit die Klassen nicht auf dem Client oder Server liegen müssen, können sie nachträglich über den RMI-Klassenlader geladen werden. Doch das Laden von Klassen muss erst abgesegnet werden. Die Erlaubnis, ob Klassen übertragen werden dürfen, regelt ein spezieller Sicherheitsmanager. Die Klasse RMISecurityManager definiert eine Sicherheitsrichtlinie, dass serialisierbare Klassen von einem Rechner auf den anderen übertragen werden können. Wenn wir mit primitiven Werten arbeiten, wie in unserem ersten Beispiel, oder mit Standardklassen, ist dieser RMISecurityManager nicht nötig. Da wir aber serialisierbare Klassen übertragen müssen, ist der Sicherheitsmanager vorgeschrieben. Da Applets schon vom Applet-Sicherheitsmanager überwacht werden, können sie keinen zusätzlichen RMISecurityManager installieren.
Beispiel Ein RMISecurityManager ist nichts anderes als ein SecurityManager.
public class RMISecurityManager extends SecurityManager { |
Wenn wir folgende Zeile in unserem Servercode aufnehmen, wird RMI vom Klassenlader die Klassen laden können:
System.setSecurityManager( new RMISecurityManager() );
Erst der Sicherheitsmanager gibt uns das Recht für die Übertragung. Tragen wir ihn nicht ein, so führt es zu einer Fehlermeldung der folgenden Art:
java.security.AccessControlException:
access denied
(java.net.SocketPermission 127.0.0.1:1099 connect,resolve)
Die Meldung zeigt an, dass die aktuellen Sicherheitsrichtlinien die Übertragung nicht zulassen. Häufig ist es so, dass die Java-Installationen Sicherheitsrichtlinien vorgegeben, die sehr eingeschränkt sind.
Um die Richtlinien zu lockern, müssen wir eine Policy-Datei anlegen, die uns die Rechte zum Laden von Klassen gibt.
Beispiel Die Datei rmi.policy gibt Rechte für alle Dateien.
grant { |
Der Sicherheitsmanager bindet nun eine Reihe dieser Policy-Dateien ein. Wollen wir zusätzliche Policies einbinden, so geben wir sie auf der Kommandozeile für den Java-Interpreter an.
jav