Konfiguration an JavaBeans binden – mit Spring Boot

Keine Kommentare

Mit Spring ist es einfach, Konfigurationswerte auszulesen und Variablen zuzuweisen. Die @Value-Annotation gib es schon eine Weile im Spring Framework. Durch die Einführung von @ConfigurationProperties in Spring Boot ist eine weitere Möglichkeit hinzugekommen, Konfigurationswerte an JavaBeans zu binden. Abhängig davon, wie die Konfiguration an eine Anwendung übergeben wird, gibt es kleine Unterschiede, wie diese an JavaBeans gebunden werden. Da ich vor kurzem selbst darüber sehr verwirrt war, werde ich in diesem Blogpost die Unterschiede in der Bindung erläutern.

Lesen von Konfigurationswerten aus unterschiedlichen Quellen

Es gibt viele verschiedene Wege, um Konfigurationswerte für eine Spring-Boot-Anwendung zu setzen, und das Framework definiert Regeln, wie diese sich gegenseitig überschreiben. Auf diese Art und Weise können wir zum Beispiel Profile nutzen, um verschiedene Umgebungen zu definieren (z.B. lokal, Dev, QA und Prod). Eine andere Möglichkeit ist die Nutzung der application.properties-Datei für die lokale Entwicklung und das Setzen von Umgebungsvariablen in den verschiedenen Umgebungen. Konfigurationswerte mittels Umgebungsvariablen zu übergeben ist laut 12 Faktor Methode der bevorzugte Weg für skalierbare Anwendungen. Deshalb werde ich im weiteren Verlauf zeigen, wie Umgebungsvariablen die Konfigurationswerte aus der application.properties-Datei überschreiben.
Der Rest des Blogposts nutzt ein kleines Beispielprojekt, um zu analysieren, wie Konfigurationswerte gebunden werden. Darum gehe ich davon aus, dass Spring Boot generell sowie @Value und @ConfigurationProperties bekannt sind.

Das spring-boot-configuration-example Projekt

Abhängig davon, ob Konfigurationswerte als Umgebungsvariablen gesetzt oder in Properties-Dateien definiert werden, gibt es Unterschiede bei der Bindung an JavaBeans. Des Weiteren ist das Ergebnis der Bindung auch davon abhängig, ob die @Value-Annotation verwendet wird oder @ConfigurationProperties. Um dies zu veranschaulichen, habe ich ein kleines Beispielprojekt angelegt: codecentric/spring-boot-configuration-example. Mit dem Projekt können wir verschiedene Arten der Bindung ausprobieren und uns die Ergebnisse untersuchen.

Zum Auslesen der Konfiguration enthält das Projekt zwei Konfigurationsklassen: AnnotationConfiguration, die die @Value-Annotation nutzt und TypeSafeConfiguration, die die @ConfigurationProperties-Annotation nutzt. In AnnotationConfiguration werden die folgenden Spring Expression Language (SpEL)-Ausdrücke genutzt, um Konfigurationswerte zu setzen:

@Component
public class AnnotationConfiguration {
    @Value("${example.property-name:not set}")
    public String propertyDashName;
 
    @Value("${example.property.name:not set}")
    public String propertyPointName;
 
    @Value("${example.propertyName:not set}")
    public String propertyName;
 
    @Value("${example.propertyname:not set}")
    public String propertyname;
 
    @Value("${example.property_name:not set}")
    public String property_name;
}

Die TypeSafeConfiguration auf der anderen Seite nutzt das Konfigurationspräfix “example” und hat die folgenden Felder:

@Component
@ConfigurationProperties(prefix = "example")
public class TypeSafeConfiguration {
    private String propertyName;
 
    private String propertyname;
 
    private String property_name;
}

Dazu ist noch zu sagen, dass wir example.property-name und exmample.property.name nicht in der TypeSafeConfiguration abbilden können, da es sich nicht um valide Feldnamen in Java handelt. Um uns anzuschauen, wie Spring-Konfigurationswerte an die beiden Klassen binden, können wir das run.sh-Skript ausführen. Es schreibt einige Log-Ausgaben, die angeben, was gerade passiert und wie der Zustand der Anwendung durch die Konfiguration beeinflusst wird. Die folgenden Schritte werden nacheinander durchgeführt:

  1. Anwendung ohne Konfiguration ausführen.
  2. Anwendung mit Konfiguration aus Umgebungsvariablen ausführen.
  3. Anwendung mit einer application.properties Datei ausführen.
  4. Anwendung mit Umgebungsvariablen und application.properties ausführen.

Bei den Schritten 2 und 4 werden die folgenden Umgebungsvariablen gesetzt:

export EXAMPLE_PROPERTY_NAME=EXAMPLE_PROPERTY_NAME
export EXAMPLE_PROPERTYNAME=EXAMPLE_PROPERTYNAME

Man beachte den fehlenden Unterstrich in der zweiten Zeile. Es wird interessant zu sehen sein, wie Spring dies auf die Konfigurationsklassen mappt. Beispielsweise kann ich nicht sagen, welcher SpEL-Ausdruck aus der AnnotationsConfiguration den Wert von EXAMPLE_PROPERTYNAME bekommt. Bei den Durchläufen 3 und 4 wird der folgende Inhalt in die Datei src/main/resources/application.properties geschrieben:

example.property.name=example.property.name
example.property-name=example.property-name
example.property_name=example.property_name
example.propertyname=example.propertyname
example.propertyName=example.propertyName

Das Mapping zwischen Properties und SpEL-Ausdrücken ist hierbei klar, da es eine 1-zu-1-Beziehung gibt. Allerdings ist es überhaupt nicht offensichtlich, wie die Werte aus der application.properties-Datei an die TypeSafeConfiguration gebunden werden. Also lasst uns das run.sh-Skript ausführen und das Ergebnis anschauen. Ihr könnt es selbst mit den folgenden Befehlen ausprobieren:

git clone https://github.com/codecentric/spring-boot-configuration-example
cd spring-boot-configuration-example
./run.sh

Im Rest dieses Blogposts werde ich Schritt für Schritt durch die Ausgabe des run.sh-Skripts gehen. Wenn ihr euch die Details anschauen wollt, könnt ihr euch dieses Gist ansehen, das die Ausgabe des Skripts enthält. Erwartungsgemäß sind alle Felder in beiden Konfigurationen leer, wenn die Anwendung ohne Konfiguration gestartet wird.

Konfigurationswerte aus den Umgebungsvariablen auslesen

Der Anwendungslauf nur mit Umgebungsvariablen ist schon interessanter. Das Log zeigt die folgenden Bindings für die TypeSafeConfiguration:

FeldnameKonfigurationswert
propertyNameEXAMPLE_PROPERTY_NAME
propertynameEXAMPLE_PROPERTYNAME
property_namenull

… und für die SpEL Ausdrücke in AnnotationConfiguration:

SpEL AusdruckKonfigurationswert
example.property-namenot set
example.property.nameEXAMPLE_PROPERTY_NAME
example.propertyNameEXAMPLE_PROPERTYNAME
example.propertynameEXAMPLE_PROPERTYNAME
example.property_nameEXAMPLE_PROPERTY_NAME

Wenn wir uns die erste Tabelle anschauen, können wir sehen, dass TypeSafeConfiguration.propertyName den Wert von EXAMPLE_PROPERTY_NAME zugewiesen bekommt, während TypeSafeConfiguration.propertyname den Wert von EXAMPLE_PROPERTYNAME bekommt. TypeSafeConfiguration.property_name kann nicht mit Umgebungsvariablen gesetzt werden. Das Ergebnis für AnnotationConfiguration können wir aus der zweiten Tabelle ablesen: example.property.name und example.property_name bekommen beide den Wert von EXAMPLE_PROPERTY_NAME. example.propertyName und example.propertyname erhalten den Wert von EXAMPLE_PROPERTYNAME. Den SpEL Ausdruck example.property-name können wir nicht mittels Umgebungsvariablen setzen.

Was ist daran interessant? Wenn wir uns nur die TypeSafeConfiguration anschauen, dann würden wir doch eigentlich erwarten, dass der Wert von EXAMPLE_PROPERTY_NAME genutzt wird um TypeSafeConfiguration.property_name zu setzen. Stattdessen wird aber der Wert von EXAMPLE_PROPERTYNAME verwendet. Noch verwirrender wird das Ganze, wenn man es mit dem Ergebnis aus der zweiten Tabelle vergleicht. Hier wird dem SpEL-Ausdruck example.property_name der Wert von EXAMPLE_PROPERTY_NAME zugewiesen!

Eine weitere Inkonsistenz ist die Behandlung von TypeSafeConfiguration.propertyName und TypeSafeConfiguration.propertyname verglichen mit der Behandlung der SpEL-Ausdrücke example.propertyName und example.propertyname. Während auf der einen Seite TypeSafeConfiguration.propertyName den Wert von EXAMPLE_PROPERTY_NAME bekommt und TypeSafeConfiguration.propertyname den Wert von EXAMPLE_PROPERTYNAME, bekommen auf der anderen Seite beide SpEL-Ausdrücke den Wert von EXAMPLE_PROPERTYNAME.

Die letzte Beobachtung, die wir machen können, ist, dass es nicht möglich ist, SpEL-Ausdrücke mit Bindestrichen über Umgebungsvariablen zu setzen. Das ist besonders unglücklich, da dies der empfohlene Weg für die Definition von Schlüsseln in Property-Dateien laut Spring-Boot-Dokumentation ist (vgl. Tabelle 24.1 in der Spring Boot Dokumentation). Man stelle sich ein Team vor, das die gesamte Konfiguration auf Basis von Schlüsseln mit Bindestrichen in einer application.properties-Datei aufbaut, nur um dann festzustellen, dass diese nicht über Umgebungsvariablen gesetzt werden können.

Konfigurationswerte aus einer Properties-Datei auslesen

Der nächste Teil des Logs zeigt, wie die Konfiguration aus einer application.properties-Datei ausgelesen wird. Hier ist zusammengefasst das Ergebnis für die TypeSafeConfiguration:

FeldnameKonfigurationswert
propertyNameexample.propertyName
propertynameexample.propertyname
property_nameexample.property-name

… und für AnnotationConfiguration:

SpEL AusdruckKonfigurationswert
example.property-nameexample.property-name
example.property.nameexample.property.name
example.propertyNameexample.propertyName
example.propertynameexample.propertyname
example.property_nameexample.property_name

Die Werte für AnnotationConfiguration sind genau so wie erwartet – darauf brauchen wir nicht weiter eingehen. Wirklich verwunderlich ist, dass TypeSafeConfiguration.property_name der Wert von example.property-name zugewiesen wird und nicht example.property_name. Ich habe keine Ahnung, warum das so ist. Des Weiteren können wir sehen, dass es möglich ist, alle Werte zu setzen (das war nur mit Umgebungsvariablen nicht möglich).

Mischen von Konfiguration aus Umgebungsvariablen und Properties Dateien

Der letzte Aspekt, den wir uns anschauen, ist, wie sich Konfigurationswerte gegenseitig überschreiben, wenn sowohl application.properties als auch Umgebungsvariablen vorhanden sind. Den Anfang macht wieder die TypeSafeConfiguration:

FeldnameKonfigurationswert
propertyNameEXAMPLE_PROPERTY_NAME
propertynameEXAMPLE_PROPERTYNAME
property_nameexample.property-name

… und hier die Werte für die AnnotationConfiguration:

SpEL AusdruckKonfigurationswert
example.property-nameexample.property-name
example.property.nameEXAMPLE_PROPERTY_NAME
example.propertyNameEXAMPLE_PROPERTYNAME
example.propertynameEXAMPLE_PROPERTYNAME
example.property_nameEXAMPLE_PROPERTY_NAME

Da Umgebungsvariablen Vorrang vor Werten in den application.properties haben, werden alle Werte, die durch Umgebungsvariablen gesetzt werden können, auch so gesetzt. Nur TypeSafeConfiguration.property_name und dem SpEL-Ausdruck example.property-name werden die entsprechenden Werte aus den application.properties zugewiesen.

Fazit

In Spring Boot gibt es zwei Möglichkeiten, Konfigurationswerte an JavaBeans zu binden: mit Hilfe des typsicheren Bindings durch @ConfigurationProperties und durch die Verwendung von SpEL-Ausdrücken zusammen mit @Value. So praktisch das sein mag – es kann zu verwirrenden Ergebnissen kommen. Das ist abhängig davon, ob die Konfigurationswerte aus den Umgebungsvariablen kommen oder aus einer Properties-Datei. Was man aus diesem Blogpost mitnehmen sollte, sind die folgenden Empfehlungen:

  • Konsistenz wahren: Wir sollten Camel Case, Snake Case usw. nicht mischen, wenn wir Felder und Property-Schlüssel definieren.
  • Keine Bindestriche in Property-Schlüsseln verwenden.
  • Keine Unterstriche in Feldnamen verwenden.

Diese Tipps können uns eine Menge Kopfschmerzen ersparen, wenn wir mit Konfigurationswerten in Spring Boot arbeiten.

Benedikt Ritter arbeitet seit September 2013 als Software Craftsman bei der codecentric AG. Sein Können bringt er nicht nur in der Berufswelt zum Einsatz: Benedikt ist Member der Apache Software Foundation und Committer beim Apache Commons Projekt.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.