Ein Ansatz zur Kommunikation in verteilten Systemen auf der Basis von OSGi Services

Die Basis einer guten Beziehung

Alexander Ziegler

In einem verteilten System müssen die verteilten Anwendungsbestandteile miteinander interagieren – nur so funktioniert ein großes Ökosystem. Es wird daher ein Mechanismus benötigt, der eine Prozess-übergreifende verteilte Kommunikation ermöglicht. Equinox selbst bzw. die OSGi-Spezifikation lässt offen, wie Kommunikation auf Basis von OSGi realisiert wird. Am Beispiel der Open-Source-Implementierung Nyota [1] von der compeople AG wird gezeigt, wie eine solche Kommunikation umgesetzt werden kann.

Eine Anwendung, die auf einer OSGi-/Equinox-Plattform aufbaut, ermöglicht eine modulare und Komponenten-orientierte Entwicklung ihrer Bestandteile. Durch die Wiederverwendung und Erweiterungen von Komponenten wird die Wartbarkeit der Anwendung verbessert. OSGi ist als Ökosystem für den Lebenszyklus der Komponenten verantwortlich, und die Kommunikation zwischen den Komponenten wird auf Basis von OSGi Services geregelt. Bei einer Anwendung in einem verteilten System sind ihre Bestandteile auf Clients und Server verteilt. Das verteilte System stellt ein großes Ökosystem dar, in dem die Bestandteile aufeinander abgestimmt und eine verteilte Kommunikation zwischen den Komponenten hergestellt werden muss. Der Artikel zeigt einen Ansatz, wie dies mit OSGi Services möglich ist.

OSGi als Plattform für Anwendungen
OSGi besitzt viele Eigenschaften [2], die für moderne Anwendungen von Bedeutung sind. An dieser Stelle seien nur die wichtigsten Eigenschaften erwähnt:

  • ein modularer, erweiterbarer, Komponenten-orientierter Aufbau (Bundle-Konzept)
  • ein dynamisches Laufzeitverhalten mit Lifecycle Management
  • ein Service-orientiertes Programmiermodell

Eine OSGi-basierte verteilte Anwendung – Client oder Server – profitiert direkt von den Eigenschaften der Plattform. Dieser Vorteil wird deutlich, wenn das verteilte System aus Desktop- und Server-Anwendungen besteht wie z.B. bei einem Smart-Client-Ansatz mit einheitlichem Plattform-Konzept und Programmiermodell [5]. Erfahrungsgemäß reduziert das die Einarbeitungszeiten, weil vorhandenes Wissen genutzt werden kann. Umdenken ist nicht nötig, und das „sich Wiederfinden“ in Konzepten und Strukturen wird erleichtert.

Ein Ansatz für verteilte OSGi Services

Nyota verfolgt den Ansatz einer transparenten verteilten Kommunikation. Im Vordergrund steht nicht das Kommunikations-spezifische Protokoll oder dessen API, sondern das vorhandene bekannte API der OSGi-Plattform. Hierdurch soll erreicht werden, dass die Anwender effizient verteilte Kommunikation realisieren können, ohne sich mit technischen Details auseinander setzen zu müssen. OSGi Services stehen bei diesem Ansatz im Mittelpunkt. Sie stellen Dienste innerhalb einer Anwendung (der JVM) dar und dienen dazu, gezielt Funktionalität aus einem Bundle bereitzustellen. OSGi Services entkoppeln die Abhängigkeiten zwischen den Bundles. Services sollten immer über Schnittstellen realisiert werden, auch wenn die OSGi-Spezifikation die Verwendung von Schnittstellen offen lässt.

Abb. 1: OSGi Services registrieren/finden

Nyota macht sich die Service-orientierte Architektur von OSGi zunutze, um auch Dienste in einem verteilten System bereitzustellen. Um OSGi Services zu veröffentlichen oder zu finden, benutzt der Anwender das gewohnte OSGi API, indem er Services in der OSGi Service Registry registriert oder Remote Services über sie findet (Abb.1). Das tatsächliche Veröffentlichen und Finden eines fernen Service führt Nyota transparent durch.

Verteilte OSGi Services im Beispiel

In den folgenden Abschnitten wird anhand einer einfachen PingPong-Beispielanwendung gezeigt, wie verteilte OSGi Services mit Nyota programmatisch umgesetzt werden. Da Nyota sich an den OSGi-Service-Standard hält, können Remote OSGi Services auch deklarativ über Declarative Services [2] konfiguriert bzw. über Komponenten injiziert werden. Das Prinzip, nach dem Nyota Remote OSGi Services veröffentlicht oder findet, ist jedoch immer gleich, daher werde ich hier nicht weiter auf Declarative Services eingehen.
Das PingPong-Beispiel definiert einen Server Container, der einen IPingPong Service als Remote OSGi Service veröffentlicht (Listing 1). Ein Client Container verwendet den entfernten IPingPong OSGi Service. Beim Aufruf von ping(..) wird eine Ping-Nachricht zum Server Container gesendet und dort auf der Console ausgegeben. Diese antwortet mit einem Pong-Returnwert, der im Client Container ausgegeben wird.

/** service interface */
public interface IPingPong {

    String ID = IPingPong.class.getName();

    Pong ping(Ping ping);

}

/** service implementation class */
public class PingPong implements IPingPong {

  public Pong ping(Ping ping) {
    
    System.out.println("PingPong::Server:: " + ping);
    
    Pong pong = new Pong();
    pong.setText( "Thx. I got the ping!" );

    return pong;
  }

}
Die Bundle-Struktur

Als Erstes muss man sich für eine Bundle-Strukturierung entscheiden. Für dieses Beispiel werden vier Bundles verwendet:

  • sample.pingpong.common – ein allgemeines Bundle, das die Service-Schnittstelle und alle Typen definiert, die Bestandteile der Schnittstelle sind. Dieses Bundle wird sowohl im Server Container als auch im Client Container benötigt.
  • sample.pingpong.server – ein Bundle im Server Container. Es definiert die Implementierungsklasse für die Service-Schnittstelle und veröffentlicht den OSGi Service.
  • sample.pingpong.client – ein Bundle im Client Container. Es verwendet den Remote OSGi Service und führt den Ping durch.
  • sample.pingpong.client.config – ein Bundle im Client Container. Es konfiguriert den Endpunkt des veröffentlichten OSGi Service im Client Container als Remote OSGi Service.

Abbildung 3 zeigt die Abhängigkeiten unter den Bundles der Beispielanwendung. Eine besondere Beziehung besteht zwischen dem sample.pingpong.common Bundle und dem nyota.core Bundle. Es handelt sich dabei um eine Buddy-Beziehung, auf die zu einem späteren Zeitpunkt eingegangen wird.

Abb. 3: Bundle-Strukturierung bei der verteilten Kommunikation
OSGi Service auf dem Server veröffentlichen

Um den IPingPong OSGi Service zu veröffentlichen, muss eine Instanz der Klasse PingPong als OSGi Service registriert werden (Listing 2). Die Registrierung erfolgt über den Namen IPingPong.ID. Die Registrierung wird im Activator des Bundles sample.pingpong.server durchgeführt. Beim Registrieren des Services müssen drei OSGi Properties gesetzt werden: nyota.remote definiert den Service als Remote-Endpunkt; nyota.remote.porotocol bestimmt das Transportprotokoll, und nyota.remote.path legt fest, unter welchem Namen der Endpunkt veröffentlicht wird.
Der Server Container kann anschließend zum Testen gestartet werden. Listing 3 zeigt die vollständig konfigurierten Bundles im Server Container.

public class Activator implements BundleActivator {

    private ServiceRegistration pingPongReg;

    public void start(BundleContext context) throws Exception {
        PingPong pingPong = new PingPong();
        Hashtable properties = new Hashtable(3);

        properties.put("nyota.remote", "true");
        properties.put("nyota.remote.protocol", "hessian");
        properties.put("nyota.remote.path", "/PingPongWS");

        pingPongReg = context.registerService(IPingPong.ID, pingPong, properties);
    }

    public void stop(BundleContext context) throws Exception {
        pingPongReg.unregister();
        pingPongReg = null;
    }
}
id	State       Bundle
0	ACTIVE      org.eclipse.osgi_3.3.0.v20070523
1	ACTIVE      de.compeople.nyota.core_0.4.0
2	ACTIVE      org.mortbay.jetty_5.1.11.v200705231735
3	ACTIVE      org.eclipse.equinox.common_3.3.0.v20070426
4	ACTIVE      org.eclipse.core.jobs_3.3.0.v20070423
5	ACTIVE      org.eclipse.equinox.http.jetty_1.0.0.v20070523
6	ACTIVE      org.apache.commons.logging_1.0.4.v200705231731
7	ACTIVE      org.eclipse.equinox.http.servlet_1.0.0.v20070523
8	ACTIVE      de.compeople.nyota.sample.pingpong.common_0.4.0
9	ACTIVE      de.compeople.nyota.publisher.hessian_0.4.0
10	ACTIVE      de.compeople.nyota.sample.pingpong.server_0.4.0
11	ACTIVE      org.eclipse.osgi.services_3.1.100.v20060918
12	ACTIVE      org.eclipse.equinox.registry_3.3.0.v20070522
13	ACTIVE      org.eclipse.equinox.http.registry_1.0.0.v20070523
14	ACTIVE      javax.servlet_2.4.0.v200705231727
15	ACTIVE      com.caucho.hessian_3.0.8
16	ACTIVE      de.compeople.nyota.publisher_0.4.0

Der IPingPong OSGi Service wird automatisch beim Starten des Server Containers von Nyota gefunden und veröffentlicht. Da es sich bei dem hier verwendeten Protokoll um einen Hessian [3] Web Service handelt, kann die Verfügbarkeit des entfernten Endpunkts direkt im Browser getestet werden. Hierzu wird http://localhost/hessian/PingPongWS eingegeben. Der Browser liefert folgendes Ergebnis (Der HTTP Error 405 wird bei GET von Hessian erzeugt und kann für diesen Test ignoriert werden.):

HTTP ERROR: 405
Hessian requires POST
RequestURI=/hessian/PingPongWS
Powered by Jetty://

Entfernte OSGi Services auf dem Client verwenden

Das sample.pingpong.client Bundle benutzt den entfernten IPingPong OSGi Service über die OSGi Service Registry. Hierzu wird im Activator der IPingPong OSGi Service über seinen Namen IPingPong.ID ermittelt. Danach kann der ping(..) durchgeführt werden (Listing 4).

public class Activator implements BundleActivator {

    public void start(BundleContext context) throws Exception {
        ServiceReference pingPongRef = context.getServiceReference(IPingPong.ID);
        if (pingPongRef == null) {
            return;
        }
        IPingPong pingPong = (IPingPong) context.getService(pingPongRef);
        if (pingPong == null) {
            return;
        }

        Ping ping = new Ping();
        ping.setText("I ping you and you pong me");
        try {

            Pong pong = pingPong.ping(ping);
            System.out.println("PingPong::Client:: " + pong);

        } finally {
            context.ungetService(pingPongRef);
        }
    }

    public void stop(BundleContext context) throws Exception {
    }
}
Service-Endpunkt im Client konfigurieren

Wie wird der entfernte OSGi Service im Client Container bereitgestellt, sodass das sample.pingpong.client Bundle diesen über das OSGi API verwenden kann?
Der Service-Endpunkt muss im Client Container konfiguriert werden. Hierzu muss zuerst eine Proxy-Referenz auf den Endpunkt des im Server Container veröffentlichten OSGi Services erzeugt werden und anschließend die Proxy-Referenz als OSGi Service im Client Container in der OSGi Service Registry registriert werden. Nyota besitzt hierfür die Utilityklasse RemoteServiceFactory.
RemoteServiceFactory kann für einen gegebenen Service-Endpunkt eine Proxy-Referenz (RemoteServiceReferenz) erzeugen und diese automatisch als (Remote) OSGi Service registrieren.
Listing 5 zeigt, wie im Activator des sample.pingpong.client.config Bundles eine Instanz der RemoteServiceFactory erzeugt wird. Über createAndRegisterProxy(..) wird mit den Angaben des Endpunkts lokal ein OSGi Service erzeugt. Die Methode liefert ein IremoteServiceRegistration -Objekt zurück. Damit kann der OSGi Service wieder aus der OSGi Service Registry ausgetragen werden: IRemoteServiceRegistration #unregister().
Als nächstes wird das kleine verteilte System getestet. Hierzu wird der Client Container mit den folgenden konfigurierten Bundles (Listing 6) gestartet.

public class Activator implements BundleActivator {
    
    private IRemoteServiceRegistration pingPongReg;

    public void start(BundleContext context) throws Exception {
        RemoteServiceFactory rsf = new RemoteServiceFactory();
        Class> serviceInterface = IPingPong.class;
        String url = "http://localhost/hessian/PingPongWS";
        String protocol = "hessian";

        pingPongReg = rsf.createAndRegisterProxy(serviceInterface, url, protocol);

    }

    public void stop(BundleContext context) throws Exception {
        if (pingPongReg != null) {
            pingPongReg.unregister();
            pingPongReg = null;
        }
    }
}
id	State       Bundle
0	ACTIVE      org.eclipse.osgi_3.3.0.v20070523
1	ACTIVE      de.compeople.nyota.core_0.4.0
2	ACTIVE      org.eclipse.osgi.services_3.1.100.v20060918
3	ACTIVE      de.compeople.nyota.sample.pingpong.client_0.4.0
4	ACTIVE      de.compeople.nyota.registry_0.4.0
5	ACTIVE      de.compeople.nyota.console_0.4.0
6	ACTIVE      de.compeople.nyota.sample.pingpong.common_0.4.0
7	ACTIVE      com.caucho.hessian_3.0.8
8	ACTIVE      de.compeople.nyota.factory.hessian_0.4.0
9	ACTIVE      de.compeople.nyota.sample.pingpong.client.config_0.4.0

Das sample.pingpong.client Bundle wird automatisch gestartet und ruft den ping(..) auf dem Remote OSGi Service auf. Im Server Container wird der Empfang des Pings auf der Console ausgegeben :

>

> PingPong::Server:: [Ping] says = I ping you and you pong me

und anschließend der Pong auf der Console des Client Containers:
> PingPong::Client:: [Pong] says = Thx. I got the ping!

Warum die Konfiguration in einem separaten Bundle durchgeführt wird? Die Antwort lautet „separation of concerns“: die Trennung von Konfiguration und Verwendung durch getrennte Bundles. Ein entfernter OSGi Service wird potenziell an mehreren Stellen in der Anwendung verwendet. Man muss sich dann für einen „single point of configuration“ entscheiden. Das sample.pingpong.client.config Bundle könnte zum Beispiel durch ein Bundle ausgetauscht werden, das ein automatisches Service Discovery durchführt. Ein solches Bundle würde alle entfernten OSGi Services für einen Client Container finden und bereitstellen. Dadurch würden programmatische Konfigurationen wie in diesem Beispiel im Client Container entfallen. Ein Service-Discovery-Ansatz wird derzeit bei Nyota entwickelt und steht in einem frühen Stadium zur Verfügung [1].
Wie jedoch wird ein OSGi Service veröffentlicht? Was hat es mit dem Protokoll hessian auf sich? Wie wird eine Proxy-Referenz erzeugt und als Remote OSGi Service wiederum registriert? Diese Fragen werden im Folgenden erklärt.

Der Aufbau von Nyota

Nyota ist in drei Kernkomponenten unterteilt, die in entsprechenden Bundles definiert sind: den Publisher, die Service Factory und die Remote Service Registry.
Zunächst werden der Publisher und die Service Factory betrachtet (Abb. 4). Der Publisher erzeugt in einem Server Container für OSGi Services Protokoll-spezifische Service-Endpunkte. Im Client Container erzeugt die Service Factory für Protokoll-spezifische Service-Endpunkte Proxy-Referenzen. Der Begriff protokollspezifisch deutet darauf hin, dass Nyota selbst kein festes Protokoll implementiert. Nyota delegiert diese Aufgabe an Protokoll-spezifische Publisher (IServicePublisher) und Service-Factory-Implementierungen (IRemoteServiceFactory). Diese stehen als OSGi Services im jeweiligen Container zu Verfügung. Die Publisher- und die Service-Factory-Komponente verwenden einen Filter nyota.remote.protocol=[protocolvalue] , der über das Protokoll den jeweils passenden IServicePublisher oder IRemoteServiceFactory OSGi Service findet. Hierzu müssen die jeweiligen OSGi Services bei ihrer Registrierung die entsprechende OSGi Property setzen.
Nyota unterstützt bisher zwei Protokolle. Ein binärbasiertes Protokoll auf der Basis von Hessian [3] und ein SOAP basiertes Bean-Protokoll von XFire [4]. Nyota kann um weitere Protokoll-spezifische Implementierungen erweitert werden.
Die Nyota Remote Service Registry ist dafür zuständig, Proxy-Referenzen in einem Client Container als OSGi Services bereitzustellen und ihren Lebenszyklus zu verwalten. Die Remote Service Registry wird als OSGi Service vom Typ IRemoteServiceRegistry bereitgestellt. Über diesen Service werden Proxy-Referenzen registriert bzw. deregistriert.

Abb. 4: Nyota-Aufbau
Buddy-Konzept und Factory Bundles

Im vorangegangen Abschnitt wurde das nyota.core Bundle als Buddy für das sample.pingpong.common definiert (Abb. 3). Das ist notwendig, damit Nyota generisch fachliche Schnittstellen- und Klassen-Typen aus anderen Bundles laden kann, um diese anschließend zu instanziieren. Jedes Bundle besitzt einen eigenen Klassenkontext und ClassLoader.

Abb. 5: Buddy-Konzept

Typen sind nur innerhalb eines Bundles bekannt, es sei denn, sie werden explizit exportiert und von einem abhängigen Bundle benutzt (Abb. 5.1). Nach diesem Prinzip müsste das nyota.core Bundle eine Abhängigkeit zu sample.pingpong.common besitzen, um dessen Typen verwenden zu können. Das nyota.core Bundle ist jedoch ein generisches Factory Bundle, das beliebige fachliche Typen laden und instanziieren muss, je nachdem in welchem fachlichen Kontext Nyota eingesetzt wird. Das Eclipse-BuddyPolicy-Konzept hilft hier aus (Abb. 5.2). Damit können Typen aus einem Bundle für andere Bundles sichtbar gemacht werden, auch wenn keine Abhängigkeiten zwischen den Bundles bestehen.
Das Buddy-Konzept ist kein Teil der OSGi-Spezifikation, sondern von Equinox. Daher ist Nyota aktuell zusammen mit Equinox zu verwenden.

OSGi-Plattform versus Java EE
Beide Plattformen basieren auf unterschiedlichen Komponentenparadigmen. Die OSGi-Plattform ist ein Container, in dem alle Laufzeitkomponenten als Bundles (JAR-Dateien mit besonderem Manifest) bereitgestellt und gleich behandelt werden. Der Aufbau eines Bundles ist immer gleich. Bei einer JavaEE-basierten Anwendung gibt es vier unterschiedliche Container-Typen (Web Container, EJB Container, Applet Container und Application Container). Die Laufzeitkomponenten sind unterschiedliche Typen, wie z.B. WAR-Dateien für Web Container und EAR-Dateien für EJB Container. Der Aufbau dieser Laufzeitkomponenten ist unterschiedlich. (Zum Thema Komponenten, Bundle und JavaEE siehe [6]).
Anwendungen auf der Basis einer OSGi-Plattform können unter dem gleichen Paradigma als Rich-Client- und Server-Anwendungen realisiert werden. Java-EE-Anwendungen sind typischerweise serverseitige Anwendungen mit Web-basiertem Client.
Die Java-EE-Architektur adressiert vornehmlich Lösungen im Enterprise-Umfeld. Die OSGi-Plattform spezifiziert bisher keine umfassenden Enterprise-Lösungen. Ende 2006 wurde bei der OSGi-Allianz die Enterprise Expert Group [7] ins Leben gerufen, um einen Standard rund um Enterprise-Themen für OSGi zu erarbeiten.
Fazit

Das unter der EPL-1.0-Lizenz [8] veröffentlichte Open-Source-Projekt Nyota bietet einen leichtgewichtigen Ansatz, mit dem die Kommunikation für verteilte Systeme auf der Basis von OSGi Services umgesetzt werden kann. Bei der Verwendung von Web-Service-Protokollen, wie z.B. Hessian oder XFire auf der Basis von generischen Beans bzw. POJOs, erfolgt das Veröffentlichen von Endpunkten sowie das Erzeugen von Proxy-Referenzen transparent und ohne spezielles Zutun der Anwendung. Der modulare Aufbau von Nyota lässt offen, Protokolle durch eigene Implementierungen auszutauschen bzw. neue Protokolle zu implementieren. Neben der bereits erwähnten Service-Discovery-Entwicklung sind Funktionalitäten wie z.B. Security im Entstehen, wodurch Nyota um wichtige Eigenschaften erweitert wird.

Alexander Ziegler leitet die Framework-Entwicklung bei der compeople AG, Frankfurt am Main. Er verfügt über umfassendes Know-how sowie jahrelange Erfahrung im Bereich der auf Dreischichten-Architekturen basierenden Smart-Client-Plattformen. Zurzeit arbeitet er an einem Ansatz, der die Smart-Client-Technologie mit der Eclipse RCP und den Equinox-Plattformen verbindet. Darüber hinaus ist er am Open-Source-Projekt Nyota beteiligt. Kontakt: Alexander.Ziegler@compeople.de.

Geschrieben von
Alexander Ziegler
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.