Zukunftssicher unterwegs mit OSGi und REST - JAXenter

Zukunftssicher unterwegs mit OSGi und REST

Verteilung mit p2

Das Szenario verteilter und entkoppelter Entwicklung eines Systems ist typisch für die Kombination aus einem Standardsoftwareprodukt und kundenspezifischen Erweiterungen. OSGi bietet diese Modularität innerhalb der Laufzeitumgebung an. Das beste Beispiel dafür ist sicherlich die Eclipse IDE in all ihren Ausprägungen. API-Module anderer Parteien müssen aber nicht erst zur Laufzeit, sondern bereits während der Entwicklung bereitstehen. Eclipse PDE bietet mit der Target-Plattform hierfür gute Unterstützung. Eine Target-Plattform [5] ist ein Set von OSGi Bundles, gegen die die Projekte im Workspace kompiliert werden. Bundles der Target-Plattform können aus verschiedenen Quellen kommen: vom lokalen Dateisystem, aus einer bestehenden lokalen Eclipse-Installation oder – und das ist für unseren Fall die interessanteste Option – aus einem über das Netz erreichbaren p2 Repository [6] oder einer Eclipse-Updatesite. Für den Proof of Concept entwickeln wir gegen eine Target-Plattform, die eine lokale Kopie von Restlet, eine lokale Kopie der Restlet/OSGi-DS-Integration und die Eclipse-Indigo-Updatesite enthält (Abb. 3).

Abb. 3: Target-Plattform für die Entwicklung des Proof of Concept

Das Charmante an der Verwendung von p2 Repositories ist, dass Bundles aus diesen Repositories nicht nur während der Entwicklung konsumiert, sondern auch direkt als Installations- beziehungsweise Updatequellen für installierte Instanzen genutzt werden können. P2 unterstützt auch die Komposition mehrerer Repositories, was in verteilten Umgebungen wie der unseren hilfreich ist. Unter Verwendung des p2 Provisioning API ist es leicht, applikationsspezifische Updateagenten zu implementieren, die den Updateprozess automatisieren. Die Verwendung von p2 als Bundle Repository unterstützt uns bei unserem Anwendungsfall also an zwei Stellen: während der Entwicklung über die Integration in die Eclipse-PDE-Target-Plattform und als Updatemechanismus für dezentrale Installationen der Ladestationssoftware.

Semantic Versioning und OSGi

Semantic Versioning [7] bezeichnet ein System zur Versionierung von Softwarekomponenten, das Versionsnummern in Hinsicht auf die Kompatibilität von Clientcode eine bestimmte Bedeutung zuweist. Ausgehend von der Syntax <major>. <minor>. <patch> werden im Kern folgende Regeln aufgestellt:

  • Die Patch-Version muss inkrementiert werden, wenn nur rückwärtskompatible Änderungen in der neuen Version vorhanden sind.
  • Die Minor-Version muss inkrementiert werden, wenn neue, aber rückwärtskompatible Änderungen an dem öffentlichen API vorgenommen wurden. Sie kann erhöht werden, wenn in privatem Code signifikante Anpassungen vorliegen.
  • Die Major-Version muss erhöht werden, wenn nicht rückwärtskompatible Änderungen an dem öffentlichen API durchgeführt wurden.

OSGi gilt mit seinen Versionierungsmöglichkeiten als die aktuell reifste Laufzeitumgebung im Java-Umfeld und erlaubt die Koexistenz verschiedener Versionen eines Bundles/Packages innerhalb derselben Runtime. Nicht nur einzelne Bundles, sondern auch jedes enthaltene Package kann separat versioniert werden. Konsumenten von in externen Bundles spezifizierten Packages können neben dem benötigten Package auch eine Version oder eine Versionsspanne angeben, in der dieses Package vorliegen muss. Die OSGi Runtime ist dafür verantwortlich, diese Abhängigkeiten aufzulösen. Aus welchem Bundle die Abhängigkeit befriedigt wird, ist dabei für den Konsumenten transparent. Alle in der OSGi-Spezifikation beschriebenen Packages wurden bereits von Anfang an semantisch versioniert [8]. Die Spezifikation enthält eine Empfehlung für die Nutzung semantischer Versionierung, schreibt deren Einhaltung aber nicht fest vor [9].

Eine Anforderung der Ladestation ist es, Clients in unterschiedlichen Versionen bedienen zu können und gleichzeitig die Weiterentwicklung nicht zu behindern. Da der Betrieb von mehr als einer Version einer Komponente möglich ist, muss nicht jede Weiterentwicklung rückwärtskompatibel sein, was die Flexibilität für neue Entwicklungen deutlich erhöht. Durch den Einsatz von Semantic Versioning kann die Versionsinformation verwendet werden, um Clientanfragen automatisiert auf entsprechend kompatible Services umzuleiten. Die Clientanfrage muss dabei die benötigte oder erwartete Version enthalten. Der Server entscheidet anhand dessen, welche Version eines Service die Anfrage bedient.

Genug geredet! Wo ist der Code?

Um die getroffenen Architekturentscheidungen zu validieren, implementieren wir einen Proof of Concept anhand eines beispielhaften Anwendungsfalls: der Anmeldung eines Fahrzeugs an der Ladestation. Illustriert werden die modulare Komposition der Komponenten, die Updatefähigkeit zur Laufzeit und die Versionierung der Komponenten. Die Verwendung von Hypertext als Repräsentation und das Provisioning über p2 werden nicht behandelt. Diese Erweiterungen sind jedoch problemlos ohne signifikante Änderungen des Prototyps implementierbar.

In der ersten Version des Service erwartet der Server nur eine Identifikationsnummer des Fahrzeugs. In der zweiten Version gibt das Fahrzeug zusätzlich die Länge der nächsten geplanten Fahrt und den Startzeitpunkt an, sodass der Netzbetreiber die Fahrzeugbatterie innerhalb dieser Rahmenparameter als Puffermedium nutzen kann (Kasten: „Vehicle to Grid“). Wir nehmen an, dass die Schnittstellen des Service in den unterschiedlichen Formaten nicht untereinander kompatibel sind. Weiterhin nehmen wir an, dass dieser Service vom Netzbetreiber (com.powersupplier.*) zur Verfügung gestellt wird, das Framework, in dem die Services laufen, und die Möglichkeit ihrer Versionierung jedoch vom Hersteller der Ladestation (com.manufacturer.* ). Die vom Hersteller zur Verfügung gestellte Infrastruktur beinhaltet in der ersten Version nur eine Restlet-Applikation und einen Router, an die deklarativ Ressourcen gebunden werden. Die Applikation stellt eine Implementierung des IApplicationProvider-Interface zur Verfügung und referenziert IRouterProvider statisch in der Kardinalität 1:1. Um den Einstiegs-URL zu unserer Applikation zu definieren, legen wir in der Deklaration eine Property mit dem Namen alias und dem Wert „/“ an. Die zweite Komponente stellt den RouterProvider bereit und referenziert ihrerseits auf Ressourcen- und Filterprovider (IResourceProvider, IFilterProvider). Diese Referenzen können dynamisch gebunden werden, um die dynamische Erweiterung einer Applikation zur Laufzeit zu ermöglichen.

Applikation und Router können unter Verwendung von restlet-osgi deklarativ erzeugt werden. Dazu muss für beide Komponenten eine Service Component Description in Form einer XML-Datei in OSGI-INF angelegt werden. Damit diese Beschreibungen geladen werden, muss aus MANIFEST.MF in folgender Form auf sie verwiesen werden: Service-Component: OSGI-INF/*.xml (Listing 1). Mit diesem API ausgestattet, kann jetzt eine weitere Partei einen Service in dieser Struktur bereitstellen. Um bei unserem Beispiel zu bleiben, implementieren wir den Service zum Initiieren eines Ladevorgangs. In einem neuen Bundle (com.powersupplier.chargeomatic) erstellen wird dafür eine einfache POJO-Modellklasse der Ladeanfrage, die nur eine Fahrzeugidentifikation als String und ein Flag zur Bestätigung der Anfrage enthält. Dieses Modell wird von unserer Restlet-Ressource empfangen, verarbeitet und wieder zurückgeliefert. Zusätzlich benötigen wir einen ResourceProvider, der die Interaktion mit der SCR ermöglicht. Als Format für die Repräsentation unserer Ressource wählen wir der Einfachheit halber JSON. In der tatsächlichen Implementierung würden wir auf ein spezifisches XML-Format und/oder Atom setzten, da es für die Abbildung von Hyperlinks in JSON zum aktuellen Zeitpunkt noch keine verbreiteten Standards gibt (Listing 2, Listing 3).

Listing 1: Komponentendeklaration der Restlet-Applikation
Listing 2: Charging-Request-Ressource
package com.powersupplier.chargeomatic.resources;
import org.restlet.resource.Put;
import org.restlet.resource.ServerResource;
import com.powersupplier.chargeomatic.model.ChargingRequest;

public class ChargingRequestResource extends ServerResource {
  @Put("json")
  public ChargingRequest initateCharging(ChargingRequest request) {
    if (request == null) { request = new ChargingRequest(); }
    request.setRequestAcknowledged(true);
    return request;
  }
}
Listing 3: Changing Request Resource Provider
package com.powersupplier.chargeomatic.resources;
import org.eclipselabs.restlet.ResourceProvider;
import org.restlet.Context;
import org.restlet.resource.Finder;

public class ChargingRequestResourceProvider extends ResourceProvider {
  @Override
  protected Finder createFinder(Context context) {
    return new Finder(context, ChargingRequestResource.class);
  }
}

Wir starten eine OSGi Runtime und deployen zunächst das Bundle des Herstellers. Um die Implementierung zu testen, verwenden wir als Client cURL auf der Kommandozeile, da es uns cURL leicht macht, mit HTTP Requests zu experimentieren. Damit unser Service korrekt angesprochen wird, müssen wir eine Reihe von Parametern spezifizieren. Die HTTP-Methode PUT wird mittels des Parameters -X, der Request Header für den Content Type mit -H content-type und der Inhalt des Requests mit -d angegeben:

$ curl localhost:8080/charge -X PUT -d "{"vehicleId":"WAUZZZ8E99A000042"}" -H  content-type:application/json

Da die angefragte Ressource noch nicht deployt ist, erhalten wir keine Antwort. Wir exportieren also das Bundle des Energieversorgers und installieren es unter Verwendung der OSGi-Konsole in unsere Runtime:

osgi> install file://tmp/com.powersupplier.chargeomatic_1.0.0.201108282225.jar
Bundle id is 28
osgi> ss chargeomatic
Framework is launched.
id	State       Bundle
20	ACTIVE      com.manufacturer.chargeomatic_1.0.0.qualifier
28	INSTALLED   com.powersupplier.chargeomatic_1.0.0.201108282231
osgi> start 28

Starten wir unsere Anfrage jetzt erneut, erhalten wir als Antwort eine Bestätigung der Ladeanfrage: {„vehicleId“:“WAUZZZ8E99A000042″,“requestAcknowledged“:true}. Stoppen wir das eben installierte Bundle, so läuft unsere Anfrage wieder ins Leere. Wir haben also gerade einen neuen Service installiert und wieder entfernt, ohne die Laufzeitumgebung zu stoppen und ohne weitere Komponenten explizit zu konfigurieren. Spannend wird es aber erst, wenn die ersten Clients diesen Service im Feld produktiv nutzen und wir inkompatible Änderungen an dessen Schnittstellen vornehmen müssen. In unserem fiktiven Fall will der Betreiber die angeschlossenen Fahrzeuge in einem Vehicle-to-Grid-Szenario nutzen. Dazu muss zu Beginn jedes Ladevorgangs der Startpunkt und die Länge der nächsten Fahrt angegeben werden.

Kommentare

Schreibe einen Kommentar

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