Teil 1

OSGi enRoute 1.0: Hintergründe, Architektur, Best Practices

Peter Kriens

@istock/logoboom

Das OSGi Framework enRoute ist im September 2015 in Version 1.0 erschienen. enRoute-Erfinder und OSGi-Mastermind Peter Kriens stellt das Projekt in einer zweiteiligen Artikelserie vor.

Das Ziel von OSGi enRoute ist es, anhand von Best Practices schnell in die OSGi-Entwicklung einsteigen zu können. enRoute ist ein API, eine Toolchain (bnd, Bndtools, Gradle, Git und Travis) und eine Distribution basierend auf Open Source Bundles, die es ermöglichen, OSGi-(Web-)Anwendungen für verschiedene Zielumgebungen zu bauen. enRoute ist zudem eine ausgezeichnete Umgebung, um zu lernen, wie die Nutzung von OSGi vorgesehen ist. Die beste Art und Weise, um in die Materie einzusteigen, ist, das OSGi-enRoute-Quick-Start-Tutorial durchzugehen, das sich auf der enRoute-Website befindet.

Im Folgenden wollen wir detailliert beschreiben, was OSGi enRoute heute anbietet und welche Verbesserungen geplant sind. Auf dem Weg dahin werden wir uns zudem zahlreiche OSGi Best Practices ansehen.

Hintergrund

Als Ericsson mich im November 1998 nach Raleigh NC schickte, hatte ich keine Ahnung, worauf ich mich da einließ. Open Source stand noch in seinen Kinderschuhen, Embedded bedeutete 256 K, Wifi war ein Funkeln in jemandes Augen, Mark Zuckerberg war erst vierzehn und lediglich gemein zu den Mädchen in seiner High School, Apple stellte den iMac vor, Windows 95 war das dominierende Betriebssystem, Kommandozeilen gerieten ins Hintertreffen und der Begriff „Cloud Design“ wurde nur im negativen Sinne gebraucht. Und was für jüngere Leser vielleicht fast unvorstellbar ist: Es gab noch keine Smartphones.

An jenem Tag in Raleigh saßen wir zusammen an einem Tisch: Ericsson, IBM, Sun und andere führende Unternehmen, um über Standards für Home Gateways zu sprechen. Kleine Computer sollten bald (!) in jedermanns Haus zu finden sein und von den Telco-Unternehmen betrieben werden. Das war es zumindest das, was sie uns sagten.

Das jähe Platzen der dot.com-Blase im Jahr 2001 (genau zu der Zeit, als OSGi v1 veröffentlicht wurde – wobei ich nicht glaube, dass wir etwas mit dem Börsencrash zu tun hatten) verscheuchte im Großen und Ganzen die Motivation, diese Spezifikationen weiterzuentwickeln. Allerdings hatten die Gründer der OSGi Alliance andere Verwendungsmöglichkeiten für die Specs gefunden und wollten die technische Arbeit aufgrund ihrer Qualität weiterführen. Die OSGi-Spezifikationen sind nur selten in Sackgassen geraten; fast in allen Situationen haben wir uns für den richtigen Weg entschieden – im Gegensatz zu so manch anderer Spezifikation, die hier nicht beim Namen genannt werden sollte. So schritten wir also weiter voran und veröffentlichten im Laufe der Jahre Version 2, Version 3 …

Einige Leser werden nun darauf hinweisen wollen, dass der Erfolg von OSGi im Enterprise-Segment durchaus gemischt ausfiel. Obwohl alle großen Application Server OSGi übernahmen, hat das „Bundle“ nie das „WAR“ ersetzt. Dafür gibt es Gründe. So zerstörte Open Source definitiv die ökonomische Landkarte, auf der sich die Gründungsunternehmen von OSGi bewegten; und das veränderte auch ihre Zielsetzungen. Es brauchte Zeit, bis sie Open Source akzeptierten und bis wir mit ihnen zusammenarbeiten konnten, und selbst heute scheinen sich nicht alle völlig mit Spezifikationen und Open Source anfreunden zu können.

Und es gibt auch einen technischen Grund. Die Tatsache, dass die Spezifikation heute noch existiert, ist zu einem guten Teil darauf zurückzuführen, dass wir dem State of the Art zu weit voraus waren (einige von uns kamen aus der Forschung und nicht aus der Entwicklung). Als ein Beispiel, wie nahe wir an den Grenzen des Machbaren waren: Bei Ericsson Research haben wir Java und ein Vorläufer von OSGi auf einer 8 MB Linux Box zum Laufen gebracht. Das war cool, mit Ausnahme eines kleinen Details: Es gab keinen Platz mehr für die Anwendungen. Wir verschoben die Grenzen in anderen Bereichen in ähnlicher Weise weiter nach vorne: bei der Dynamik, der Modularität und bei Services. Das war sehr cool, aber ungemütlich weit vor dem, was die Entwickler da draußen zu dieser Zeit gewohnt waren.

Doch der wichtigste Grund war wahrscheinlich, dass zwei miteinander unverträgliche Konzepte aufeinandertrafen. Als OSGi den Enterprise-Sektor betrat, stieß es sogleich auf die Java Enterprise Edition (EE). Während Java EE viele Domänen-APIs aufwies, die in OSGi nicht vorkamen, verfehlte Java EE zwei wichtige Architekturkonzepte, die OSGi ausmachten. Doch wir dachten zunächst, dass beides wie die sprichwörtliche Faust aufs Auge passen würde.

Das offensichtliche Konzept, das OSGi bereitstellte, war Modularität. OSGi brachte eine starke Kapselung und Kontrolle des Klassenpfads mit. Java-EE-Anwendungen endeten hingegen meist mit langen, verschlungenen Klassenpfaden, die völlig ungeschützt dalagen, schwierig aufzubauen und oft fehleranfällig waren. OSGi Bundles stellen hingegen solide Module bereit, die sauber voneinander abgegrenzt sind und ausreichend Metadaten enthalten, um vor dem Start die Konsistenz der Anwendung sicherzustellen. Dieser Teil wird von fast allen Java-Entwicklern geliebt, wenn sie sich zum ersten Mal mit OSGi beschäftigen. Selbst ausgesprochene OSGi-Hasser müssen zugeben, dass das Klassenladermodell herausragend ist.

Doch es gibt ein Aber: Der Nachteil von starken Modulen ist, dass sie eben genau dies sind: stark! Der Einsatz der Module in klassischen Java-Umgebungen enthüllte die Tatsache, dass fast alle Anwendungen den Klassenlader be- bzw. missbrauchten, um ihre eigenen Erweiterungsmechanismen zu schaffen (und Java hatte bereits eine große Zahl an eingebauten Klassenladerhacks für diesen Zweck).

Ein wenig mehr Hintergrund dazu: Objektorientierte Technologien wurden in den frühen Neunzigern dominant. Anfänglich waren diese sehr erfolgreich, obwohl das Versprechen der Wiederverwendbarkeit aufgrund der Koppelung nicht wirklich eingehalten wurde. In echten Systemen war fast jedes Objekt von einem anderen Objekt transitiv erreichbar. Javas hauptsächliche Innovation – Interfaces – änderte das, weil Interfaces die Kopplung zwischen Implementierer und Client aufhob. Jetzt hatten beide keine Abhängigkeiten mehr voneinander und konnten unabhängig voneinander auf typensicherere Art und Weise verwendet werden. Das Interface was lediglich der „Contract“ zwischen beiden.

Das Konzept der Interfaces war unbedingt nötig und eine sehr wünschenswerte Lösung – doch es erzeugte ein neues Problem. Da die Implementierung und der Client nicht mehr länger aneinander gekoppelt waren, musste irgendjemand sie zur Laufzeit zusammenbringen. Der Client konnte nicht über „new“ die Implementierungsklasse aufrufen, da dies die Koppelung zurückgebracht hätte. Dieser Umstand führte zu einer regelrechten Flickwerkindustrie von Lösungen in Java auf der Basis von Klassenlader. Zuerst gab es Propertys, dann die Suche nach Muster in Klassennamen, danach XML mit Spring, bis schließlich Java 6 eines Tages den Service Loader einführte – und heute scheinen CDI mit Annotationen die Lösung zu sein.

Leider sind all diese Tricks nicht sonderlich modular, was OSGi den Leuten mit „Class Not Found Exceptions“ mitteilte. Wenn Ihnen jemand sagt, dass Ihr Baby hässlich ist, ist es sehr viel einfacher, sich im Internet über den Überbringer der Botschaft zu beklagen, als die eigene Codebasis zu verändern und nach einer sauberen Lösung zu suchen. Die Tatsache, dass die Modularisierung einer Anwendung harte Arbeit bedeutet, ist bis heute sicherlich nicht ausreichend verstanden worden. Zu oft wurde OSGi dafür ungerechterweise kritisiert.

Wie erwähnt, stellte OSGi zwei fehlende Teile für Java-EE-Architekturen bereit. Neben der Modularität war dies das OSGi-Servicemodell. Dieses Servicemodell wurde entworfen, um das Problem der echt modularen Verdrahtung zur Laufzeit zu lösen. „Echt modular“ deshalb, weil OSGi ein Peer-to-Peer-Broker-Modell bereitstellt. Services werden von den Implementierern angeboten und an die Clients gebunden, wenn diese einen solchen Service anfragen. Allerdings wurden Services von den meisten Entwicklern noch weniger verstanden als die ganze Modularitätsthematik – und deshalb ignoriert. Die Tatsache, dass Services dynamisch waren, wurde leider nicht als Vorteil erkannt, sondern als potenzielles Risiko betrachtet.

In Zusammenarbeit mit vielen führenden Industrieplayern haben wir Spezifikationen geschrieben, um populäre Java-EE-Spezifikationen auf OSGi abzubilden. Doch dies führte uns zu dem Problem, dass diese Spezifikationen etwas „seltsam“ waren. Dinge, die sich in ein ganz einfaches Service-API übersetzen lassen würden, mussten auf eine Art und Weise erledigt werden, die in einer OSGi-Welt nicht ganz richtig erschienen. An dieser Stelle äußerten sich deutlich die unterschiedlichen Ansätze der beiden Systeme.

Auszeit

Wir hatten mit OSGi also ein sehr gutes Modell auf der Basis von Modulen und Services, doch in der Praxis war der Schritt zu groß für die meisten Enterprise-Entwickler. Das Ergebnis war, dass Java EE viele APIs entwarf, die nicht sehr konsistent waren und Modularität nicht richtig unterstützten.

Ein Beispiel: OSGi-Spezifikationen trennen strikt zwischen Konfiguration und API. Es hat sich gezeigt, dass das API, das wirklich gebraucht wird, in den meisten Fällen ziemlich klein ist. Doch die Konfiguration, die nötig ist, um jede Seite aufzusetzen, kann sehr zeitintensiv sein. Sagen wir, Sie müssen mit einer Queue kommunizieren. In einer klassischen App müssen Producer und Konsument sich auf einen Namen einigen und dem Queue Manager auf irgendeine Weise mitteilen, welchen Namen er benutzen soll. In OSGi kann man einen Service nutzen, der anonym von beiden Parteien genutzt wird; die Details liegen in der Konfiguration. Da das Konfigurationsmanagement standardisiert ist, funktioniert das in OSGi üblicherweise sehr gut. Es erstaunt mich häufig immer noch, wie dieser Mechanismus Module und Services vereinfachen kann und gleichzeitig beides sehr viel besser wiederverwendbar macht.

Im Jahr 2012 entschloss ich mich dafür, eine Auszeit zu nehmen. Nach so vielen Jahren der Spezifizierung, in denen sich die Branche so verändert hatte, hatte ich das Bedürfnis, zur Basis zurückzukehren. Mir fehlte das tiefe Verständnis, das man nur erwirbt, wenn man tatsächlich eine Technologie einsetzt, anstatt nur darüber zu lesen und am Wochenende damit herumzuspielen.

Ich baute während dieser Auszeit eine Webseite: jpm4j.org. Dabei verfolgte ich einen extremen OSGi-Ansatz: Alles sollte sauber servicebasiert sein und bis ins Letzte die Möglichkeiten von OSGi ausnutzen. Und um ehrlich zu sein, wollte ich mir auch selbst beweisen, dass ich auf all den Konferenzen keinen Unsinn erzählt hatte, wenn ich sagte, dass OSGi eine wunderbare Entwicklungsumgebung darstellt.

Das Ergebnis war gemischt. Einerseits konnte ich mir definitiv beweisen, dass OSGi wunderbar funktioniert, vor allem das Servicemodell (was meiner Meinung nach auch durch das aktuell viel zitierte, webbasierte Microservice-Modell deutlich werden wird, das auf demselben Konzept beruht – nur dass OSGi Services einfach um Größenordnungen leichtgewichtiger sind).

Andererseits stellte sich aber auch heraus, dass in OSGi eine ganze Reihe von Spezifikationen fehlten, die ich von Null auf neu erstellen musste. Zudem gab es viele andere nützliche Bundles, die nirgendwo verfügbar zu sein schienen.

Mir wurde zudem bewusst, dass der Aufbau einer Werkzeugkette und einer Laufzeitumgebung keine trivialen Dinge sind. Die Auszeit hat mir definitiv die Augen geöffnet, mit welchen Herausforderungen die Leute konfrontiert sind, wenn sie in die OSGi-Entwicklung einsteigen wollen.

Nun sucht die OSGi Alliance schon seit Langem nach Wegen, die Entwicklerfreundlichkeit von OSGi zu verbessern. Als ich meine Erfahrungen mit der Alliance teilte, entschlossen wir uns dazu, das Projekt OSGi enRoute zu starten, mit dem Ziel, den Einstieg in die OSGi-Entwicklung zu erleichtern.

OSGi enRoute

Zum jetzigen Zeitpunkt stehen für OSGi ein unglaublich elegantes Programmiermodell und ein hervorragendes Tooling zur Verfügung. Wenn man allerdings im Web sucht, ist man mit einem regelrechten Friedhof von toten Tutorials und faden Blog-Posts konfrontiert. Und es ist eine besorgniserregend große Menge an schlechten Ratschlägen in Umlauf. OSGi enRoute soll hiermit aufräumen und es wirklich einfach machen, OSGi auf die richtige Art und Weise einzusetzen.

Im Folgenden beschreiben wir OSGi enRoute aus den verschiedenen Blickwinkeln, die das Projekt umfassen.

Komponenten

Das OSGi-enRoute-Programmiermodell basiert auf Komponenten. Zugegeben – das ist ein sehr allgemeines Wort. Eine Komponente ist in unserem Kontext aber eine Declarative-Services-Komponente.

In der Praxis bedeutet das, das eine Komponente ein „Plain Old Java Object“ ist, das einige Annotationen beinhaltet. Auf den ersten Blick ähneln diese Annotationen CDI, und sie haben auch einen ähnlichen Zweck. Doch umfassen OSGi Services dynamische Abhängigkeiten und eingebauten Konfigurationssupport. Es stellt sich heraus, dass diese beiden Dimensionen im Vergleich zu einer statisch verdrahteten Welt zahlreiche Softwaremuster einfacher implementierbar machen.

Ein Beispiel: Eine einfache Komponente, die einen Speaker-Service bereitstellt, wird durch Configuration Admin konfiguriert, und hängt (dynamisch) vom EventAdmin-Service ab. Dies könnte etwa wie in Listing 1 aussehen.

@Component
public class MyComponent implements Speaker {

  @Reference
  EventAdmin  eventAdmin;

  interface @Config {
    int port();
    String host();
  }

  @Activate
  void start(Config config) {
  }
}

Komponenten sind recht befreiend, weil sie keine zentrale Registrierung verlangen. Sie wollen einen Listener auf einem Socket entwickeln? Dann schreiben Sie unmittelbar eine Komponente, und schon können Sie Ihren Server starten. Sie müssen warten, bis irgendwelche wichtige Services fertig sind? Fügen Sie einfach eine Referenz auf diese Services ein.

Komponenten sind also „billig“, fast so billig wie Objekte. Es gibt Systeme, die hunderttausende von Komponenten aufweisen. Wenn Sie niemals Services verwendet haben, dann sind diese neuen Dimensionen, die OSGi-Komponenten bereitstellen, schwierig zu erklären. Sie müssen mir vertrauen, dass sich die kleine Mühe wirklich mehrfach auszahlen wird.

Komponenten sind also der grundlegende Designbestandteil von OSGi enRoute. Sie abstrahieren einen Teil der Problemdomäne und kommunizieren mit anderen Komponenten über Services. Komponenten enthalten oft eine Reihe assoziierter Hilfsobjekte, die mittels Standard-Java-Mechanismen entwickelt werden. In der Praxis können eine signifikante Anzahl dieser Klassen package private bleiben.

Services

Komponenten registrieren im Allgemeinen einen Service. Ein Service ist nach seinem Interface benannt und verfügt über eine Reihe von Propertys, die ihn beschreiben. Verwechseln Sie einen Service allerdings nicht mit einem bloßen Objekt samt Interface. Ein Service hat einen Contract. In OSGi wird dieser Contract in einem Package spezifiziert. Wir benötigen ein Package, weil es in den allermeisten Fällen Hilfsobjekten und zusätzlicher Interfaces bedarf, obgleich gute Contracts versuchen, die Zahl dieser Zusatzkonstrukte zu begrenzen und die Packages so hochgradig kompakt zu halten.

Services sind seit 1998 Bestandteil von OSGi. Ein Service ist nützlich, weil er die Koppelung zwischen verschiedenen Parteien mit einer Menge wohldefinierter Operationen auf einen einzigen Punkt konzentriert. Beide Parteien können sich dann unabhängig voneinander entwickeln, solange sie nur ihrem Contract treu bleiben. Es ist schwierig, den Wert dieser Indirektion für große Systeme zu überschätzen, da Komplexität dazu tendiert, sich exponentiell zur Anzahl der Verbindungen eines Moduls zu entwickeln. Service Contracts erledigen für Module, was Interfaces für Klassen taten: Sie lösen das Problem der transitiven Koppelung.

Serviceorientierte Architekturen lagen vor einigen Jahren im Trend. Heute heißt die neue Mode: Microservices. Bei diesen Services handelt es sich allerdings um Web Services. Sie benötigen ein Kommunikationsprotokoll und sind indifferent gegenüber vielen wichtigen Dingen wie zum Beispiel dem Lebenszyklus des Service. Im Gegensatz dazu sind OSGi µServices extrem leichtgewichtig, vollständig dynamisch und empfänglich für die Verwaltung des Lebenszyklus. Obwohl Web Service, Microservices und µServices von denselben Vorteilen des Servicemodells profitieren, ist es nur das OSGi-Modell, das transparent auf Web- oder Microservices abgebildet werden kann.

Services sind die architektonischen Primitiven des OSGi-Designs. Das Servicediagramm liefert eine Architekturübersicht auf sehr hohem Niveau – es ist fast eine verdinglichte Architektur. Um diese Designs zu dokumentieren, haben wir über die Zeit eine Diagrammtechnik entwickelt (Abb. 1).

Abb. 1: OSGi-Servicediagramme

Abb. 1: OSGi-Servicediagramme

Verteiltes OSGi bedient sich des Servicemodells und ermöglicht es, Services in anderen Frameworks aufzurufen. Ein Topologiemanager entdeckt die Services in anderen Frameworks und registriert auf der Basis ihrer Policy ein Proxy zu einem Remote Service in der lokalen Registry. Da Services dynamisch sind, kann der Topologiemanager diese Services auch abmelden, wenn die Verbindung zum Remote-Framework verlorengeht. Das geschieht fast komplett transparent. Allerdings ist es nötig, dass Services im Hinblick auf die Verteilung entworfen werden. Vor der Verfügbarkeit von verteiltem OSGi verwendeten alle OSGi-Servicedesigns sauber gekapselte Objekte. Diese extrem objektorientierten Objekte sind allerdings recht schwierig zu deserialisieren, da die Implementierungsklassen private sind (gemäß der Modularitätsregel Nr. 1: verstecke Implementierungen!). Seit verteiltes OSGi aber an Bedeutung gewonnen hat, werden Services mit dem Konzept der Serialisierung im Hinterkopf entworfen. Das verbreitetste Muster ist momentan wohl, DTOs in einem API zu verwenden. DTOs sind Objekte mit öffentlichen Feldern, die ein kleines Set an Java-Typen nutzen, darunter andere DTOs. In OSGi enRoute stellen DTOs deshalb ein weiteres Schlüsselelement dar.

Services werden zur Laufzeit reifiziert. Wie im Declarative-Servicesbeispiel gezeigt, ist es trivial, Services für die Implementierung von Software-Pattern zu nutzen. Eines der populärsten Patterns ist das White Board Pattern, das ein Eckstein aller modernen OSGi-Services-APIs ist. In klassischem Java ist es üblich, ein Serverobjekt zu erhalten und dann ein Listener auf dieses Objekt zu registrieren. Beispielsweise muss man in Android zunächst den Location Manager haben und dann einen Location Listener registrieren, um die GPS-Positionen zu erhalten. Und wenn Sie ein sauberer Programmierer sind, sollten sie natürlich auch wieder aufräumen, wenn Ihr Code deaktiviert wird.

Wir kopierten zunächst dieses Listener Pattern im OSGi Http Service und im Log Service, merkten aber bald, dass es genügt, den Location Listener in der Service Registry zu registrieren. Ein anonymer Location Manager kann dann recht einfach alle Location Listener Services entdecken. Mit diesem Muster wird oft die Anzahl von Typen in einem Service Contraxt halbiert.

Diese Services zu reifizieren, bringt einen weiteren riesigen Vorteil: Visualisierung. Da die Services die Bestandteile einer Anwendung sind, bringt die Visualisierung der Services in einem Framework eine High-Level-Übersicht der Anwendungsarchitektur. OSGi enRoute beinhaltet das XRay-Apache-Felix-Web-Console-Plug-in, um die Services im Framework anzuzeigen (Abb. 2).

Abb. 2: Serviceanzeige via XRay-Apache-Felix-Web-Console-Plug-in

Abb. 2: Serviceanzeige via XRay-Apache-Felix-Web-Console-Plug-in

Dynamismus

Das obige Beispiel war komplett dynamisch. Es wird aktiviert, wenn der EventAdminService registriert wird, und deaktiviert, wenn dieser verschwindet. Wie Sie sehen können, kostet diese Dynamik nichts, da sie in den meisten Fällen gar nicht sichtbar wird. Nichtsdestotrotz können wir die Konfiguration zu jeder Zeit aktualisieren, weil die Komponente dynamisch ist. Wir brauchen uns nicht um die Initialisierungssequenz kümmern, da dies vom selben Dynamikmechanismus erledigt wird.

Ein solcher Dynamismus ist entscheidend in einer IoT-Welt, in der Software mit der realen Welt interagiert. Dynamismus scheint indes weniger wichtig in einer Enterprise-Welt, da in einem einzigen Computerprozess alles gleichzeitig lebt und stirbt. Aber auch dort gilt: In OSGi ist die Dynamik mit keinen Zusatzkosten verbunden. Doch in einer verteilten Welt wird die dynamische Herangehensweise wieder unabdingbar. Die meiste Enterprise-Software geht der Dynamik aus dem Weg, was oft in fehleranfälligen Systemen resultiert. OSGi rechnet hingegen mit Ausfällen, da es von Beginn an dynamisch konzipiert ist.

Stellen Sie sich zum Beispiel vor, dass Sie einen Service haben, der auf einer Reihe verschiedener Systeme implementiert ist, sie aber das Ergebnis des schnellsten Aufrufs zurückgeben wollen.

@Component(property="service.ranking=1000")
public class MyComponent implements Foo {

  @Reference
  volatile List<Foo>  foos=
    new CopyOnWriteArrayList<>();

  @Override
  Promise<String> foo(int n) {
    Deferred<String> deferred = new Deferred();
    AtomicBoolean    first = new AtomicBoolean(false);

    for ( Foo foo : foos ) {
      if ( foo == this )
        continue;

      foo.foo(n).then(
        (p)-> {
      if (first.getAndSet(true)==false)
        deferred.resolve( p.getValue() );
        return null;
    });
  }
  return deferred.getPromise();
}

Das überraschend kleine Stückchen Code aus Listing 2 reicht dafür tatsächlich aus. Die foo-Liste wird durch die Declarative Services Runtime automatisch mit den passenden Services aus der OSGi Service Registry befüllt. Wenn immer also die Payload-Methode aufgerufen wird, testen wir das letzte Set von Services, das sichtbar ist.

Packaging

Komponenten können in OSGi nicht direkt deployt werden. Sie müssen in Bundles gepackt werden. Bundles sind die Module von OSGi. Bundles enthalten Java-Packages (meistens Ordner in einem JAR/ZIP-File). Das Manifest in diesem JAR-File beschreibt, welche Packages dieses Bundle exportiert und welche Packages importiert werden. Nicht gelistete Packages sind private; sie sind nicht direkt für andere Bundles zugänglich.

Es gibt lediglich einen verbindlichen Header in OSGi, aber über die Zeit haben wir zusätzliche Manifest-Header hinzugefügt, um mehr Metadaten bereitstellen zu können. Diese sind in manchen Fällen nützlich, aber wohlgemerkt alle optional. Wir haben versucht, die Bundles so selbstbeschreibend wie möglich in einem einzigen ZIP-File zu gestalten.

Bundles werden zur Laufzeit reifiziert. Dies führte in den letzten Jahren zum Aufkommen des Extender Pattern. Da Bundles entdeckt werden können, ist es möglich, in ein Bundle hineinzuschauen, um zu sehen, ob Sie etwas Brauchbares finden. Ist das der Fall, können Sie Daten aus dem Bundle nehmen und damit anfangen, was Sie wollen. Die Absicht dieses Pattern ist es, Bundles zu erlauben, nur das Minimum des Gebrauchten bereitzustellen und alles Überflüssige in einem anderen Bundle zu belassen. Da der Observer den kompletten Lebenszyklus betrachten kann, kann er das Bundle sauber auseinanderreißen oder deinstallieren, welche Funktionalität auch immer er aus dem Bundle zieht.

Das idealtypische Beispiel der Extender Board Pattern sind die Declarative Services. Die Runtime der Declarative Services horcht auf Bundles, die aktiv werden. Sie prüft dann, ob die Bundles irgendwelche Declarative-Service-Komponenten enthält, indem sie auf das Manifest schaut. Wenn ja, wird das korrespondierende XML gelesen, und die Komponente wird entsprechend orchestriert. Wenn das Bundle gestoppt wird, werden alle Komponenten deaktiviert.

Ein anderes Beispiel ist der Configurator, der durch OSGi enRoute bereitgestellt wird. Es handelt sich dabei um einen Extender, der Konfigurationsdaten von Bundles liest. Das bloße Aktivieren eines Bundles veranlasst den Configurator, die Konfigurationsdaten aus dem Ordner /configuration im Bundle-JAR auszulesen. Diese Konfiguration wird dann mittels des Configuration Admin-Services installiert. Es gibt sogar ein spezielles Bundle Tracker Utility, um die Nutzung dieses Pattern zu vereinfachen.

Capabilities und Requirements

Nachdem wir mit einem Dependency-Modell auf der alleinigen Basis von Packages gestartet waren, haben uns diverse Parteien (die hier nicht genannt werden sollen) mehr oder weniger dazu gezwungen, Bundles und Fragmente als zusätzliche Dependency-Typen einzuführen. Nennen wir diese – also Package, Bundle, Fragments – „semantische Dependencies“. Um das Jahr 2005 herum begannen wir, ein Repository-Modell zu entwickeln, in dem diese semantischen Dependencies gut verstanden werden mussten, um die richtigen Artefakte aus einem Request zu erhalten. Diese semantischen Dependencies zu nutzen, fühlte sich sehr seltsam an, weil wir auch zusätzliche Dependencies wie die Version der VM, das Betriebssystem und die Hardware modellieren mussten. Als wir diese Idee noch weiter dachten, wurde schnell klar, dass die Dinge, von denen wir Abhängigkeiten haben wollten, grundsätzlich endlos waren.

Wir entwickelten deshalb eine Sprache, um so genannte „Capabilities“ und „Requirements“ zu repräsentieren. Diese Sprache ist ausdrucksstark und überraschend einfach. Eine Capability besteht aus einem Namespace, der den Typ der Capability definiert, und einer Menge an Properties (Key-Value-Paare). Ein Beispiel:

namespace  = 'osgi.wiring.package'
    { osgi.wiring.package=com.example.foo,version=1.2.3 }

Ein Requirement besteht aus einem Namespace und einer OSGi Filter Expression, also etwa:

  namespace  = 'osgi.wiring.package'
    (osgi.wiring.package=com.example.foo)

Obwohl die Sprache zum Ausdruck von Capabilities und Requirements sehr einfach aussieht, ist sie überraschend mächtig. Das liegt hauptsächlich daran, dass OSGi-Filter ziemlich komplett sind: Sie können jede mögliche Tiefe von Unterausdrücken umfassen und unterstützen Wildcards und Größenvergleiche.

Diese Arbeit an Repositorys floss zurück in die Codespezifikationen, und heute basieren alle OSGi-Framework-Implementierungen auf diesem Capability-Modell. Die Frameworks verdrahten die Bundles unter Nutzung der Capabilities und Requirements. Die Verbindungen werden im API reifiziert, sodass sie zur Laufzeit genutzt werden können, um herauszufinden, womit ein bestimmtes Bundle A gekoppelt ist.

Beispielsweise sind Web-Ressourcen Bundles, die Ressourcen wie JavaScript, CSS, HTML, Bilder etc. enthalten. OSGi enRoute stellt einen Webressourcen-Namespace bereit. Er erlaubt einem Webressourcen-Bundle, eine Capability zu liefern, die dann von einem Application Bundle benötigt wird. Die Webressourcen werden der Runtime hinzugefügt, sodass immer die richtige Version installiert ist.

In OSGi enRoute stellt das Capability-Modell ein weiteres architektonisches Schlüsselelement dar. Ein OSGi enRoute Bundle sollte vollständig die Capabilities beschreiben, die es liefert und die es benötigt.

Um dies für Java-Entwickler nun so einfach und natürlich wie möglich zu machen, haben wir im bnd-Werkzeug eine Menge unterstützende Funktionen eingebaut. Das Tool analysiert den Bytecode, um automatisch die Rquirements auf der JVM und die importierten Packages zu erzeugen. Wir haben in bnd außerdem Support für die Erzeugung von Requirements über Annotationen eingebaut.

Beispielsweise kann eine spezifische Webressource eine Annotation entwerfen, die eine Version oder einen Versionsraum dieser Webressource benötigt. Das erlaubt es dem Java-Entwickler, der diese Webressource nutzt, eine Klasse mit einer einfachen Annotation auszustatten, um sicherzustellen, dass das Bundle die korrekten Requirements aufweist. Eine Annotation, um beispielsweise die Bootstrap-CSS-Webressource anzufordern, wird automatisch den nötigen Require-Capability-Header im Bundle-Manifest hinzufügen:

 @RequireBootstrapWebResource(resource = "css/bootstrap.css")

Deployment

Es gibt zwei Arten von Deployments. Die gebräuchlichste Art, die sozusagen exklusiv der Java-Welt angehört, ist das klassische WAR-Modell. Dabei ist vorgesehen, die Anwendung in einem Java-EE-Application-Server zu deployen, der die Anwendung mit allem Nötigen versorgen soll. Die Applications WAR kann darauf bauen, dass die Umgebung sie mit einem reichhaltigen API ausstattet; sie muss jede zusätzliche Abhängigkeit indes selbst mitbringen.

Die andere Art des Deployments ist die eines ausführbaren JARs, wo die einzige Abhängigkeit die JVM ist. In diesem Fall muss das JAR alle Abhängigkeiten enthalten, inklusive ein OSGi Framework. Da der Trend in Richtung selbstständigeren Einheiten geht (man denke etwa an Docker), legt enRoute den Fokus auf ausführbare JARs. Bald schon werden wir aber Exportfunktionen für die populärsten Application Server bieten, die OSGi unterstützen.

In einem Komponentensystem liegt das Ziel darin, die Komponenten so wiederverwendbar – und daher entkoppelt – wie möglich zu machen. Java-Entwickler fühlen es intuitiv, wann immer sie Interfaces statt Klassen verwenden sollten. Es ist so viel einfacher, wenn man Klassen entwirft und den Code dann in verschiedenen Anwendungen nutzen kann. Wie gesagt: Entkoppelung ist befreiend.

Koppelungen sind indes wie Luftballons. Man kann die Luft an einer Stelle nur so herauspressen, dass man sie irgendwo anders hinverschiebt. Extrem entkoppelte Komponenten verlagern einen Teil des Problems in die Phase des Zusammensetzens, während dieser ein Set an Komponenten ausgewählt werden muss, um die Anwendung zu bilden. Das ist wirklich ein schwieriges Problem, da sichergestellt werden muss, dass jede Komponente korrekt in der Runtime ankommt und keine ihrer Annahmen verletzt wird.

Beispielsweise nutzt Maven transitive Dependencys, um einen Klassenpfad zu ermitteln. Transitive Dependencys können allerdings verschiedene Versionen haben. Die Reihenfolge in solchen Klassenpfaden kann also einen Unterschied im Runtime-Verhalten machen. Das Schlimmste ist, dass Maven als einzigen Dependency-Typ den Modulnamen verwendet. Das verlagert vor allem die Last auf den Entwickler, zu verstehen, was sie tatsächlich in ihrem Klassenpfad aufgeführt bekommen. Angesichts der heute üblichen Wiederverwendung von Software, kann das bedeuten, dass man die Implikationen von hunderten von Komponenten verstehen muss.

Da wir ein feingranulares Capability-Modell in OSGi haben, können wir diesen Prozess in weiten Teilen automatisieren. Auf der Basis einer Menge an initialen Requirements kann ein Resolver das minimale Set and Bundles errechnen, die nötig sind, um die zusammengesetzten Requirements zu erfüllen.

Fazit

Wie eingangs erwähnt, besteht OSGi enRoute aus einem API, einer Toolchain und einer Open-Source-Distribution. Zudem werden Best Practices in Form verschiedener Tutorials und Beispielapplikationen vorgestellt. Während wir uns im ersten Teil dieser Serie mit den Hintergründen zu OSGi und den architektonischen Ecksteinen des enRoute-Projekts beschäftigt haben, wollen wir im nächsten Teil das enRoute-Basis-API im Detail vorstellen sowie die Basis-Distro und das Tooling unter die Lupe nehmen. Bis dahin sei jeder eingeladen, sich OSGi enRoute genauer anzusehen. Alle Quellen inklusive der Webseite sind auf GitHub verfügbar. Feedback in Form von Issues oder Pull Requests sind mehr als willkommen!

Geschrieben von
Peter Kriens
Peter Kriens
Peter Kriens ist seit 1990 als unabhängiger Consultant tätig. Zurzeit arbeitet er für die OSGi Alliance und am Package Manager jpm4j (www.jpm4j.org). Während der 80er Jahre entwickelte er in der Zeitungsbranche fortgeschrittene verteilte Systeme mittels der zu dieser Zeit sehr neuen objektorientierten Technologien. Aufgrund dieser Erfahrung wurde er von zahlreichen internationalen Unternehmen engagiert, darunter Adobe, Intel, Ericsson und IBM. Während seiner Arbeit bei Ericsson Research 1998 wurde er in die Spezifizierung von OSGi involviert; später avancierte er zum hauptsächlichen Verfasser dieser Spezifikationen. 2005 wurde ihm der Titel OSGi Fellow verliehen. Nach seiner Auszeit im Jahr 2012, die er der Entwicklung von jpm4j widmete, kehrte er zur OSGi Alliance zurück, um die Verbreitung von OSGi zu fördern.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: