![]() |
![]() |
|
![]() |
![]() |
Home Kontakt Links English |
|
Einführung in die OSGi R4 Service PlattformGrundlegendesDie OSGi R4 Service-Plattform ist eine Spezifikation für ein Service-orientiertes und komponentenbasiertes Framework für Anwendungen. Die Spezifikation erfolgt durch die OSGi-Allianz, zu der mehrere Expertengruppen gehören:
Ebene L0 spezifiziert eine minimale Java-Umgebung, die zur Ausführung der Plattform benötigt wird. Dabei könnte es sich um eine JavaSE oder auch eines der Java ME Geräteprofile (z.B. für mobile Geräte) handeln. Die nächste Ebene L1 führt das Konzept der isolierten Module/Services/Komponenten ein, die als "Bundles" bezeichnet werden. Bundles können gegenseitig, sorgfältig durch das Framework kontrolliert, auf ihre Klassen zugreifen. Dies wird durch die gemeinsame Nutzung von Java-Paketen erreicht und beinhaltet eine Versionenverwaltung. Ebene L2 kümmert sich um den Lebenszyklus dieser Bundles innerhalb eines sogenannten Bundle-Repositories und macht einen Neustart der virtuellen Maschine unnötig. Darauf aufbauend bietet Ebene L3 ein Service-Modell zur Entkopplung von Bundles und bietet ein Benachrichtigungssystem, das Informationen über den Start und die Beendigung von Bundles bekannt gibt. Bundles (L1)Ein Bundle wird als einzelne jar-Datei ausgeliefert und besitzt einen eigenen Namensraum. Das Framework bietet einen separaten "Bundle-Classloader" für jedes Bundle. Dadurch werden die Klassen eines Bundles von den Klassen anderer Bundles isoliert. Um die Kommunikation durch gemeinsame Objekte zu ermöglichen, können Java-Pakete von Bundles zur gemeinsamen Nutzung freigegeben werden. Zu diesem Zweck hat jedes Bundle die gemeinsam genutzten Java-Pakete in seiner eigenen Manifest-Datei zu deklarieren. Das Manifest wird innerhalb der jar-Datei im Pfad "/META-INF/MANIFEST.MF" abgelegt und bietet die Anweisungen "Export-Package:", "Import-Package:" und "Require-Bundle:". Der Im- und Export der Java-Pakete von oder zu anderen Bundles wird hauptsächlich durch diese Anweisungen gesteuert. Um ein Paket von dem aktuellen Bundle in andere Bundles zu exportieren, muss eine Liste dieser Pakete durch "Export-Package:" deklariert werden. Durch den Export werden die betreffenden Pakete zur gemeinsamen Nutzung freigegeben und können danach auch von anderen Bundles genutzt werden. In dem Manifest anderer Bundles definiert eine Liste von Paketen hinter der "Import-Package:"-Anweisung, welche dieser Pakete importiert werden. Dies erlaubt den Import unabhängig von speziellen Bundles zu spezifizieren - die betreffenden Bundles müssen dann nicht in der Liste der benötigten Bundles mit angegeben werden. Nur wenn Sie sicher sind, dass alle Pakete eines Bundles importiert werden müssen, sollte dieses Bundle hinter der "Require-Bundle:"-Anweisung mit aufgelistet werden. Es ist wichtig zu verstehen, dass jedes Bundle seinen eigenen Raum von Klassen erzeugt, indem ein eigener Classloader benutzt wird. Zu Beginn enthält dieser Raum von Klassen nur die java.*-Klassen sowie Klassen, die das Bundle selbst besitzt, aber jedes importierte Paket fügt weitere Klassen hinzu. Exports können nur explizit und nur aus diesem Raum heraus vorgenommen werden. Alle importierten Klassen werden nicht durch den eigenen Classloader des Bundles geladen, sondern durch den Classloader desjenigen Exporteurs, der diese Klassen anbietet. Das nachfolgende vereinfachende1 Bild zeigt das grundlegende Suchkonzept, das zur Anwendung kommt, wenn eine Klasse (oder Ressource) geladen werden muss: Zuerst prüft der Bundle-Classloader, ob die gesuchte Klasse zu einem java.*-Paket gehört. Wenn ja, dann wird der Suchauftrag zum elterlichen Classloader (welches normalerweise der Bootstrap-Classloader der JVM ist) weitergeleitet. Im Gegensatz zur J2SE-Umgebung wird der Bundle-Classloader die Suche nicht fortsetzen, wenn der elterliche Classloader die Klasse nicht finden kann2. Falls die Klasse nicht zu einem java.*-Paket gehört, dann prüft der Classloader, ob sie zu einem Paket gehört, das durch eine "Import-Package:"-Anweisung importiert wurde. Falls ja, dann wird der Suchauftrag zu dem Classloader desjenigen Bundles delegiert, das dieses Paket exportiert. Falls nein, dann wird als nächstes geprüft, ob die gesuchte Klasse zu einem exportierten Paket eines derjenigen Bundles gehört, die durch die "Require-Bundle:" spezifiziert wurden. Falls ja, dann wird der Suchauftrag zum Classloader des zugehörigen Bundles weitergeleitet, das dieses Paket exportiert. Falls nein, dann wird die Klasse im Bundle-Klassenpfad des aktuellen Bundles gesucht. Dieser Klassenpfad wird durch die Manifest-Anweisung "Bundle-ClassPath:" gebildet. OSGi bietet einen Weg, Features zu einem Bundle später hinzuzufügen, ohne dass eine neue Version des Bundles erstellt werden muss. Zu diesem Zweck können Bundles durch sogenannte "Bundle-Fragmente" erweitert werden. Alle Fragmente eines Bundles laufen im Kontext des Bundles mit, d.h. für sie wird der Classloader desjenigen Bundles benutzt, zu dem sie gehören. Bundle-ManifestWie Sie bereits wissen, werden Abhängigkeiten zwischen Bundles in deren Manifest-Datei deklariert.
Um Ihnen einen einfachen ersten Eindruck zu vermitteln, wie ein Manifest aussieht,
schauen Sie sich bitte den folgenden Auszug aus einer Manifest-Datei an.
Dieser Auszug wurde durch den grafischen Manifest-Editor von Eclipse V3.3.0 erzeugt (nur die OSGi-spezifischen Anweisungen werden hier gezeigt):
Die "Bundle-ManifestVersion: 2" zeigt an, dass hier die R4 Semantik und Syntax verwendet wird. Der "Bundle-Name:" spezifiziert den Namen des Bundles in menschenlesbarer Form, aber das OSGi-Framework benutzt für Bundles als globalen Identifizierer und Namensraum den "Bundle-SymbolicName:" zusammen mit der durch "Bundle-Version:" angegebenen Version. Dieser globale Identifizierer wird als "Bundle-ID" bezeichnet und lautet in diesem Beispiel "de.luers_net.test.MyBundle1 (1.0.0)". Folglich kann keine andere Bundle-ID sowohl bzgl. des Namens als auch der Version identisch sein. Um dies sicherzustellen hat der symbolische Name (gemäß der üblichen Praxis bei der Benennung von Java-Paketen) mit dem umgekehrten Domänennamen des Herstellers zu beginnen - demjenigem, der in menschenlesbarer Form auch durch "Bundle-Vendor:" beschrieben wird. In diesem Beispiel spezifiziert die "Require-Bundle:"-Anweisung ein zweites Bundle mit dem global eindeutigen Identifizierer "de.luers_net.test.MyBundle2". Ein Versionsbereich ist ebenfalls angegeben und so ist die Abhängigkeit nur für MyBundle2-Versionen von "1.0.0" (inklusive!) bis "2.0.0" (exklusive!) gültig. Bei Bedarf ist das aktuelle Bundle in der Lage, jede Klasse zu laden, die durch das zweite Bundle exportiert wird, aber nur wenn es nicht zum Paket "de.luers_net.test.mybundle3.interf" gehört, das übrigens auch durch weitere andere Bundles exportiert sein könnte. In diesem Beispiel exportiert das aktuelle Bundle "de.luers_net.test.MyBundle1 (1.0.0)" ein einziges Paket: "de.luers_net.test.mybundle1.interf". Die letzte Anweisung "Bundle-ClassPath:" bestimmt den Klassenpfad des aktuellen Bundles. Wenn er spezifiziert ist, dann sollte ein einzelner "." hinzugefügt werden, damit Klassen im Root-Verzeichnis innerhalb der jar-Datei gefunden werden. Dieser Klassenpfad wird hier durch "library.jar" sowie dem Inhalt des Ordners "./lib/" innerhalb der aktuellen Bundle-jar-Datei erweitert. Bundle-Lebenszyklus (L2)Ein OSGi Bundle definiert die explizite Grenze eines Moduls und das Manifest des Bundles definiert explizit die Abhängigkeiten zu anderen Bundles. Das Framework gewährleistet die Einhaltung dieser Regeln indem es die Verwaltung der Bundles und deren Abhängigkeiten übernimmt. Das folgende Bild zeigt die Stati des Bundle-Lebenszyklusses: Nachdem ein Bundle installiert wurde, werden seine Abhängigkeiten automatisch aufgelöst. D.h. dass die von ihm benötigten und in seinem Manifest deklarierten Paketimporte gegen die von anderen Bundles exportierten Pakete geprüft werden. Das Framework prüft ebenfalls, ob die Abhängigkeiten aller benötigten Bundles erfolgreich aufgelöst werden konnten. Wenn alle Prüfungen erfolgreich verlaufen sind, dann werden die Ergebnisse im sogenannten Bundle-Repository abgelegt und der Bundle-Status auf "resolved" gesetzt. Nun kann das Bundle ohne weiteren Statuswechsel benutzt werden. Damit ist aber nicht gemeint, dass das Bundle selbst aktiv ist - es kann von anderen wie eine Library aufgerufen werden. Später, wenn die Eclipse-RCP-Umgebung diskutiert wird, werden Sie sehen, dass dies für RCP-Plugins vollkommen ausreicht Bundles jedoch, die einen Service darstellen, müssen gestartet werden, weil sie auf eingehende Nachrichten zu warten haben und
sich dafür vorbereiten müssen.
Zu diesem Zweck können Bundles einen sogenannten Aktivator anbieten, der das Interface
"org.osgi.framework.BundleActivator" zu implementieren hat.
Wenn ein Bundle gestartet wird, gelangt es in den Status "starting" und die Methode "void start(BundleContext context)"
seines Aktivators wird aufgerufen.
In dieser Methode bereitet ein Service normalerweise seine Transportschicht auf eingehende Anfragen vor.
Sobald dieser Aufruf erfolgreich zurückkehrt (d.h. es wurde keine Ausnahme geworfen), dann wird der Bundle-Status auf "active" gesetzt.
Natürlich ist es möglich, einen laufenden Service zu stoppen.
Wenn dies geschieht, dann wird der Status des Bundles dadurch auf "stopping" gesetzt und die Methode
"void stop(BundleContext context)" seines Aktivators aufgerufen.
Dies ist das Signal für den Service, alle neu eingehenden Anfragen abzuweisen, aktuell laufende kurz dauernde Anfragen abzuarbeiten,
alle länger dauernden Aktivitäten (z.B. seine Threads) zu beenden und alle von ihm belegten Ressourcen freizugeben.
Andere von ihm abhängige Services müssen ebenfalls entsprechend reagieren, aber dies ist ein Thema für später.
Nachdem das Beenden des Services erledigt ist, wird das Bundle wieder auf den Status "resolved" gesetzt.
Fortgeschrittene AbhängigkeitsverwaltungWie Sie gesehen haben, dreht es sich beim OSGi Framework meist um die Verwaltung von Abhängigkeiten. Etwas was hier bisher noch nicht diskutiert wurde ist, was passiert, wenn ein Import zu mehreren Exporten passt. Für alle Probleme, die die Auflösung von Abhängigkeiten betreffen, kommt eine Prioritätsliste zur Anwendung:
Unterstützung von mehreren Versionen bei Paketen und optionale PaketeBis hierher wurde gezeigt, dass Bundles einen symbolischen Namen besitzen und
dass es möglich ist, sich auf eine bestimmte Bundle-Version zu beziehen.
Die im- und exportierten Pakete hatten keine Versionen und deswegen wurde nur der Paketname zur Bestimmung der Abhängigkeiten herangezogen,
aber es gibt ebenso einen Weg spezielle Paketversionen zu im- und exportieren oder Pakete nur dann zu importieren, wenn sie präsent sind:
Die drei obigen Manifest-Zeilen zeigen hierfür ein Beispiel. Das Bundle versucht das Paket "de.luers_net.test.mybundle2.interf" mit einer Version ab "1.2.3" (ohne Versionsbegrenzung nach oben) zu importieren, aber die Auflösung dieser Abhängigkeit ist optional. Mit anderen Worten rechnet das Bundle damit, dass das Paket evtl. doch nicht verfügbar ist. Außerdem wird das Paket "de.luers_net.test.mybundle3.interf" benötigt, aber nur Versionen von "1.0.0" (exklusive!) bis "1.0.5" (inklusive!) werden akzeptiert. Wenn keine passende Version gefunden wird, dann wird die Abhängigkeitsauflösung nicht erfolgreich sein und das Bundle deswegen nicht benutzbar sein. Daneben exportiert das Bundle das Paket "de.luers_net.test.mybundle1.interf" in der Version "1.5.0". Explizite Paketversionen erlauben mehrere Versionen desselben gemeinsam genutzten Pakets gleichzeitig im Speicher zu halten. Dies wird benötigt, wenn rückwärtige Kompatibilität zu früheren Versionen implementiert werden soll. Apropos ... bitte beachten Sie die Benutzung von ":=" versus "="! Attributwerte (wie z.B. version) werden durch ein "=" zugewiesen. Wertangaben zu Anweisungen (wie z.B. resolution) werden davon unterschieden, indem ein ":=" benutzt wird. Ein weiterer Punkt ist die Angabe der Einbeziehung und Ausgrenzung von Versionen an den Bereichsgrenzen: "(" und ")" bedeuten Einbeziehung während "[" und "]" Ausgrenzung meinen. Re-Export von Bundles und optionale BundlesPakete, die durch "Import-Package:" importiert werden,
können durch "Export-Package:" re-exportiert werden und es wurde gezeigt,
dass optionale Paketimporte durch das Hinzufügen einer "resolution:=optional"-Anweisung möglich sind.
Die folgende Zeile zeigt, wie dasselbe auch für ein Bundle erreicht werden kann:
Das aktuelle Bundle versucht alle Pakete des Bundles "de.luers_net.test.MyBundle2" zu importieren, aber die Auflösung dieser Abhängigkeit ist nicht verpflichtend. Indem die Anweisung visibility mit einem Wert von reexport spezifiziert wird, werden die von MyBundle2 importierten Pakete durch das aktuelle Bundle exportiert. Probleme mit AbhängigkeitenWenn Sie eine ClassNotFoundException oder eine ClassCastException gemeldet bekommen (obwohl die betreffenden Klassentypen zu passen scheinen), dann ist wahrscheinlich ein Problem mit den Abhängigkeiten zu Bundles oder Paketen die Ursache. In solchen Momenten sollten Sie sich noch einmal das Abhängigkeits-Design der involvierten Bundles sorgfältig vergegenwärtigen. Schauen Sie sich dazu bitte das unten stehende Bild an: Wenn Bundle A eine Klasse X des Pakets p sucht, dann wird es hierfür die Bundles B, D und C prüfen - in dieser Reihenfolge, weil B eine kleinere Bundle-ID als C hat und weil D von C benötigt wird. Wird die Klasse X in B gefunden, dann wird sie benutzt. Dies bedeutet natürlich, dass X durch den Classloader von B geladen wird. Aber was passiert, wenn Bundle A nun eine Methode einer Klasse von Bundle C aufruft und diese ebenfalls ein Objekt der Klasse X zurückgibt? Diese Klasse X wurde durch den Classloader von Bundle D geladen und deshalb passen die beiden Klassen X nicht zueinander! Diese Situation tritt häufig auf, wenn gemeinsame Libraries benutzt werden - z.B. eine Logging- oder XML-Bibliothek oder eine Bibliothek für Datenbankzugriffe. Um diese Probleme zu vermeiden, sollten diese Libraries immer in ein eigenes Bundle verpackt werden (siehe unten). Hier ist die Bibliothek, die das Paket p implementiert, durch das Bundle X verpackt. Wenn Bundle A eine Klasse des Pakets p sucht und dafür den Classloader von B anspricht, dann wird dieser den Suchauftrag zum Classloader von Bundle X weitergeben. Entsprechendes passiert, wenn Bundle C auf dieselbe Klasse zugreifen möchte und so wird Bundle A niemals wieder ein Problem mit Klassen haben, die von Bundle X kommen. Nun versuchen Sie bitte zu erraten was passiert, wenn Bundle B eine rückwärts gerichtete Abhängigkeit zu Bundle A besitzt (siehe obiges Bild). Wenn Bundle A die Klasse X des Pakets p lädt, dann wird ihr Classloader diesen Auftrag zum Classloader von B delegieren, welcher dann wiederum zu A delegiert und so weiter, richtig? Mitnichten! OSGi legt fest, dass jeder Classloader zur Suche einer Klasse nur einmal angesprochen wird. Service-Registry (L3)Mit den Schichten L0 bis L3 existiert bereits ein Framework, dass isolierte Services unterstützt, die gestartet und beendet werden können. Was fehlt ist ein Verzeichnis über das potentielle Clients alle verfügbaren Services für einen bestimmten Zweck ermitteln können. Was ebenfalls fehlt ist ein Mechanismus, der die Clients über das Beenden und erneute Starten von Services benachrichtigt. Dies wird durch die Schicht L3 des OSGi Frameworks realisiert. Service-SeiteWird ein Service-Bundle gestartet, dann wird seine Aktivator-Methode "start(BundleContext)" aufgerufen. Über den übergebenen Kontext kann sich der Service bei der Service-Registry registrieren. Dies kann durch Aufruf der Methode "BundleContext.registerService(String, Object, Dictionary)" geschehen: (A)
Wenn ein Service-Bundle beendet wird, dann wird seine Aktivator-Methode "stop(BundleContext)" aufgerufen. Neben dem Stoppen aller Service-Aktivitäten und dem Aufräumen und Freigeben aller belegten Ressourcen hat der Service sich selbst bei der Service-Registry zu deregistrieren. Dies kann durch Aufruf der Methode "ServiceRegistration.unregister()" erreicht werden. Ein Service sollte die möglichen negativen Effekte von eventuell falschem Verhalten seitens der Clients minimieren; insbesondere falls ein Client seine Referenzen zum Service nicht freigibt, sobald der Service heruntergefahren wird. Zu diesem Zweck sollte nur ein Proxy-Objekt beim Aufruf der registerService-Methode übergeben werden. Wenn der Service dann heruntergefahren wird, dann sollten alle Referenzen in allen Proxy-Objekten durch den Service auf null gesetzt werden. Client-SeiteEin Client-Bundle hat immer damit zu rechnen, dass ein Service beendet wird oder neu startet. Es ist sehr wichtig all die entsprechenden Nachrichten korrekt zu verarbeiten, da der Client sonst hängengebliebene Referenzen (sogenannte "stale references") auf einen Service oder auf Teile davon verursachen könnte. Dies würde den Garbage-Collector daran hindern, den Speicher freizugeben und könnte es unmöglich machen, den Service zu aktualisieren oder erneut zu starten. Um die korrekte Verwendung von Services zu vereinfachen bietet OSGi die Klasse "org.osgi.util.tracker.ServiceTracker". Ein Client kann eine Instanz dieser Klasse mittels des Konstruktors "ServiceTracker(BundleContext, String, ServiceTrackerCustomizer)" erzeugen:
Um auf den Service zuzugreifen, muss die Methode "Object[] ServiceTracker.getServices()" aufgerufen werden. (D) Das zurückgegebene Array enthält alle Service-Objekte von allen überwachten Services. Wenn mindestens ein Objekt im Array zurückgegeben wird, dann kann der Client es auf das entsprechende Interface casten, das durch den Service implementiert wird und kann dann den Service durch dieses Objekt aufrufen. (E/F) Falls kein Service zurückgegeben wird so bedeutet dies, dass alle überwachten Services inzwischen heruntergefahren wurden. Wenn der Client keinen der überwachten Service mehr benutzen möchte, dann hat er "ServiceTracker.close()" aufzurufen. Dies muss spätestens in der stop-Methode seines Activators geschehen. Dabei sollte sichergestellt werden, dass diese Methode auch dann aufgerufen wird, wenn Ausnahmen geworfen werden. 1 Normalerweise ist es nicht notwendig, weitere Einzelheiten des Algorithmus zur Suche von Klassen zu kennen.
Nichtsdestotrotz sind die fehlenden Schritte im Bild ... 2 Das genaue Verhalten der Suche nach Klassen hängt aber von den Konfigurationsparametern der OSGi-Umgebung ab. Copyright (c) 2016: Jürgen Luers |