Wir bauen uns unsere Konfigurationslösung

Apache Tamaya mit Modulen erweitern

Anatole Tresch, Thorben Janssen

© Shutterstock.com/Yuri Turkov

In den vorherigen Teilen dieser Serie haben wir uns mit den Anforderungen an Konfiguration befasst, bereits vorhandene Lösungsansätze bewertet und die Grundkonzepte hinter Apache Tamaya näher betrachtet. Nun möchten wir die wichtigsten Erweiterungsmodule genauer vorstellen und darauf aufbauend eine mächtige und flexible Konfigurationslösung für Java-SE-Anwendungen erstellen.

Die Erweiterungsmodule von Apache Tamaya sind recht vielfältig, und es kommen immer wieder neue hinzu. Dabei achtet das Projektteam besonders darauf, die folgenden Aspekte einzuhalten:

  • Module dürfen keine Abhängigkeiten zur Implementierung aufweisen, d. h., sie benutzen nur das API/SPI.
  • Module dürfen Funktionen anderer Erweiterungsmodule benutzen, dies sollte jedoch möglichst nicht notwendig (Standalone-Erweiterungen) oder falls es sich nicht vermeiden lässt, so minimal wie möglich gehalten werden.
  • Wenn möglich, sollten Module mit Java 7 bauen, damit diese sowohl mit Java 7 als auch mit Java 8 eingesetzt werden können.
  • Erweiterungsmodule implementieren möglichst nur einen isolierten Aspekt, sodass diese möglichst frei zu einer angepassten Konfigurationslösung assembliert werden können.
  • Erweiterungsmodule sollten im Sinne von „Convention over Configuration“ möglichst einfach integriert werden können. In den meisten Fällen genügt es, diese einfach als Abhängigkeit hinzuzufügen.

So weit, so gut. Aber welche Funktionen werden denn nun angeboten? Die nachfolgende Auflistung gibt einen guten Überblick:

  • tamaya-injection stellt Annotationen und Mechanismen zur Verfügung, um Konfiguration direkt zu injecten bzw. „proxyfizierte“ Konfigurationstemplates aus annotierten Interfaces zu erzeugen.
  • tamaya-format stellt eine einfache Abstraktion zur Verfügung, welche es erlaubt, Konfigurationsformate von der Abbildung auf PropertySources zu trennen.
  • tamaya-json erweitert Tamaya mit der Möglichkeit, JSON-Dateien einzulesen.
  • tamaya-resources erlaubt es, Ressourcen mittels Ant Patterns zu lokalisieren und unterstützt die entsprechende Implementierung von PropertySourceProvidern. Diese lesen die identifizierten Ressourcen in den entsprechend passenden Formaten aus und kombinieren sie zu PropertySources.
  • tamaya-resolver stellt einen Auflösungsmechanismus und ein SPI zur Verfügung, welcher es erlaubt, mit Platzhaltern in den Konfigurationswerten zu arbeiten.
  • tamaya-functions stellt diverse nützliche funktionale Erweiterungen zur Verfügung.
  • tamaya-events erweitert Tamaya mit einer Abstraktion, um den Zustand einer Konfiguration serialisierbar zu machen und Änderungen mittels entsprechender Ereignisse entweder lokal oder auch remote zu aktualisieren.
  • tamaya-model erlaubt es, einzelnen Modulen ihre unterstützte Konfiguration zu deklarieren und somit diese einfach zu validieren und zu dokumentieren.
  • tamaya-management implementiert JMX Beans, welche den Zugriff auf Konfiguration über Management-APIs erlauben.
  • tamaya-server implementiert einen einfachen Konfigurationsserver, welcher Konfiguration als JSON-Strukturen zur Verfügung stellt.
  • tamaya-remote implementiert PropertySources, welche die Konfigurationsdaten vom Servermodul beziehen.

In den folgenden Abschnitten gehen wir nun detaillierter auf einige Erweiterungen ein.

tamaya-injection – DI für Konfigurationsdaten

Das tamaya-injection-Erweiterungsmodul bietet die Möglichkeit, auf Basis von Annotationen eine Konfiguration in eine Klasse zu injecten bzw. Konfigurationsklassen auf Basis von Interfaces zu erzeugen.

Listing 1 zeigt ein Beispiel für eine annotierte Klasse. Wie man sieht, werden die einzelnen Eigenschaften der Klasse mit Annotationen versehen, die den Zugriff auf die Konfigurationsdaten sowie deren weitere Verwendung beschreiben. Mithilfe der @ConfigurationProperty-Annotation werden z. B. die Schlüsselwerte benannt, unter denen die Konfigurationsdaten für diese Eigenschaften in der Konfiguration hinterlegt sind. Wenn, wie in Listing 1 zu sehen, mehrere Schlüsselwerte angegeben werden, prüft Tamaya diese in der angegebenen Reihenfolge und verwendet den ersten gefundenen Wert. Wenn auf die Verwendung der @ConfigurationProperty verzichtet wird, versucht Tamaya den Konfigurationswert auf Basis von Property-Name, Klasse und Package-Name aufzulösen.

Mit der @WithPropertyConverter-Annotation kann ein PropertyConverter für die Konvertierung des Konfigurationswerts benannt werden, der für die Typkonvertierung verwendet wird. Ist keiner benannt, verwendet Tamaya den passenden Standardkonverter.

Des Weiteren kann mit der Annotation @DefaultValue ein Standardwert für die Eigenschaft definiert werden bzw. mit @NoConfig der Wert aus der Konfiguration ausgeschlossen werden.

Damit Tamaya die Annotationen auswerten und die Eigenschaften mit den entsprechenden Konfigurationswerten füllen kann, ist ein weiterer Schritt notwendig. Ein Objekt dieser Klasse muss an den ConfigurationInjector übergeben werden (Listing 2).

public class ConfiguredClass{

  // Konfigurationswert wird basierend auf Property-Name, Klasse 
  // Package-Name aufgelöst
  private String backendName;

  // Verwendet Standardkonverter zur String -> URL Konvertierung
  @ConfiguredProperty(keys={"backend.url","backend.v1.url"})
  @DefaultValue("http://127.0.0.1:8080/res/api/v1/info.json")
  private URL backendUrl;

  // Konfiguration deaktiviert
  @NoConfig
  private int i;

  // Verwendet einen eigenen Konverter
  @WithPropertyConverter(MyCustomBigDecimalConverter.class)
  private BigDecimal bigNumber;

  ...
}
ConfiguredClass classInstance = new ConfiguredClass();
ConfigurationInjector.configure(configuredClass);

tamaya-format – eine Abstraktion für Konfigurationsformate

Konfigurationsdaten liegen, je nach Anwendungsfall und organisatorischen Vorgaben, in den verschiedensten Formaten vor. Manche Konfigurationsdateien verwenden einfache Schlüssel-Wert-Paare, andere (z. B. XML und JSON) bilden hierarchische Strukturen und Gruppierungen ab. Das Lesen verschiedener Formate ist unabhängig vom einzelnen Anwendungsfall. Das tamaya-format-Modul löst dies, indem es ein Zwischenformat (ConfigurationData) definiert, das die gelesenen Daten enthält. Aus diesem wird dann eine, oder bei komplexen Formaten auch mehrere, PropertySourcen erzeugt, die von Tamaya in die Konfiguration gelesen werden.

tamaya-resources – Ressourcen a la Ant

Das tamaya-resources-Modul bietet die Möglichkeit, Ressourcen deklarativ wie in ant zu beschreiben, z. B classpath:mycompanyconfig/**/*.json. Diese Funktionalität ist sehr hilfreich, wenn man alle Konfigurationsdateien innerhalb eines Verzeichnisses lokalisieren will, um z. B. daraus einen entsprechenden PropertySourceProvider zu implementieren (dazu wird jede gefundene Datei auf mindestens eine PropertySource abgebildet). Das Modul definiert dazu ein entsprechendes ConfigResources Singleton und entsprechende abstrakte Basisklassen.

tamaya-builder – Wir bauen uns die Konfiguration selbst

Das tamaya-builder-Modul bietet die Möglichkeit, eine Configuration programmatisch und unabhängig vom aktuellen Konfigurationssetup zu erzeugen. Dazu bietet die ConfigurationBuilder-Klasse u. a. verschiedene Methoden, um PropertySources miteinander zu kombinieren und ProperyFilter hinzuzufügen (Listing 3). Auch bereits im aktuellen ConfigurationContext bekannte PropertySources und PropertyFilter können hinzugefügt werden. Dadurch erhält der Entwickler die vollständige Kontrolle über die Erzeugung und den Lebenszyklus der Konfiguration.

ConfigurationBuilder builder = new ConfigurationBuilder();
Configuration config = builder.addPropertySources(mySource)
        .addPropertySources(anotherSource)
        .addPropertyFilters(new MySpecialFilter());
        .build();

tamaya-resolver – Werteplatzhalter

Häufig werden Platzhalter in Konfigurationsdateien verwendet, um Konfigurationsdaten abhängig von anderen Konfigurationsparametern zu definieren. Das tamaya-resolver-Modul unterstützt dazu die in Tabelle 1 dargestellten Resolver. Wenn diese nicht ausreichen, besteht zusätzlich die Möglichkeit, eigene Resolver für eigene Präfixe zu implementieren. Dazu muss lediglich das ExpressionResolver-Interface implementiert und die Klasse beim ServiceLoader registriert werden. Anschließend kann das neu geschaffene Präfix in der Konfiguration verwendet und mithilfe des Resolver verarbeitet werden.

janssen_tamaya_tab1

tamaya-functions – funktionale Erweiterungen

Tamaya definiert mit ConfigurationOperator und ConfigurationQuery zwei einfache, aber mächtige funktionale Erweiterungspunkte. Das tamaya-functions-Modul benutzt diese, um diverse nützliche Hilfsfunktionen zu implementieren, wie z. B. das Filtern bestimmter Konfigurationsabschnitte oder Schlüssel, das Zusammenstellen der existierenden Sektionsnamen oder einfache Abbildung nach JSON und XML.

Das Servermodul erlaubt es, die Konfiguration und/oder Teile davon über ein REST-API aufzurufen. Das Server-SPI erlaubt es uns dabei auch, den genauen Rückgabewert über einen ConfigurationOperator zu beeinflussen. Das machen wir uns zunutze, um nur einen Ausschnitt als Remote-Konfiguration zur Verfügung zu stellen, der sich aus den Einträgen aus client.defaults-*, client.clientId.* zusammensetzt. Die zurückgelieferte JSON-Struktur ist in Listing 4 gezeigt.

HTTP localhost:8888/config/scoped/CLIENT

configuration": {
  "class": "org.apache.tamaya.core.internal.DefaultConfiguration",
  "timestamp": 1440602189581,
  "request": {
    "scope": "CLIENT"
  }
  "data": {
    "name": "client1",
    "owner": "Anatole Tresch",
    "deputy": "Thorben Jannsen",
    "app.title": "First Client",
    "app.bg-color": "white",
    "app.fg-color": "black",
    "port": 8080,
    ...
  }
}

<h2

Nun benutzen wir einige der vorgestellten Module, um eine einfache, aber schon recht verteilte Konfigurationslösung zusammenzubauen. Diese besteht aus einem Server und zwei Microservices (-Clients), welche ihre Konfigurationen vom Server beziehen. Die Clients werden dabei nichts anderes tun, als ihre Konfiguration mit einem einfachen Servlet anzuzeigen. Die vorab vorgestellten Module werden wir benutzen, um den Server zu implementieren und den Client mit dem Server zu verbinden.

Clientseitig werden wir das tamaya-remote-Modul verwenden, um den Client mit dem Server zu verbinden und die Clientkonfiguration vom Server zu beziehen. Dabei benutzen wir ein Environment Property, um dem Client seine Client-ID zu übergeben. Dazu implementieren wir eine Subklasse von AbstractRemotePropertySource (Listing 5).

public class RemoteClientPropertySource extends AbstractRemotePropertySource{
  public RemoteClientPropertySource(){
  super("http://localhost:8888/config/scoped/"+
    System.getenv().get("CLIENT_ID"));
   }
}

Auf der Serverseite wollen wir folgenden Funktionen bieten:

  • Konfiguration kann im Klassenpfad unter META-INF/config/*.ini abgelegt werden.
  • ini-Dateien erlauben es, zusätzlich einen Header festzulegen, den wir für die Clientkonfiguration nutzen wollen. Header soll dabei für clientspezifische Einstellungen die Form [client.clientId] haben und [client.defaults] für Default-Werte.
  • Es soll auch möglich sein, Konfiguration als System Property beizusteuern.

Wir verwenden das Servermodul, um die Konfiguration per REST-Call für die Clients verfügbar zu machen. Dabei nutzen wir den Scope-Mechanismus des Servermoduls, welcher es erlaubt, einen ConfigurationOperator zwischenzuschalten. Dieser kombiniert in unserem Falle die verschiedenen Konfigurationsbereiche für globale Default-Werte (client.defaults) und clientspezifische Properties (client.clientId) zu einer neuen Clientkonfiguration. Hier erweisen sich die Erweiterungen des functions-Modul als sehr nützlich (Listing 6). Der implementierte Scope wird sodann noch in einen ScopeProvider gesteckt (hier nicht gezeigt) und per ServiceLoader registriert.

import static org.apache.tamaya.functions.ConfigurationFunctions.*;

public class ClientScope implements ConfigOperator{
  private String clientId;
  ...
  public Configuration operate(Configuration config){
    return combine(
      config.with(section("client.defaults"),
      config.with(section("client. "+clientId));
  }
}

Damit nun die oben beschriebenen Überschreibungsmechanismen funktionieren, definieren wir die folgenden Regeln für das Berechnen der Ordinal-Werte, welche die Reihenfolge und damit Priorität der Konfigurationsquellen in unserem System festlegen (Tabelle 2).janssen_tamaya_tab2

Am Ende müssen wir nur noch den Server und die Clients starten. Jeder Client hört gemäß Konfiguration auf unterschiedliche Ports und zeigt mit einem einfachen Servlet unter http://localhost:port/info seinen entsprechenden Konfigurationsbaum an.

Der gesamte hier diskutierte Code ist auf der Tamaya-Seite als Beispielcode verfügbar (remote-example).

Ausblick

Wie wir in diesem Teil der Reihe gezeigt haben, bietet das Apache-Tamaya-Projekt die Möglichkeit, auch komplexere Konfigurationsszenarien im Java-SE-Umfeld einfach umzusetzen. Vor allem die relativ frei kombinierbaren Module bieten dabei viele nützliche und wiederverwendbare Funktionen, die zu maßgeschneiderten Konfigurationslösungen zusammengesetzt werden können.

Im nächsten Teil werden wir uns die Verwendungsmöglichkeiten in der Java-EE-Welt, besonders die Integration mit CDI, genauer ansehen.

Aufmacherbild: Gear wheels in the engine von Shutterstock.com / Urheberrecht: Yuri Turkov

Geschrieben von
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
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
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: