Softwareentwicklung und Architektur/ 04.05.2021 / Holger Tiemeyer

Flexibilität auf Mikroarchitekturebene in Java mit OSGi und dem Modulsystem

Flexibilität und Modularität sind zwei wichtige Merkmale moderner Softwarearchitekturen. Anstelle eines großen, in sich geschlossenen Monolithen bestehen sie aus dezentralen Einzelbausteinen. Diese lassen sich nach Bedarf austauschen oder auch neu kombinieren, um die Anwendungen und Funktionalitäten des Systems dynamisch zu verwalten. Die vorliegende Artikelserie beschreibt, wie sich Flexibilität in Java-Softwarearchitekturen erreichen lässt. Der zweite Teil zeigt auf, welche Rolle OSGi und das Modulsystem in Java 9 hierfür spielen.    

Ein Service ist eine autarke Einheit, die über eine klar definierte Schnittstelle Funktionalitäten nach außen anbietet (engl. provides). Diese können von Service-Konsumenten benutzt (engl. uses) werden. Mit der Einführung des Modul-Systems in Java wurde der seit der Version 6 im JDK vorhandene ServiceLoader überarbeitet. Starre Modulabhängigkeiten, die mittels der exports- und requires-Anweisungen entstehen, lassen sich in dem neu eingeführten Konzept durch angebotene und konsumierende Services auflösen.

Modularisierung von Java-Architekturen mit OSGi

Eine erweiterte Möglichkeit, Module auch zur Laufzeit aufzufinden und für die Ausführung zur Verfügung zu stellen, ist die Modularisierung mit OSGi. Um die beschriebene Komplexität der flexiblen Austauschbarkeit von Java-Modulen und Anwendungslogiken beherrschbar zu machen, existieren zwei grundlegende Konzepte: Modularisierung und Abstraktion. Auf diesen basiert das Komponentenmodell OSGi. Die von der OSGi Alliance bereitgestellte Spezifikation (siehe [5]) definiert eine dynamische, Java-basierte Softwareplattform, deren Kern einzelne Services bilden.

Bereitgestellt werden die Services dabei in speziell aufbereiteten JAR-Dateien, sog. Bundles. Diese können sich aus Java-Klassen und weiteren Ressourcen zusammensetzen (Konfigurationsdateien, abhängigen Jar-Dateien, usw.). Notwendige OSGi-spezifische Meta-Informationen werden in einer Manifest-Datei angegeben. Dazu gehören der Bundle-Name, die Version des Bundles, importierte und exportierte Packages sowie von dem jeweiligen Bundle benötigte Abhängigkeiten (Requirements) und angebotene Definitionen (Capabilities).

Während die Security-Schicht die Java-eigenen Sicherheitsmechanismen ergänzt, wird über die Modul-Schicht die Modularisierung der Anwendung ermöglicht. Dabei muss jedes Bundle explizit angeben, welche Bundles es importiert und welche Packages es selbst exportiert. Da jedes Bundle einen eigenen Class-Loader besitzt, können so mehrere Services unabhängig voneinander in einer Java Virtual Machine (JVM) laufen.

Die Lifecycle-Schicht stellt dagegen eine API bereit, welche die Installation und Deinstallation sowie das manuelle, programmatische oder automatisierte Starten und Stoppen der Bundles ermöglicht. Besonderer Vorteil hierbei ist, dass OSGi-Frameworks das sogenannte „Hot-Deployment“ unterstützen: Bundles können jederzeit zur Laufzeit während des Betriebs des Gerätes installiert, gestartet, beendet oder ausgetauscht werden. So lassen sich Änderungen an den einzelnen Anwendungen vornehmen – ohne dass dies mit einem kurzzeitigen Ausfall des Gerätes oder der laufenden Software verbunden ist.

Die Service-Schicht ist dagegen für die Kommunikation der einzelnen Bundles untereinander zuständig sowie die Entkopplung der Schnittstellen der Services von ihrer Implementierung. Hierfür kann in einem Bundle ein einfaches Java-Interface bei einer zentralen Service-Registry als Schnittstelle registriert werden, beispielsweise in der start()-Methode der BundleActivator-Klasse. Diese wird ausgeführt, sobald ein Bundle startet. Auf diese Weise werden die einzelnen Services abstrahiert, wodurch unter anderem die Komplexität sinkt.

Aus diesem Aufbau ergeben sich zusätzliche Vorteile für die Verwendung in flexiblen Umgebungen: So sieht die OSGi-Spezifikation vor, eine stabile API für ein Bundle zu erzeugen und die Service-Implementierung gegen diese vorzunehmen. Die Idee dabei ist, dass die API anderen Lebenszyklen unterliegt als die Service-Implementierungen. Auf diese Weise lassen sich für jede API mehrere Service-Implementierungen zur Verfügung stellen, die sich in ihrer Version und Funktionalität unterscheiden.

OSGi ermöglicht die Trennung von API- und Service-Implementierung

Ein Vorteil in der Verwendung von OSGi für flexible Mikroarchitekturen ergibt sich aus der strikten Trennung von API- und Service-Implementierungen. Dies geschieht durch die Bereitstellung von Bundles, die zum einen die API bereitstellen und zum anderen die entsprechende Implementierung der API. Insbesondere in der Kommunikation der Bundles untereinander entfaltet OSGi anhand der Trennung der API von der Service-Implementierung sowie einer zentralen Service-Registrierung sein Potential: Bundles können dynamisch zur Laufzeit durch die Service-Registry anhand ihrer mitgelieferten META-Informationen zueinander in Beziehung gebracht, registriert, de-registriert, gestartet und gestoppt werden. Besonders hervorzuheben ist dabei, dass sich hinter einer gegebenen API mehrere Service-Implementierungen dynamisch zur Laufzeit laden lassen – unter anderem auch in unterschiedlichen Versionen und Ausprägungen.

Durch die zuletzt genannte Besonderheit unterschiedlicher Bundle-Versionen kann ein Bundle für zu importierenden Artefakte die benötigten Versionen explizit angeben; dies beispielsweise auch als Intervall mit oder Höchst- bzw. Mindestwert. Dank des OSGi-eigenen Versionierungssystems können so mehrere Versionen eines Bundles gleichzeitig nebeneinander laufen. Sollen nun beispielsweise für die EU-Region verschiedene Algorithmen hinzugefügt werden, können diese somit parallel in Betrieb genommen werden. Durch diese Trennung ergeben sich folgende Vorteile:

  1. Hinter der stabilen API-Definition können unterschiedliche Ziel-Implementierungen stehen. Dabei entscheidet das OSGi-Service-Framework durch die Auswertung der MANIFEST-Dateien über die Service-Registry zur Laufzeit, welche Implementierung gerade benötigt wird.
  2. Es können für jede Implementierung eine Vielzahl von Versionen existieren. So kann beispielsweise eine Implementierung des Moduls in den Versionen 1.0.0 und 1.5.0 vorliegen. Benötigt ein Service nun die Version 1.0.0, so kann dies in der Requirements-Definition innerhalb des entsprechenden Bundles definiert werden. Ein anderer Service kann dagegen beispielsweise die Version 1.5.0 anfordern. Die Definitionen werden zur Laufzeit ausgewertet und passende Bundles zueinander über die stabile API in Beziehung gesetzt.

OSGi eignet sich demnach hervorragend für die Reduktion der Komplexität und damit für den Einsatz in flexiblen Microservice-Umgebungen. Dies liegt zum einen an dem dynamischen Komponentenmodell. In diesem lassen sich Bundles jederzeit austauschen, hinzufügen oder auch parallel in verschiedenen Versionen betreiben. Und andererseits daran, dass die strikte Trennung der Serviceimplementierungen von ihren Schnittstellen eine einfache Abstraktion ermöglicht.

Eine ausführliche Version dieses Artikels mit praxisnahen Implementierungen und Codebeispielen wird voraussichtlich in Ausgabe 03-2021 der Fachzeitschrift Java aktuell erscheinen.

Einblicke

Shaping the future with our clients