Suche
Einführung in Apache Tamaya

Konfiguration in Java

Thorben Janssen, Anatole Tresch

© Shutterstock.com / isak55

Konfiguration erlaubt es, das Verhalten eines Softwaresystems in kontrollierter Art und Weise zu verändern, ohne dass eine Neuimplementierung notwendig ist. Dazu sind Designentscheidungen darüber zu treffen, welche Aspekte konfigurierbar sind und welche eben nicht. Alles konfigurierbar zu machen, ist genauso sinnlos, wie alle Aspekte fest zu kodieren. Dennoch scheiden sich die Geister daran, was Konfiguration nun ist und wie diese gemanagt werden soll. Wir möchten mit dieser mehrteiligen Artikelreihe Licht ins Dunkel bringen.

Konfiguration ist ein häufiges und sehr facettenreiches Thema in der Softwarewelt. Auch wenn es bisher keine einheitliche Lösung für Konfiguration in Java gibt, wurden im letzten Jahr einige Anstrengungen unternommen, dies zu ändern. Daraus ist das neu gegründete Projekt Apache Tamaya entstanden, mit dem eine Lösung für die vielfältigen Aufgaben der Konfiguration entwickelt werden soll.

Im Rahmen dieser Artikelreihe werden wir die Themen Konfiguration und Apache Tamaya genauer betrachten. Schnell wird sich zeigen, dass jeder eine Vorstellung davon hat, was Konfiguration ist und leisten soll. Unglücklicherweise werden sich kaum zwei Ansichten als deckungsgleich zeigen, was auch mit der Vielzahl der Anwendungsfälle zusammenhängt. In dieser Ausgabe werden wir uns zuerst sehr einfachen Einsatzszenarien im Java-SE-Umfeld widmen und dann im weiteren Verlauf immer neue Anforderungen aus anderen Einsatzbereichen, wie Java EE, Microservices und Cloud-basierten Anwendungen, hinzufügen. Ziel ist es, die Komplexität verständlich zu veranschaulichen, aber auch Gemeinsamkeiten aufzuzeigen. Diese erlauben es, Konfigurationsaspekte in einem Framework zu modellieren, so wie es aktuell mit Apache Tamaya gemacht wird.

Konfiguration mit Java SE

Lassen Sie uns zunächst die Konfigurationsmechanismen näher betrachten, die bereits mit Java geliefert werden. Das prominenteste Beispiel sind sicherlich .properties-Dateien, die sowohl in Textform als auch in xml-Form unterstützt werden. Dazu bietet die Klasse java.util.Properties Methoden, um einen entsprechenden InputStream zu lesen. Anschließend können die konfigurierten Werte mit getProperty(key) ausgelesen werden:

InputStream is = ...;
Properties properties = new Properties();
properties.read(is); // properties.readXml(is);

Derselbe Mechanismus wird nun auch an anderen Stellen im JDK wiederverwendet, z. B. im Zusammenhang mit ResourceBundle oder dem java.util.ServiceLoader. Letzterer erlaubt es, Services zentral zu registrieren. Diese werden dabei in Dateien, die dem qualifizierten Typ des Zugriffs entsprechen, unter META-INF/services im Klassenpfad abgelegt. Der ServiceLoader berücksichtigt hierbei alle Dateien, die aktuell im Klassenpfad sichtbar sind. Der Zugriff auf die Services erfolgt sodann typsicher mit

ServiceLoader<T> ServiceLoader.load(Class<T> type);

Weitere Mechanismen sind Environment- und System-Properties. Während Erstere aus den Umgebungsvariablen der Laufzeitumgebung konstruiert werden, können Letztere als Parameter der Java-Runtime übergeben werden, z. B. mit -DmyProperty=foorBar. Mit System.getenv() und System.getProperties() stehen entsprechende Zugriffs-APIs zur Verfügung, mit denen die Werte direkt ausgelesen werden können.

Es würde den Rahmen dieses Artikels sprengen, jeden Konfigurationsmechanismus zu beschreiben, der im JDK bereits mitgeliefert wird. Dennoch liefern die Beispiele erste Anhaltspunkte, wie Konfiguration modelliert werden könnte:

  • Konfigurationsdateien sind textbasiert.
  • Konfiguration kann in sehr unterschiedlichen Formaten verfügbar sein (properties, xml, json etc.).
  • Konfiguration kann aus unterschiedlichen Quellen stammen (Dateisystem, Klassenpfad, URLs, etc.).
  • Konfiguration kann auf mehrere Ressourcen aufgeteilt sein, wobei zur Laufzeit die effektive Auswahl getroffen wird.
  • Konfiguration kann dazu verwendet werden, höherwertige Dienste zur Verfügung zu stellen (z. B. ServiceLoader, ResourceBundle).
  • Mit Konfiguration können einfache Werte in Abhängigkeit ganz unterschiedlicher Laufzeiteigenschaften dynamisch gesetzt werden; oder es werden ganze Systemteile und Komponenten für die Zielumgebung aktiviert oder deaktiviert.

Bereits mit den wenigen vorhandenen Mechanismen können wir ganze Applikationen konfigurieren. Allerdings muss immer noch sehr viel Handarbeit geleistet werden, und es fehlen wichtige Abstraktionen. Einige Beispiele dafür sind:

  • Die Entwicklung von Applikationen benötigt ein Staging-Konzept, besonders, wenn gemeinsam genutzte Ressourcen wie Datenbanken und zentrale Services ins Spiel kommen. Dies ermöglicht es, unterschiedliche Fremdsysteme anzusprechen, je nachdem, ob die Applikation in einer Test- oder einer Produktionsumgebung ausgeführt wird.
  • Das Auffinden von Konfigurationsdateien und -ressourcen ist auf statische Ressourcen im Klassenpfad, Dateien und URLs beschränkt.
  • Es existieren keine Mechanismen zum Überschreiben von Werten, zum Definieren von Standardwerten und ähnliche Mechanismen.
  • Bei wachsender Größe der Anwendungen ist eine Entwicklung in Modulen und mit mehreren unabhängigen Teams der Normalfall. In diesem Zusammenhang sind mehrstufige Konfigurationssysteme unabdingbar, die das Überschreiben von Einträgen erlauben.

Geht man diesen Weg konsequent weiter, so landen wir unweigerlich bei Systemen, bei denen die Logik auf verschiedene Knoten verteilt wird und die Kommunikation über Netzwerkprotokolle stattfindet. Das bringt uns direkt zum nächsten Bereich: Java EE.

Konfiguration in Java EE

Wenden wir uns also dem aktuellen Java-EE-Stack zu. Grundsätzlich setzt sich die Java-EE-Plattform aus einer Menge unterschiedlicher Java Specification Requests (JSRs) zusammen, die in einem koordinierten Prozess alle drei bis vier Jahre einem Major-Update unterzogen werden. Aktuell sind die Arbeiten an EE 8 in vollem Gange. Entsprechend konzentrieren wir uns zunächst auf den aktuellen EE-7-Stack (soweit sinnvoll, werden wir Bemerkungen zu aktuellen Entwicklungen einfließen lassen). Die verschiedenen JSRs definieren dabei eine Fülle von Konfigurationsdateien. Die Übersicht zu bewahren, ist zugegebenermaßen nicht leicht. Daran wird sich leider mit großer Wahrscheinlichkeit auch in EE 8 nichts ändern.

Konfiguration in Java EE unterteilt sich grundsätzlich in Konfiguration, die mit dem deployten Artefakt mitgeliefert wird (applikatorische Deployment-Konfiguration) und Konfiguration, die den Server konfiguriert (administrative Ressourcen). Da Applikationskonfiguration ein Bestandteil des Deployment-Artefakts ist und es keine Möglichkeit gibt, diese von außerhalb hinzuzufügen, muss sie bereits zur Bauzeit vollständig bekannt sein. Dabei unterstützt, mit Ausnahme von Java Batch, praktisch keine Spezifikation Platzhaltermechanismen, die einige dieser Einschränkungen hätten mildern können. Soll also eine Konfiguration z. B. in einer Datenbank verwaltet werden, so stehe ich praktisch alleine da oder muss anbieterspezifische Produkte zukaufen. Dass dynamische Konfiguration zur Laufzeit nicht unterstützt wird, ist eine logische Konsequenz. Wir möchten aber auch anmerken, dass wir uns der Komplexität dynamischer Laufzeitänderungen in einem Java-EE-Container sehr wohl bewusst sind. Aber es gibt auch Ausnahmen: Vor allem die von Red Hat betreuten Spezifikationen sind vergleichsweise flexibel. Meist wird zwar auch hier die Einbindung einer Konfigurationslösung nicht direkt unterstützt, aber die jeweiligen SPIs (z. B. die Extension SPI in CDI) sind so flexibel, dass wir basierend auf diesen Konfigurationserweiterungen relativ einfach implementieren können.

Beim zweiten Teil, den administrativen Ressourcen, gestaltet sich die Situation nicht besser. Diese sind definiert und müssen von den Applikationen entsprechend angefordert, also in den Deployment-Deskriptoren referenziert werden. Allerdings werden die Ressourcen vom Applikationsserver anbieterspezifisch verwaltet und zur Verfügung gestellt. Man kann es auch mit anderen Worten formulieren: Die Applikation gehört dem Server und eben nicht umgekehrt.

Staging/Environment Configuration

Ein weiterer, verbreiteter Anwendungsfall im Enterprise-Umfeld ist die Verwendung unterschiedlicher Konfigurationsprofile in verschiedenen Umgebungen. So ist in der Testumgebung z. B. der verwendete Datenbankserver unter einer anderen IP zu erreichen oder eine andere Cluster-Konfiguration zu verwenden.

Bei komplexeren Anwendungen, die aus verschiedenen Deployments bestehen, stellt sich zusätzlich noch die Frage, ob alle mit demselben Profil konfiguriert werden oder ob jede Anwendung über ein eigenes verfügt (Single Application, Multiple Deployments).

Andere Aufgabenstellungen ergeben sich, wenn eine Anwendung für mehrere Kunden oder in unterschiedlichen Rechenzentren eingesetzt wird. Hierbei verfügt in vielen Fällen jede Installation über spezifische Konfigurationseinstellungen und Anpassungen. Diese sollten geändert werden können, ohne dass die Applikation erneut gebaut werden muss.

Ähnlich verhält es sich, wenn die Anwendung aus verschiedenen, wiederverwendeten Komponenten besteht, die für den jeweiligen Anwendungsfall und Kunden konfiguriert werden müssen.

Für all diese Fälle bietet Java EE nur wenig Unterstützung. In JSF finden sich Ansätze, das Staging zu modellieren. Das Konzept ist aber zu unflexibel und auf JSF beschränkt. Am meisten hilft hier das so genannte altdd-Feature. Es erlaubt durch Setzen einer zusätzlichen System-Property so genannte Override-Konfigurationen zu aktivieren, die Stage-spezifisch Werte beisteuern und auch überschreiben können. Zusammenfassend kommt Java EE recht unflexibel daher:

  • Die Konfiguration administrativer Ressourcen ist anbieterabhängig. Hier gilt es, JSR 373 zu beobachten, der für EE 8 ein RESTful-Konfigurations-API für das Servermanagement definieren soll.
  • Applikations-Deployment-Konfigurationen können kaum von extern beigesteuert werden oder müssen alle komplett mit der Applikation verpackt werden.
  • Platzhalter und Ersetzungen werden kaum unterstützt.

Ein Lichtblick ist CDI, welches mit dem mächtigen Extension-SPI die Injection von Konfigurationen stark erleichtert. Mit CDI ist es problemlos möglich, applikatorische Konfigurationen wie die folgende direkt zu injizieren:

@Inject @ConfiguredProperty("my.own.property")
private String myProperty;

Diese kann über entsprechende Producer auch dynamisch aus externen Quellen angezogen werden. Allerdings gibt es auch hier aktuell keinen Konfigurationsstandard.

Weitere Architekturen und Cloud

Lassen Sie uns den Faden noch etwas weiter spinnen: Stellen wir uns eine moderne Microservice-Architektur vor, die auf Basis einer vollständigen Cloud hoch dynamisch, flexibel und ausfallsicher operiert. JVM-Instanzen laufen dort in kleinen Containern, z. B. auf Basis von Docker-Images. Also das, was Google, Twitter, Netflix und andere bereits seit Jahren sehr erfolgreich praktizieren. In einer solchen Umgebung ergeben sich eine ganze Reihe weiterer Anforderungen an die Dynamik, den Geltungs- und den Sichtbarkeitsbereich der Konfigurationsdaten.

Durch die transparente Verlagerung von Anwendungen oder Komponenten auf andere Server sowie das Hinzufügen und Entfernen von Anwendungsinstanzen muss die Konfiguration der Anwendung jederzeit angepasst werden können. Auch reaktive Mechanismen verlangen eine dynamische Anpassung der Laufzeitumgebung (z. B. Autothrottling, Short-Circuits, …).

Wird die Anwendung als Software as a Service betrieben, werden auch besondere Anforderungen an die dynamische Anpassung der Konfiguration gestellt. Während in anderen Umgebungen die Konfiguration vor dem Start der Anwendung definiert wurde, müssen in diesen Systemen auch tiefgreifende Änderungen wie das Hinzufügen neuer Mandanten zur Laufzeit durchgeführt werden können. In vielen Fällen werden diese Systeme in der Cloud betrieben, sodass sich interne Konfigurationsänderungen aufgrund zusätzlicher Mandanten mit externen Änderungen aufgrund von Autoskalierung und Elastizitätsmechanismen kombinieren.

In derart komplexen Systemen ist für einige Konfigurationsdaten die Unterscheidung von Geltungsbereichen notwendig, um z. B. die Sichtbarkeit von oder den Zugriff auf Konfigurationsdaten für die verschiedenen Teilsysteme einzuschränken. Ein anderes Beispiel kann bei Systemen mit Mehrmandantenfähigkeit die Trennung der Konfigurationsdaten zwischen unterschiedlichen Mandanten sein. Dabei unterscheidet sich der Inhalt der Konfiguration je nach angemeldetem Nutzer.

Oftmals ist es in großen Systemen auch sinnvoll, einer bestimmten Benutzergruppe nur Ausschnitte einer Konfiguration zugänglich zu machen, um den Entwickler und Anwender vor unnötiger Komplexität und Implementationsdetails zu schützen.

Somit können wir für dynamische Umgebungen und Architekturen, wie sie in der Cloud und in auf Microservices basierenden Systemen zu finden sind, die Anforderungen wie folgt zusammenfassen:

  • Es werden hohe Anforderungen an die Anpassungsdynamik gestellt, um die Konfiguration jederzeit an die veränderten Ausführungsbedingungen anzupassen.
  • Auch tiefgreifende Konfigurationsänderungen müssen zur Laufzeit erfolgen können.
  • Konfigurationsdaten sollten für unterschiedliche Geltungsbereiche definiert werden können, um verschiedene Konfigurationen gleichzeitig verwenden zu können.
  • Die Sichtbarkeit von Teilen der Konfiguration sollte eingeschränkt werden können, um eine zu hohe Komplexität zu vermeiden.

Konfigurationsmanagement

Um die verschiedenen Konfigurationen zu verwalten und zur Verfügung zu stellen, sind unterschiedlichste Lösungen denkbar und je nach Anwendungsfall notwendig. Die meisten dürften sich gut mit dateibasierten Konfigurationen auskennen, die während des Build- oder des Deployment-Prozesses der Anwendung bereitgestellt werden. Diese sind für Java-SE- und Java-EE-Anwendungen weit verbreitet. Im Java-EE-Bereich wären auch andere Formen des Konfigurationsmanagements wünschenswert, allerdings aufgrund der bereits beschriebenen Einschränkungen nicht möglich.

So lassen sich Konfigurationen auch in einer Datenbank oder einem verteilten Datengrid (z. B. Hazelcast) ablegen, oder sie werden als Key-Value-Paare automatisch auf alle Systeme verteilt (z. B. etcd, Zookeeper). Auch können Konfigurationen dynamisch, z. B. in Form von Chef-/Puppet-Skripten, vorliegen. Diese Ansätze ermöglichen es, die Konfigurationsdaten zentral zu verwalten und automatisch auf alle Teilsysteme zu verteilen oder durch diese abfragen zu lassen. Entsprechend muss Konfiguration auch versioniert und über das Netzwerk serialisiert werden können. Eine dynamische Verwaltung der Konfigurationsdaten ohne Single Point of Failure wird vor allem für stark verteilte und dynamische Anwendungen benötigt, wie sie z. B. bei Cloud-basierten Systemen entstehen. Die zentrale Visualisierung der aktuell aktiven Konfiguration des Gesamtsystems und der verschiedenen Anwendungsinstanzen in einer Managementkonsole ist hierbei absolut notwendig.

Es wird deutlich, dass typischerweise mit steigender Komplexität der Anwendung und der Ausführungsumgebung auch die Komplexität der Anforderungen an das Konfigurationsmanagement steigt. Während für viele Anwendungen eine einfache Verwaltung von Konfigurationsdateien ausreicht, sind diese für hochkomplexe Systeme oft nicht ausreichend.

In diesem Artikel haben wir einen ersten Eindruck erhalten, welche Vielfalt und auch Komplexität hinter dem Wort „Konfiguration“ steckt. Dazu haben wir die Anforderungen in Java SE, Java EE und in anderen Architekturen betrachtet und einen zugegebenermaßen sehr summarischen Überblick über die bestehenden Mechanismen in Java SE und EE gegeben (mehr Details können online nachgelesen werden). Da es in diesem Bereich nach wie vor an Standards mangelt, hat wohl auch schon jeder selbst eine für ihn passende Konfigurationslösung implementiert oder aus den am Markt verfügbaren Lösungen ausgewählt. Einige dieser Lösungen werden wir im nächsten Artikel genauer betrachten und ihre Unterschiede und Gemeinsamkeiten darlegen. Darauf aufbauend werden wir uns in den darauffolgenden Artikeln Apache Tamaya zuwenden. Dieses Projekt hat sich nichts Geringeres zum Ziel gesetzt, als Konfiguration für Java zu vereinheitlichen und mit einem schlanken, aber extrem modularen Bausatz effektiv zu unterstützen.

Aufmacherbild: Web server data on a monitor via Shutterstock.com / Urheberrecht: isak55

Geschrieben von
Thorben Janssen
Thorben Janssen
Thorben Janssen arbeitet als Seniorentwickler und Architekt für die Qualitype GmbH und entwickelt seit mehr als zehn Jahren Anwendungen auf Basis von Java EE. Er ist Mitglied der JSR 365 Expert Group und bloggt über Themen rund um Java Enterprise auf http://www.thoughts-on-java.org. Twitter: @thjanssen123
Anatole Tresch
Anatole Tresch
A​natole Tresch studierte Wirtschaftsinformatik und war anschließend mehrere Jahre lang als Managing Partner und Berater aktiv. ​Die letzten Jahre war ​Anatole Tresch als technischer Architekt und Koordinator bei der Credit Suisse​ tätig. Aktuell arbeitet Anatole ​als Principal Consultant für die Trivadis AG, ist Specification Lead des JSR 354 (Java Money & Currency) und PPMC Member von Apache Tamaya. Twitter: @atsticks
Kommentare

Schreibe einen Kommentar

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