Metamodeling
and its Applications
tud

Update: Mit der Einführung von Java 5 wird der RMI-Compiler nicht mehr benötigt. Die Stub- und Skel-Klassen werden dynamisch und automatisch während der Laufzeit erstellt. Der entsprechende Abschnitt unten kann also übersprungen werden.

  1. Einführung
  2. Was wird benötigt
  3. Erstellung des Serverinterfaces
  4. Erstellung der Serverklasse
  5. Erstellung des Stub und Skeleton (ab Java 5 nicht mehr notwendig)
  6. Programmierung eines Clients
  7. Serialisierung von Objekten
  8. Besonderheiten bei Remoteobjekten
  9. Übungsaufgabe

Einführung

Das "Remote Method Invocation"-Framework existiert in Java bereits seit JDK 1.1. Es ermöglicht eine transparente Kommunikation von Java-Anwendungen, die in verschiedenen virtuellen Maschinen -- und bei Bedarf auf anderen, vernetzten Rechnern -- laufen.

Um eine RMI-taugliche Anwendung zu entwickeln sind einige Dinge zu beachten, die in diesem Tutorial kurz vorgestellt werden.

Für weitere Informationen sei auf folgende Seiten verwiesen:

Was wird benötigt

Wenn ein Objekt über RMI verschickt werden soll oder über RMI Methodenaufrufe erhält, so muß dessen Klasse bestimmte Interfaces implementieren und teilweise von speziellen Klassen erben.

Jedem JDK liegt der RMI compiler rmic bei. Dieser generiert aus Klassen, deren Objekte über RMI Methodenaufrufe erhalten sollen, die entsprechenden Stellvertreterklassen (den Stub und das Skeleton). Diese Proxies sorgen für das Verpacken der Nachrichten

Damit eine Java-Anwendung von einer anderen VM angesprochen werden kann, muß sie sich bei der lokalen Registry registrieren. Dies kann wahlweise manuell über die Anwendung rmiregistry oder mit ein paar Zeilen java-sourcecode geschehen.

Wenn über RMI auch Klassen geladen werden sollen, muß man sich um einen passenden SecurityManager kümmern, dies wird jedoch in diesem Tutorial nicht weiter behandelt.

Erstellung des Serverinterfaces

Zuerst erstellt man ein Java Interface. Dieses erweitert das Interface java.rmi.Remote. In dem neuen Interface werden alle Methoden deklariert, die Remote aufrufbar sein sollen. Da die Methoden auch unter Umständen über das Netzwerk aufgerufen werden, kann es natürlich zu Kommunikationsproblemen kommen, daher muß zu jeder Methodendefinition ein throws java.rmi.RemoteException hinzugefügt werden. Bei der tatsächlichen Implementierung dieses Interface ist dies nicht mehr der Fall, da ja der Aufruf bereits auf Client-Seite fehlschlägt und die Methode sowieso nicht zur Ausführung gerät.

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface ServerInterface extends Remote
{
  public void method() throws RemoteException;
}

Erstellung der Serverklasse

Nun wird eine weitere Klasse hinzugefügt, die dieses Interface implementiert. Diese Klasse erweitert außerdem die Java-Klasse UnicastRemoteObject. Dies vereinfacht die Verwendung von RMI etwas und sorgt beispielsweise dafür, daß eine Instanz einer Serverklasse nicht automatisch beendet wird, wenn keine Referenzen mehr darauf existieren. Das ist recht sinnvoll, wenn ein Server dauerhaft laufen soll.

Da die Instanziierungsmethoden von UnicastRemoteObject eine RemoteException werfen können, muß die Instanziierungsmethode der Serverklasse dies deklarieren.

Wie bereits oben erwähnt, muß eine Java-Anwendung sich bei der lokalen Registry registrieren. Diese muß entweder vorher von der Kommandozeile per rmiregistry gestartet werden oder man erledigt das in einer main-Methode beispielsweise in der Serverklasse. Danach instanziiert man die Serverklasse und meldet sich bei der registry an. Hierzu bietet die Klasse java.rmi.Naming die Methode rebind, der man einen String (Die Serverkennung) und die neue Instanz des Servers übergibt. Das Ganze kann dann beispielsweise so aussehen.

import java.rmi.Naming;
import java.rmi.RemoteException;
import java.net.MalformedURLException; import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class Server extends UnicastRemoteObject implements ServerInterface
{
Server() throws RemoteException
{
super();
}

public static void main(String[] args)
{
try {
LocateRegistry.createRegistry(Registry.REGISTRY_PORT);
}

catch (RemoteException ex) {
System.out.println(ex.getMessage());
}
try {
Naming.rebind("Server", new Server());
}
catch (MalformedURLException ex) {
System.out.println(ex.getMessage());
}
catch (RemoteException ex) {
System.out.println(ex.getMessage());
}
} ... }

Erstellung des Stub und Skeleton (ab Java 5 nicht mehr notwendig)

Nun kommt der RMI-Compiler ins Spiel. Er generiert aus dem Serverinterface einen Stub für die Clientseite und einen Skeleton für die Serverseite. Diese Klassen kümmern sich um das Ein- und Auspacken der Nachrichten und die Weitergabe an die eigentlichen Empfängerklassen.

Direkt von der Kommandozeile geschieht dies durch Aufruf von rmic zusammen mit dem Klassennamen:

rmic Server

In Eclipse kann man sich hierzu auch ein Ant-Script bauen:

<?xml version="1.0" encoding="UTF-8"?>
<project name="test" default="rmic" basedir="." >
<target name="rmic">
<rmic base="." sourcebase="." includes="Server.class" />
</target>
</project>

Dieses Script speichert man mit der Endung xml ab und kann es dann aus dem Workspace von Eclipse durch Rechtsklick->Run->An build aufrufen.

Damit ist der Server fertig und kann gestartet werden.

Programmierung eines Clients

Nun wird natürlich noch ein Client benötigt, der die Methoden des Servers aufruft. Hier verwendet man wieder die Klasse java.rmi.Naming. Mit folgendem Codefragment besorgt man sich die Remote-Referenz eines Servers und kann dann auf dieser Methoden des ServerInterfaces aufrufen.

    try {
ServerInterface server = (ServerInterface)Naming.lookup(url); server.method();
}
catch (Exception ex)
{
...
}

Die URL setzt sich zusammen aus zwei Slashes, der Adresse des Servers, einem weiteren Slash und der Serverkennung. Ein Beispiel hierfür wäre:

//127.0.0.1/Server

Hiermit würde man sich mit einem, auf dem lokalen Rechner registrierten, Server verbinden. Danach können Methoden auf dem Serverobjekt aufgerufen werden, als ob diese lokal verfügbar wären. Dabei ist allerdings zu beachten, daß alle diese Methoden RemoteExceptions werfen können, falls die Netzwerkverbindungen unterbrochen wurde oder andere Netzwerk- oder RMI-Probleme auftreten.

Serialisierung von Objekten

Bisher wurde nur vom Aufruf von Methoden geredet. Damit lässt sich natürlich noch nicht allzuviel nützliche Funktionalität erreichen. Was nun benötigt wird, ist eine Möglichkeit, Objekte über RMI zu verschicken. Hierzu muß das Objekt "serialisiert" werden, also seine Datenstruktur in einen einen Bytestrom eingepackt werden, der auf der anderern Seite wieder ausgepackt werden kann.

Die meisten Basisklassen von Java (String, das Collection-Framework, etc.) bieten bereits die Serialisierbarkeit. Solange man also als Attribute nur mit diesen Typen arbeitet bekommt man die Serialisierbarkeit "geschenkt", indem man das Interface java.io.Serializable implementiert. Für kompliziertere Fälle sei auf die Java-RMI-Dokumentation verwiesen.

Besonderheiten bei Remoteobjekten

Remoteobjekte zeigen leider nicht immer das selbe Verhalten wie lokale Objekte. Angenommen ein Client schickt an den Server zwei Mal eine Referenz auf das selbe Objekt. Bei einem lokalen Aufruf zeigen beide Referenzen auf das selbe Objekt, d.h. der Operator == liefert true. Implementiert das Objekt nun das serializable-Interface (siehe vorherigen Abschnitt) so wird das Objekt zwei Mal erzeugt und führt somit zu zwei verschiedenen Objekten auf Serverseite, somit liefert == false. Das selbe passiert (leider) auch bei Objekten, die das Remote-Interface implementieren. Hier wird auf Serverseite bei jeder Übertragung ein neues Stub-Objekt generiert, was die Kommunikation zurück zum Client ermöglicht, da jedes Mal jedoch ein neues Objekt erzeugt wird, schlägt der Vergleich mit == ebenfalls fehl. Diese Eigenarten sind bei Verwendung von RMI zu beachten. Ein Workaround ist die konsequente Verwendung von equals statt == wobei die eigentliche equals-Methode zum Vergleich der Objekte ggf. Abfragen über das Netzwerk machen muß, was u.U. nicht sehr performant ist.

Übungsaufgabe

Die obigen Informationen sollten ausreichen, um eine kleine Client-Server-Architektur auf der Basis von RMI zu entwickeln. Die Beispielapplikation modelliert ein Wahlsystem, bei dem Clients ihre Stimme an den Server abgeben und von dort den aktuellen Stand erfragen können. Die Parteien sind durchnummeriert von 0 bis 9. Zur Speicherung der Stimmen sollte das Java Collection Framework verwendet werden. Hierbei ist zu beachten, daß gleichzeitig mehrere Clients auf den Server zugreifen können.

Der Server stellt Methoden zum Abstimmen zur Verfügung

public void vote(int party)

darüber hinaus eine Methode, um den aktuellen Stand zu erfragen

public List currentStandings()

Die Implementation sollte auch über das Netzwerk getestet werden.