Das OSGi-Komponentenmodell

Flucht aus der "Jar-Hölle"

Sven Johann und Bernd Böllert

OSGi ist eine Ergänzung zu bestehenden Java-Architektur-Pattern. Es bietet die Möglichkeit, Software nach individuellen Anforderungen zu entwickeln. Außerdem können mithilfe eines starken Komponentenmodells unübersichtliche Java-Anwendungen organisiert werden. Welche Chancen und Risiken daraus erwachsen, wird in diesem Artikel anhand typischer Einsatzszenarien skizziert.

Java-EE-Anwendungen haben sich in den letzten Jahren stark gewandelt. Von schwer zu handhabenden Programmiermodellen wie EJB 1.x/2.x hin zu annotierten, POJO-basierten EJB-3.0-Anwendungen. Trotz dieser starken Vereinfachung bleiben noch einige Probleme unbeachtet:

  • Schwache Serviceregistrierung/dynamische Lokalisierung von Serviceprovidern: Mittels JNDI kann in EJB dynamisch ein Service lokalisiert werden. Aber es existiert keine Metainformation darüber, ob der Service in der geforderten Version vorliegt.
  • Es fehlt ein Software-Lifecycle-Prozess auf Serviceebene. Eine JEE-Anwendung kann nur als Ganzes (war oder ear) gestartet, gestoppt und aktualisiert werden. Das widerspricht dem serviceorientierten Ansatz. Das heißt, wenn in einer SOA servicezentrierte Lifecycles realisiert werden, entsteht ein Architektur- bzw. Paradigmenbruch, da die Innensicht der Services gewechselt wird.
  • Schwaches Komponentenmodell: In Java kann nur auf Klassenebene agiert werden. Jar/war/ear-Dateien helfen zwar, Klassen zu Komponenten bzw. Applikationen zusammenzufassen. Aber für die komponentenbasierte Entwicklung ist das wenig dienlich. Hier wird die Anwendung in logische Komponenten zerlegt, die wiederum aus logischen Komponenten bestehen usw. Ein Programmiermodell, das dies unterstützt, wäre hilfreich.
  • Schwaches Komponentenmanagement: Betrachtet man eine normale Java-EE-Anwendung, die zusammen mit anderen Java-EE-Anwendungen in einem Application-Server eingesetzt wurde, kann einem schnell schwindelig werden, sobald man sich die vielen jar-Dateien ansieht, die für die jeweilige Anwendung benötigt werden. Welches jar wird für welche Anwendung benötigt? Werden alle gebraucht? In welcher Version ist welches jar erforderlich? Gibt es Konflikte? Jeder hat sich eine dieser Fragen bestimmt schon gestellt. Die jar-Dateien liegen zudem ungeordnet in allen möglichen Verzeichnissen, eine geordnete Ablage à la Maven, aus der sich alle Applikationen bedienen, wäre wünschenswert.
  • Für Aufgaben, die in einer Virtual Machine (VM) erledigt werden, ist der Footprint von EJB sehr groß. Insbesondere, wenn man keine verteilten Services plant oder die Verteilung etwa über Web Services realisiert. Betrachtet man Apache Felix [1], kommt der Overhead, den das Framework mitbringt, auf eine 600k große Distribution. Dass OSGi gerade im Bereich der Smartphones so beliebt ist, verwundert daher nicht.

OSGi bzw. Applikationen rund um OSGi haben sich zum Ziel gesetzt, diese genannten Probleme zu lösen und bessere Java-EE-Applikationen zu ermöglichen.

Versionierung

Eine Versionierung ist notwendig, um die Kompatibilität in Servicearchitekturen zu überprüfen. Daher ist sie ein wichtiger OSGi-Baustein. Die Versionierung der Abhängigkeiten wird im Manifest.mf definiert. Sie erfolgt nach folgendem Muster:

Import-Package: com.acme.foo;version="[1.23, 2)",
com.acme.bar;version="[4.0, 5.0)",
com.acme.baz;version=1.2

Die Versionen müssen als Intervalle angegeben werden. Dabei bedeutet ein einzelner Wert, dass alles ab dieser Version als erlaubte Version definiert ist. Die Intervalle können mit eingeschlossenen Grenzen [] oder ausgeschlossenen Grenzwerten () definiert werden. Kombinationen sind möglich. Obwohl es in der Spezifikation [2] keine Details zur Nummerierung gibt, wird ein dreigliedriges Versionsnummernkonzept bevorzugt. Die Wertung der Stellen Major.Minor.Fix ist so zu interpretieren, dass die erste Stelle bei Versionssprüngen mit Kompatibilitätsverlust verwendet wird, die zweite Stelle für Erweiterungen und die dritte Stelle für Bugfixes ohne Erweiterungen der Funktionalität.

Lifecycle

Der Lifecycle aller OSGi-Bundles ist gleich. Durch den Schritt install wird ein Bundle, sofern es korrekt beschrieben ist, installiert. Es kann in den Zustand resolved überführt werden, wenn alle importierten Referenzen erfüllt sind. Viele Frameworks belassen Bundles im „Installed Status“ und lösen sie erst auf, wenn sie angefordert werden. Das hat zur Folge, dass das System Ressourcen nur dann belegt, wenn sie notwendig sind. Dadurch verkürzt sich auch die Zeit, bis ein System „initial“ zur Verfügung steht, beispielsweise bei Eclipse auf Equinox. Im Zustand resolved steht das API den dienstnutzenden Bundles zur Verfügung. Optional kann ein BundleActivator definiert sein, der in der Startphase synchronisiert (Threadsafe) das Bundle initialisiert. Symmetrisch dazu gibt es im Activator optional die Stopphase, um Ressourcen wieder frei zu geben.

Im Zustand uninstalled wird ein Bundle aktualisiert bzw. kann entfernt werden, wenn es aktuell nicht im Zugriff ist. Ist das Bundle durch andere installierte Bundles im Zugriff, bleibt es verwendbar bis zum Neustart des Frameworks. Im Lifecycle besteht auch die Möglichkeit, auf Serviceebene ein „Hot Deployment“ durchzuführen, d. h. im laufenden Framework werden Aktualisierungen auf Bundle-Ebene ausgeführt.

Abb. 1: Beispiel für logische Komponenten eines Softwaresystems

Abb. 2: Lifecycle von OSGi-Bundles
Servicekomponentenmodelle

Motivation für Servicekomponentenmodelle sind Folgende:

  1. Vermeidung von Glue Code. POJOs sind leicht zu testen. Glue Code ist repetitiv und sollte, wenn möglich, vermieden werden.
  2. Die definierten Serviceübergänge können leicht mit allgemeiner Funktionalität erweitert werden, z. B. Logging, Thread-Kontrolle und Transactions-Behandlung.
  3. Verwaltung der Beziehung von Interfaces (Service), Implementierungen (Serviceprovider) und Metainformationen, z. B. Kardinalität, also die Entscheidung, ob Singleton oder nicht, kann per Konfiguration erfolgen.
Starkes Komponentenmodell durch OSGi

Verschiedene Autoren haben Vorgehensmodelle zur komponentenbasierten Architekturentwicklung beschrieben, z. B. Gernot Starke und Peter Hruschka [3], Ralf Westphal [4] oder das Fraunhofer IESE [5]. Allen geht es prinzipiell darum, eine Softwarearchitektur durch stetige Verfeinerung zu entwickeln. Man beginnt mit einer sehr abstrakten Sicht auf das System und dessen Kontext und geht immer mehr in Richtung Details vor. So besteht ein Softwaresystem aus mehreren Komponenten, die wiederum aus Komponenten bestehen, die wiederum aus Komponenten bestehen usw. Gemäß der Devise „Divide and Conquer“ verfeinern wir ein System schrittweise, um nicht durch Details erschlagen zu werden. Die Komponenten des Systems werden durch die Services beschrieben, die sie exportieren, und jene, die sie von anderen Komponenten zum korrekten Funktionieren importieren müssen (Vor- und Nachbedingungen bzw. Zustandsverwaltung der Serviceaufrufe nicht berücksichtigt).

Auf der Ebene einer Programmiersprache wie z. B. Java kann diese logische Sicht nicht abgebildet werden. Es stehen zwar jar-, war- und ear-Dateien zur Verfügung, aber diese bilden unser System aus logischer Sicht nur mittelmäßig ab. Eine ear-Datei besteht aus verschiedenen Kindelementen wie war– und jar-Dateien. Es ist keine hierarchische und logische Zerlegung des Systems auf Programmiersprachenebene möglich. Auch gibt es keinerlei Informationen über das Abhängigkeitsnetz der jar-Dateien: Welche jar-Dateien (in welcher Version) benötigt eine jar-Datei, damit sie eingesetzt werden kann? Welche Services exportiert sie und wer nutzt sie? Gut wäre es, mehrere jar-Dateien zu Composite-jar-Dateien zusammenfassen zu können und diese wiederum zu Composite-jar-Dateien usw. zu vereinen. Ebenfalls wäre es vorteilhaft, explizit die Abhängigkeiten der Composite-jar-Dateien untereinander sehen zu können.

Mit solchen Informationen ergeben sich völlig neue Möglichkeiten im Umgang mit einem System. Logische Komponenten können nun auch physisch abgebildet werden. Auch automatische Komponententests werden erleichtert. Denn die testende Komponente gibt Auskunft darüber, von welchen Services anderer Komponenten sie abhängt, die wir dann mocken können. Auch die Analyse wird vereinfacht, da Reverse-Engineering-Tools ein Komponenten-Abhängigkeitsnetz erzeugen können.

Geschrieben von
Sven Johann und Bernd Böllert
Kommentare

Schreibe einen Kommentar

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