OSGi in kleinen Dosen – Bundles und Life Cycle - JAXenter

OSGi in kleinen Dosen – Bundles und Life Cycle

Bundle Life Cycle

Das OSGi Framework stellt die Laufzeitumgebung für Bundles dar: Bundles können installiert, aktualisiert und wieder deinstalliert werden. Zudem können sie gestartet und wieder gestoppt werden. Abbildung 2 zeigt die Zustände, die ein Bundle während seines Lebenszyklus innerhalb des OSGi Frameworks einnehmen kann.

Abb. 2: Zustände innerhalb des Bundle Life Cycle

Bevor wir auf die Frage eingehen, wie der Lebenszyklus gesteuert werden kann, widmen wir uns der Bedeutung der einzelnen Zustände und deren Übergängen. Durch das Installieren eines Bundles wird dieses innerhalb des OSGi Frameworks persistiert, d.h. in einem lokalen Bundle Cache abgelegt. Die Voraussetzung dafür ist, dass es ein gültiges Format und gültige Metadaten enthält. Bei der Installation wird dem Bundle eine eindeutige ID vom Typ long zugewiesen und das Bundle wird in den Zustand INSTALLED versetzt.

Dem OSGi Framework steht offen, ob es sofort nach der Installation eines Bundles oder erst später versucht, dessen Abhängigkeiten aufzulösen. Eclipse Equinox und Apache Felix z.B. verhalten sich hier lazy, um Ressourcen zu schonen. Beim so genannten Resolving versucht das OSGi Framework für jedes zu importierende Package ein passendes exportiertes Package zu finden. Nur wenn dies für alle nicht optionalen Package-Imports gelingt, wird das Bundle in den Zustand RESOLVED versetzt, andernfalls bleibt es INSTALLED und kann nicht verwendet werden, bis möglicherweise zu einem späteren Zeitpunkt alle Abhängigkeiten aufgelöst werden können.

Falls mehrere passende exportierte Packages vorliegen, wird das mit der höchsten Version verwendet. Falls immer noch kein eindeutiger Bezug hergestellt werden kann, wird die Version der exportierenden Bundles verglichen und falls auch dies ohne eindeutiges Ergebnis bleibt, wird das Bundle mit der kleinsten ID ausgewählt. Auf diese Weise kann vom OSGi Framework immer eine eindeutige Zuordnung zwischen einem importierenden und einem exportierenden Bundle getroffen werden: Dies wird Wiring genannt. Es ist jedoch empfehlenswert, mittels Versionsnummern und gegebenenfalls weiterer Attribute möglichst genaue Vorgaben zu treffen, um „selbst die Kontrolle zu behalten“.

Wichtig: Bundles im Zustand RESOLVED können bereits verwendet werden, indem andere Bundles ihr API benutzen. Es ist nicht erforderlich, jedes Bundle zu starten, denn dies dient einem besonderen Zweck: Mit dem Manifest Header Bundle-Activator wird der voll qualifizierte Name einer Klasse angegeben, die das Interface BundleActivator implementiert:

public interface BundleActivator {
    void start(BundleContext context);
    void stop(BundleContext context);
}

Wenn ein Bundle einen Activator spezifiziert, wird beim Starten vom OSGi Framework eine Instanz erzeugt und dessen start()-Methode und beim Stoppen dessen stop()-Methode aufgerufen. Da diese Aufrufe synchron vom Framework Thread erfolgen, ist es wichtig, dass sie so schnell wie möglich zurückkehren, um die Ausführung des Systems nicht zu blockieren. Lang laufende Vorgänge sollten daher in eigenen Threads ausgeführt werden.

Beim Starten wird das Bundle zunächst in den Zustand STARTING versetzt. Wenn ein Activator spezifiziert ist, wird dessen start()-Methode aufgerufen und wenn diese erfolgreich zurückkehrt, wird das Bundle auf ACTIVE gesetzt, andernfalls wieder auf RESOLVED. Analog verhält es sich mit dem Stoppen. Um die Startup-Performance zu verbessert, kann ein Bundle mit dem Manifest Header Bundle-ActivationPolicy spezifizieren, dass die Aktivierung lazy erfolgen soll. Dann wird der Aufruf der start()-Methode solange verzögert und das Bundle verbleibt solange im Zustand STARTING, bis zum ersten Mal eine Klasse aus dem Bundle geladen wird.

Wenn ein Bundle deinstalliert wird, geht es zunächst in den Zustand UNINSTALLED über. Sofern es keine Packages exportiert, die im Wiring verwendet wurden, wird es sofort aus dem persistenten Speicher des OSGi Frameworks gelöscht. Andernfalls bleiben die Package-Exports bis zu einem Neustart des OSGi Frameworks erhalten, sodass durch das Deinstallieren eines Bundles nicht automatisch alle abhängigen Bundles funktionsunfähig werden.

Beim Aktualisieren wird ein Bundle erneut eingelesen und persistiert. Dabei bleibt die Bundle ID erhalten, wohingegen durch Deinstallieren und anschließendes erneutes Installieren eine neue Bundle ID vergeben wird. Das OSGi Framework versetzt das Bundle beim Aktualisieren wieder in den Ausgangszustand, sodass z.B. ein ursprünglich gestartetes Bundle zunächst gestoppt, dann erneut eingelesen und aufgelöst und danach wieder gestartet wird. Auf diese Weise kann ein Softwaresystem im laufenden Betrieb feingranular – nämlich auf Ebene von Bundles – aktualisiert werden. Mit anderen Worten bietet OSGi ein Hot Deployment auf Modulebene.

Life Cycle Management

Wie kann nun der Lebenszyklus von Bundles gesteuert werden? Dazu bietet das OSGi Framework ein schlankes API an, das im Wesentlichen aus den folgenden drei Klassen besteht: BundleContext, Bundle, BundleListener.

Der BundleContext stellt die einzige Schnittstelle zur Interaktion mit dem OSGi Framework dar. Um eine Referenz auf den BundleContext zu erhalten, muss ein Activator verwendet werden, in dessen start()- und stop()-Methoden der BundleContext für das jeweilige Bundle übergeben wird. Mit dessen installBundle()-Methoden kann ein Bundle im OSGi Framework installiert werden. Als Rückgabe erhalten wir eine Referenz vom Typ Bundle, mit der wir den Lebenszyklus des Bundles steuern können, z.B. mit den Methoden start(), stop(), update() und uninstall(). Um den Lebenszyklus beliebiger Bundles zu verfolgen und gegebenenfalls darauf zu reagieren, können BundleListener über den BundleContext beim OSGi Framework registriert werden. An diese werden BundleEvents versendet, welche Informationen über die Änderung im Lebenszyklus enthalten.

Wichtig: Es gibt kein API im OSGi Framework zur Steuerung des Resolving. Dazu gibt es einen optionalen Standardservice, den Package Admin Service, der Operationen wie refreshPackages() und resolveBundles() anbietet. Sowohl Eclipse Equinox als auch Apache Felix enthalten diesen Service.

Für das Life Cycle Management bieten die OSGi-Implementierungen in der Regel so genannte Management Agents als Bestandteil der Implementierung oder als separate Bundles an. Eclipse Equinox enthält die Equinox Console und Apache Felix bringt per Default die Bundles Shell und Shell Text UI mit. Beides sind textbasierte Management Agents mit sehr ähnlichen Funktionen. Darüber hinaus gibt es weitere Management Agents auf Basis von Swing oder webbasierte Agents. Letztendlich kann jedes Softwaresystem auf Basis von OSGi einen eigenen Management Agent in Form eines oder mehreren Bundles implementieren. Da wir für unser Beispiel Apache Felix verwenden, stellen wir in Tabelle 2 die wichtigsten Befehle für dessen Text Shell vor.

Tabelle 2: Wichtige Befehle der Apache Felix Text Shell
Unser Beispiel, 4. Iteration

Damit unser Beispiel bereits in diesem Stadium aktiv werden kann, fügen wir einen Activator (Listing 1) hinzu, der in der start()- und stop()-Methode Tracing-Ausgaben nach System.out schreibt. Weiter erzeugen wir beim Starten eine Instanz des InMemoryContactRepository und geben dessen Kontakte nach System.out aus.

Listing 1
public class Activator implements BundleActivator {
    public void start(final BundleContext context) {
   System.out.println(MessageFormat.format("[{0}] starting ...", context
                .getBundle().getSymbolicName()));
        final ContactRepository repository = createRepository();
        for (final Contact contact : repository.getAllContacts()) {
            System.out.println(contact);
        }
    }
    public void stop(final BundleContext context) {
      System.out.println(MessageFormat.format("[{0}] stopping ...", context
                .getBundle().getSymbolicName()));
    }
    private ContactRepository createRepository() {
        ...
    }

Die zugehörige bnd-Datei muss nun noch um die Bundle-Activator-Direktive erweitert werden, die – abgesehen von der Umformatierung in das spezielle Manifest-Format – in das Bundle Manifest übernommen wird:

# com.weiglewilczek.example.osgi.contacts.core.inmemory.bnd
Bundle-Version: 1.0.0
Private-Package: com.weiglewilczek.example.osgi.contacts.core.inmemory.internal
Bundle-Activator: 
  com.weiglewilczek.example.osgi.contacts.core.inmemory.internal.Activator

Das resultierende Bundle Manifest sieht nun folgendermaßen aus:

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-SymbolicName: com.weiglewilczek.example.osgi.contacts.core.inme
 mory
Bundle-Version: 1.0.0
Import-Package: com.weiglewilczek.example.osgi.contacts.core,org.osgi.
 framework;version="1.4"
Bundle-Activator: com.weiglewilczek.example.osgi.contacts.core.inmemor
 y.internal.Activator

Nun ist es an der Zeit, unsere beiden Bundles im OSGi Framework zu installieren, zu starten, zu aktualisieren etc. Dazu starten wir zunächst Apache Felix mittels der Run Configuration org.apache.felix aus dem gleichnamigen Projekt. In der Konsole geben wir ps ein, um die aktuell installierten Bundles aufzulisten (Abb. 3).

Abb. 3: Installierte Bundles auflisten

Anschließend verwenden wir das Kommando install, um die Bundles zu installieren und listen danach wieder die installierten Bundles auf (Abb. 4).

Abb. 4: Bundles installieren

Unsere beiden neu installierten Bundles befinden sich nun im Zustand INSTALLED. Um sie aufzulösen, geben wir das Kommando resolve mit der ID des Implementierungs-Bundles ein. Dadurch wird auch das Kern-API-Bundle aufgelöst, um die Abhängigkeiten erfüllen zu können, d.h. unsere beiden Bundles befinden sich nun im Zustand RESOLVED (Abb. 5).

Abb. 5: Bundles auflösen

Nun starten wir das Implementierungs-Bundle mit dem Kommando start. Dadurch wird der Activator instanziiert und dessen start()-Methode ausgeführt, sodass die Tracing-Ausgabe sowie die Liste der Kontakte ausgegeben werden (Abb. 6).

Abb. 6: Bundles starten

Abschließend zeigen wir noch eines der OSGi-Highlights schlechthin: Wir aktualisieren das Implementierungs-Bundle im laufenden Betrieb. Dazu passen wir dessen Activator an, sodass er eine andere Menge von Kontakten enthält, lassen Bnd das Bundle erneut erstellen und führen dann in der Konsole das Kommando update aus.

Abb. 7: Bundles aktualisieren

Wie Abbildung 7 zeigt, wird das Bundle zunächst gestoppt, dann transparent aktualisiert und anschließend wieder gestartet. Und tatsächlich finden wir einen neuen Kontakt in der Ausgabe.

Schlussbemerkung und Ausblick

Wir haben die wichtigsten Details des Module und Life Cycle Layer kennen gelernt: Bundles sind JAR-Archive mit zusätzlichen Metadaten. Diese werden im Bundle Manifest spezifiziert und deklarieren u.a. die öffentliche Schnittstelle und die Abhängigkeiten eines Bundles. Das OSGi Framework bietet Bundles eine Laufzeitumgebung, wo diese u.a. installiert, gestartet und aktualisiert werden können. Dazu dient ein Management Agent der OSGi-Implementierung, z.B. eine textbasierte Konsole. Im nächsten Teil dieser Artikelserie werden wir die dritte wesentliche Eigenschaft von OSGi unter die Lupe nehmen: Das OSGi Service Model.


Heiko Seeberger ist als Technical Director für die Weigle Wilczek GmbH tätig. Sein technischer Schwerpunkt liegt in der Entwicklung von Unternehmensanwendungen mit OSGi, Eclipse RCP, Spring, AspectJ und Java EE. Seine Erfahrungen aus über zehn Jahren IT-Beratung und Softwareentwicklung fließen in die Eclipse Training Alliance ein. Zudem ist Heiko Seeberger aktiver Committer in Eclipse-Projekten, Autor zahlreicher Fachartikel und Redner auf einschlägigen Konferenzen.

Kommentare

Schreibe einen Kommentar

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