1. Reihenfolge der Property Sources
Wenn Sie jemals server.port in application.yml geändert haben, den Dienst gestartet und gesehen haben, dass er hartnäckig auf einem anderen Port startet – herzlichen Glückwunsch: Sie sind bereits darauf gestoßen, dass die Spring Boot-Konfiguration nicht in einer einzigen Datei lebt, sondern in mehreren „Schichten“. In einer Docker-Umgebung tritt dies noch schneller auf, weil ein Teil der Werte von „außerhalb“ des Containers kommt und der Entwickler beginnt, die falsche Ursache zu behandeln: editiert die Datei, baut das Image neu, schimpft auf Docker, dann auf Spring, dann auf sich selbst (das ist der normale Weg, keine Sorge).
Hier interessiert uns nicht die konkrete Syntax von -e, -D oder --..., sondern das Fundament: Spring Boot sammelt die endgültigen Werte immer aus mehreren Quellen, und diese Quellen haben eine Reihenfolge der Priorität. Sobald Sie diese Reihenfolge verstehen, hört die Konfiguration auf, Mystik zu sein und wird zur Ingenieursaufgabe: „Wo genau ist der Schlüssel gesetzt und welche Quelle hat ihn überschrieben?“
Property Sources und Environment
In Spring Boot ist das Wort property einfach ein Paar Schlüssel=Wert. Bekannte Beispiele: server.port=8080, spring.profiles.active=standalone, app.mode=postgres. Und eine Property Source ist ein konkreter Ort, von dem solche Paare kommen können: Konfigurationsdatei, Umgebungsvariablen, Startargumente, JVM-Systemeigenschaften und so weiter. Wichtig ist hier nicht die Anzahl der Quellen (es gibt viele), sondern die Mechanik: Boot legt sie in eine „Sammelbüchse“, aber nicht nach dem Prinzip „was zuerst gelesen wurde, ist wahr“, sondern nach dem Prinzip der Priorität.
Innerhalb der Anwendung spielt das Objekt Environment die Rolle des „Hauptbuchhalters der Konfiguration“. Nahezu das gesamte Spring (und auch Ihr Code) fragt letztlich bei ihm: „Welchen Wert hat der Schlüssel X?“. Und das Interessanteste: Environment antwortet bereits mit dem endgültigen Wert, ohne Erklärung „wer genau gewonnen hat“. Es ist ihm egal, ob der Wert aus einer Datei, einer Umgebungsvariablen oder einem Startargument stammt – er gibt den Gewinner zurück.
Hier ist ein minimales Codefragment, das man praktisch in jedem temporären Diagnose-Probe behalten kann. Wichtig ist hier nicht der neue Endpunkt an sich, sondern die Tatsache: Den endgültigen Wert erfragen wir bei Environment.
String mode(Environment env) {
return env.getProperty("app.mode", "standalone");
}
Hier ist standalone einfach der Default für unser Lehr-app.mode, falls der Schlüssel nirgends gesetzt ist. Ob Sie die Datei, Umgebungsvariablen oder Startargumente ändern – Sie erhalten die Antwort immer von hier: aus dem endgültigen Environment.
2. Modell des „geschichteten Kuchens“
Um nicht in Details zu ertrinken, ist es praktisch, sich die Spring Boot-Konfiguration wie einen geschichteten Kuchen vorzustellen (oder „Hering unter Pelzmantel“, wenn Ihnen heimische Standards näher sind). Unten liegt die Basiskonfiguration aus Dateien (z.B. application.yml innerhalb des JAR). Darüber können Umgebungsvariablen des Containers liegen. Noch höher – JVM-Systemeigenschaften. Und ganz oben – Anwendungsargumente (--key=value). Wenn derselbe Schlüssel in mehreren Schichten vorkommt, gewinnt die obere Schicht. Dabei gewinnt nicht die „gesamte Datei“, sondern der konkrete Schlüssel.
Diese Idee lässt sich gut mit einem Schema festhalten:
flowchart TB
%% Unten – Basiswerte und "Defaults", oben – die priorisierten Überschreibungen.
A["Code (default in @Value / getProperty)"] --> B["Config-Dateien (application.yml)"]
B --> C["Env vars (Umgebungsvariablen)"]
C --> D["JVM system properties (-Dkey=value)"]
D --> E["Application args (--key=value)"]
%% Wichtig: Der Konflikt wird auf SCHLÜSSEL-Ebene gelöst, nicht auf der Ebene der "gesamten Datei".
note["Derselbe Schlüssel kann in mehreren Schichten sein. Es gewinnt die oberste Quelle, in der er gesetzt ist."]
E --> note
Beachten Sie die unterste Schicht. Wenn Sie ${app.mode:standalone} oder getProperty("app.mode", "standalone") schreiben, setzen Sie einen Default im Code, der nur verwendet wird, wenn der Schlüssel von nirgendwo kommt. Das ist im herkömmlichen Sinne keine Property Source, aber für das Denken eines Anfängers ist es nützlich: „Selbst wenn ich nichts übergebe, muss die Anwendung nicht abstürzen – sie kann vernünftige Standardwerte haben.“
4. Vereinfachte Prioritätsleiter
Im echten Spring Boot gibt es mehr Quellen, als für den täglichen Container-Workflow nötig sind, und das ist normal. Aber für das Container-Szenario reicht es, sich die Arbeitsleiter zu merken, die 95% der Fälle im Alltag eines Java-Entwicklers abdeckt:
| Stufe (schwach → stark) | Was ist das | Beispielschlüssel | Was es normalerweise speichert |
|---|---|---|---|
| Schwächste | Default im Code | ${app.mode:standalone} | „Lass es irgendwie ohne Einstellungen funktionieren“ |
| Basis | application.yml (config data) | server.port: 8080 | Standardwerte für das Projekt |
| Stärker | Env vars | SERVER_PORT=8081 | Umgebungsüberschreibungen (Container, Entwicklungsrechner) |
| Noch stärker | JVM-Systemeigenschaften | -Dserver.port=8082 | JVM-/Prozess-Startparameter |
| Stärkste (in unserem vereinfachten Modell) | Application args | --server.port=8083 | Explizite Startargumente der Anwendung |
Diese Tabelle soll Sie nicht zwingen, alle Formen und Nuancen zu lernen. Das Ziel ist einfacher: Wenn ein Wert „nicht so ist wie in application.yml“, dann wurde er irgendwo weiter oben auf der Leiter überschrieben. Und je näher Sie an Docker sind, desto häufiger spielen die oberen Ebenen mit.
Um Precedence nicht mit Profil-Mechanik und Infrastrukturmodi zu vermischen, behalten wir im Folgenden den einfachen Lehrschlüssel app.mode. Stellen wir uns eine sehr typische Situation vor. In application.yml innerhalb des JAR haben wir einen Standard-Port und einen Standard-Modus gesetzt:
# Basiswerte des Projekts: funktionieren "out of the box", wenn beim Start nichts überschrieben wird.
server:
# Standard-Port für lokalen Start und für Container ohne zusätzliche Einstellungen.
port: 8080
app:
# Standard-Modus der Anwendung; später werden wir ihn von oben überschreiben.
mode: standalone
Das ist eine hervorragende Baseline: Der Dienst startet lokal, im Container, überall. Aber sobald Sie den Container mit einer Port-Überschreibung (oder Profil) starten, hört application.yml auf, die „letzte Instanz“ zu sein. Es bleibt die Basis, aber nicht der Gewinner.
5. Überschreibung nach Schlüssel
Eine der täuschendsten Dinge in der Konfiguration ist das Gefühl, dass, wenn „Quelle X gewonnen“ hat, sie alles gewonnen hat. Tatsächlich wählt Spring Boot den Gewinner für jeden Schlüssel einzeln aus. Das ist sehr praktisch: Sie können genau einen Wert überschreiben, ohne die gesamte Konfiguration umzuschreiben. Aber das ist auch eine Quelle der Verwirrung: Sie bearbeiten application.yml in der Erwartung, dass „alles so wird wie dort“, aber es ändert sich nur das, was nicht von oben überdeckt wird.
Erweitern wir einen beliebigen temporären Runtime-Probe so, dass er sofort ein Paar von Werten anzeigt. Wichtig ist hier nicht der neue Controller, sondern die Tatsache: Man kann zwei Schlüssel gleichzeitig sehen und sofort verstehen, was bei jedem gewonnen hat.
// Wenn Sie bereits einen Diagnose-GET-/api/runtime-Endpunkt haben, lässt es sich bequem wie folgt erweitern:
@Value("${server.port}")
private String port;
@Value("${app.mode:standalone}")
private String mode;
@GetMapping("/api/runtime")
String runtime() {
return mode + " @ " + port;
}
Eine solche Methode hängen Sie praktisch an Ihren Diagnose-GET /api/runtime-Endpunkt. Wichtig ist die Idee: Wir schauen uns zwei Schlüssel gleichzeitig an und sehen den Gewinner für jeden.
Stellen wir uns nun drei Starts vor (lokal oder im Container – das Prinzip ist gleich). Ich zeige die Befehle absichtlich über java -jar, weil wir im Container genau so starten, selbst wenn wir das Image mit Multi-Stage gebaut haben.
Start Nr. 1: Nichts überschreiben, wir nehmen die Basis aus application.yml.
# Basisstart: Werte aus application.yml (und Defaults aus Code, falls Schlüssel nicht vorhanden).
java -jar app.jar
Dann liefert der Aufruf:
GET /api/runtime
etwa folgendes Ergebnis:
standalone @ 8080
Start Nr. 2: Wir überschreiben nur app.mode über ein Startargument (ja, das ist bereits die „obere Schicht“).
# Überschreiben genau eines Schlüssels über Application Args: Er überschreibt die Datei.
java -jar app.jar --app.mode=postgres
Dann:
standalone @ 8080 (nicht)
postgres @ 8080 (ja)
Beachten Sie: Der Port hat sich nicht geändert, weil wir server.port nicht angetastet haben. Und genau das ist das normale Modell: Die Konfiguration wird nicht „als Ganzes umgeschaltet“, sondern nach Schlüsseln zusammengesetzt.
Start Nr. 3: Wir bauen einen kleinen Konflikt auf: app.mode geben wir sowohl als System Property als auch als Application Arg an.
# Bewusster Konflikt: Derselbe Schlüssel wird sowohl über -D als auch über --args gesetzt.
# In der vereinfachten Prioritätsleiter liegt --app.mode "höher" und gewinnt.
java -Dapp.mode=fromSystem -jar app.jar --app.mode=fromArg
Das Ergebnis:
fromArg @ 8080
Und das ist der Moment, den man als Spielregel akzeptieren muss: Wenn Sie denselben Schlüssel an mehreren Stellen gesetzt haben, überschreibt die höher priorisierte Quelle die weniger priorisierte. Kein „beide Werte werden berücksichtigt“, „verschmelzen“, „zufällig ausgewählt“. Es gibt einfach einen Gewinner. Und der Verlierer bleibt in der Geschichte als Ursache für Ihre zukünftigen Fragen „warum wird es ignoriert“.
6. Praxis: Konflikte und Debugging
Image-Neubau und Konfigurationskonflikte
Wenn Sie in der Container-Welt sind, ist die erste Reaktion auf seltsames Verhalten oft: „Das Image ist wahrscheinlich alt. Muss neu gebaut werden.“ Das stimmt manchmal (wenn Sie Code geändert haben), aber bei Konfigurationskonflikten ist ein Neubau so, als würde man versuchen, Fieber durch Austausch des Thermometers zu behandeln: Es scheint eine Aktion zu sein, aber sinnlos. Die Reihenfolge der Property Sources ändert sich durch einen Neubau nicht, und wenn ein Wert von oben überschrieben wird, bleibt er überschrieben.
Das häufigste Szenario sieht so aus. Im JAR liegt application.yml, Sie ändern dort server.port: 8080, bauen das Image neu, starten den Container – und der Port ist immer noch „nicht der richtige“. Dann beginnt das Rätselraten, obwohl man einfach eine Frage stellen müsste: „Übergebe ich server.port nicht noch woanders?“. In der Docker-Realität ist das einfach: ENV im Dockerfile, -e in docker run, Umgebungsvariablen aus Ihrer IDE oder dem Terminal, Startargumente in ENTRYPOINT/CMD. All das kann die Datei überschreiben, und ein Neubau hat damit nichts zu tun.
Daher lautet eine nützliche Disziplin: Wenn sich nur die Startkonfiguration geändert hat – kein Image-Neubau, suchen Sie die gewinnende Quelle; wenn sich Code/Artifakt geändert hat – bauen Sie das Image neu. Das ist genau die Grenze, die wir in Modul 3 verankern wollen.
Debugging: „Suche, wo der Schlüssel gesetzt ist“
Wenn die Konfiguration „nicht den Erwartungen entspricht“, möchte man sofort zur auffälligsten Schicht springen – normalerweise application.yml – und dort reparieren. Aber richtiger ist es, anders anzufangen: Wählen Sie einen konkreten Schlüssel (z.B. server.port) und führen Sie eine kleine Untersuchung durch: Wo könnte er überall gesetzt sein? In dieser Phase ist es wichtig, das Problem nicht auf „die gesamte Konfiguration ist kaputt“ auszuweiten, denn sie ist fast nie komplett kaputt – normalerweise streiten sich 1–2 Schlüssel.
Der praktische Weg sieht so aus. Zuerst vergewissern Sie sich, dass Sie den endgültigen Wert sehen. Dafür haben wir /api/runtime gemacht: eine schnelle Antwort, die zeigt, „was wirklich herauskommt“. Dann prüfen Sie die Basisschicht: Was steht in application.yml. Danach erinnern Sie sich an die Prioritätsleiter und fragen sich ehrlich: „Überschreibe ich das nicht über Env Vars, über -D oder über --...?“. Wenn die Anwendung in einem Container läuft, fügen Sie eine weitere wichtige Schicht hinzu: „Was genau habe ich dem Container beim Start übergeben?“
Wenn es um Container geht, hilft sehr, was wir in früheren Kurstagen über Docker CLI gelernt haben. Der Befehl docker inspect kann zeigen, mit welchen Umgebungsvariablen der Container gestartet wurde. Und das ist kein „seltener DevOps-Befehl“, sondern eine banale Taschenlampe im Dunkeln. Wenn Sie darin Env mit so etwas wie SERVER_PORT=8081 sehen, erledigt sich die Frage „warum nicht 8080 aus der Datei“ von selbst.
Zur Verfestigung des Gedankens – noch ein kleines Schema, auf das man sich mental stützen kann, wenn die Konfigurationspanik beginnt:
flowchart TD
A["Symptom: Wert nicht korrekt"] --> B["Schlüssel fixieren (z.B. server.port)"]
B --> C["Ergebnis prüfen (Environment / /api/runtime / Logs)"]
C --> D["Schlüssel in application.yml suchen"]
D --> E["Prüfen, ob Schlüssel von oben überschrieben: env vars → -D → --args"]
E --> F["Wenn Container: docker inspect / Startbefehl prüfen"]
F --> G["Duplikate entfernen / eine Hauptquelle festlegen"]
Die Kernidee am Ende: Ihr Ziel ist nicht, „die richtige Quelle zu erraten“, sondern dafür zu sorgen, dass der Schlüssel einen eindeutigen, verständlichen Besitzer hat. Sonst wird die Konfiguration zum Sport „wer am lautesten schreit“ – und lauter schreit normalerweise der, der zuletzt und von oben gesetzt wurde.
7. Typische Fehler
Fehler Nr. 1: application.yml als absolute Wahrheit betrachten.
Die Datei application.yml ist die Basisschicht, gut für Projekt-Defaults, aber sie kann nicht die „absolute Wahrheit“ sein, weil Spring Boot speziell dafür entwickelt wurde, dass man sie beim Start überschreiben kann. In Docker-Szenarien wird das besonders deutlich: Die Container-Umgebung überschreibt oft die Werte aus dem JAR, und das ist erwartetes Verhalten, kein Fehler.
Fehler Nr. 2: Ein Konfigurationsproblem durch Image-Neubau beheben.
Wenn Sie nur die Startwerte geändert haben, wird ein Image-Neubau in der Regel nichts ändern, außer Ihrer Stimmung und der Wartezeit. Ein Konfigurationskonflikt wird nicht durch docker build gelöst, sondern durch die Suche nach der gewinnenden Quelle und das Entfernen von Dubletten. Ein Neubau ist sinnvoll, wenn sich Code oder Artifakt ändert, nicht wenn Sie mit SERVER_PORT streiten.
Fehler Nr. 3: Denselben Schlüssel gleich an drei Stellen „für alle Fälle“ setzen.
Manchmal erscheint es logisch: „Es soll sowohl in application.yml, als auch in Env Vars, als auch in den Startargumenten stehen – für den Fall, dass“. In der Praxis führt das dazu, dass nach einer Woche niemand (Sie eingeschlossen) mehr weiß, wer das Ergebnis wirklich beeinflusst. Die richtige Angewohnheit: den Default in application.yml halten und punktuell und bewusst mit einer gewählten Methode überschreiben.
Fehler Nr. 4: Die Konfiguration als „Modus-Umschalter für das Ganze“ betrachten.
Anfänger erwarten oft, dass „wenn ich einen bestimmten Modus einschalte“, die gesamte Anwendung nach einer einzigen Datei lebt und die anderen Schichten verschwinden. Aber Spring Boot überschreibt Werte nach Schlüsseln, daher können Sie einen „Hybrid“ erhalten: Ein Schlüssel kommt aus der Datei, ein anderer aus der Env Var, ein dritter aus den Startargumenten. Das ist keine Anomalie, sondern die normale Mechanik, die man im Kopf behalten muss.
Fehler Nr. 5: Die Konfiguration „nach Ort“ statt „nach Schlüssel“ analysieren.
Ein sehr häufiges Bild: Der Entwickler schaut lange nur in das Dockerfile oder nur in application.yml und kann nicht verstehen, warum sich der Wert nicht ändert. Die Konfiguration in Boot wird nach Schlüssel entschieden, also muss die Untersuchung auch nach Schlüssel gehen: „Wo ist server.port gesetzt?“, „Wo ist spring.profiles.active gesetzt?“. Sobald Sie auf diese Optik umschalten, nimmt das Chaos rapide ab.
GO TO FULL VERSION