1. Hartcodierte Werte und technische Schulden
Wenn Sie immer noch denken: „Na und, ein String "EMAIL" in einer Methode – das ist doch nicht das Ende der Welt“, ist das normal: In kleinen Projekten scheint es richtig zu sein. Aber genau solche Kleinigkeiten verwandeln eine Anwendung später in eine Schnitzeljagd „Finde, wo das geändert werden muss“, bei der der Endgegner Strg+Umschalt+F im Repository ist. Lassen Sie uns durchgehen, warum hartcodierte Werte bereits auf der Spring-Core-Ebene eine Sackgasse sind, selbst ohne Spring Boot, ohne Web und ohne Datenbank.
Beginnen wir mit einem einfachen und leicht schmerzhaften Beispiel. Stellen Sie sich vor, wir schreiben Bestellbenachrichtigungen und entscheiden „vorübergehend“: Standardmäßig sei der Kanal EMAIL und die Anzahl der Sendeversuche 3. Das sieht harmlos aus – bis jemand sagt: „Im Demo-Modus nehmen wir doch SMS“ oder „Auf CI soll gar nichts gesendet werden“.
public class NotificationDispatchService {
public void dispatch(String message) {
// Schlecht: Einstellungen sind in der Geschäftsmethode versteckt und mit Logik vermischt.
// Von außerhalb der Klasse ist nicht sichtbar, dass diese Werte überhaupt existieren und wichtig sind.
String defaultChannel = "EMAIL";
int retryLimit = 3;
// Als Beispiel drucken wir nur eine „als ob“-Sendung.
System.out.println(defaultChannel + " -> " + message + ", retries=" + retryLimit);
// EMAIL -> Order created, retries=3
}
}
In dieser Version gibt es zwei Probleme, und beide sind unangenehm. Erstens ist die Einstellung innerhalb der Methode versteckt und mit der Logik vermischt – man sieht nicht einmal, dass dies eine „wichtige Stelle“ ist, bis man das Verhalten ändern möchte. Zweitens bedeutet eine Änderung des Wertes, den Code zu ändern, neu zu bauen, neu zu starten – also das Programm zu ändern, nur um einen Parameter zu variieren, der eigentlich ein Startparameter ist.
Selbst wenn Sie einen Schritt vorwärts machen und den Wert in ein Feld oder einen Konstruktor auslagern, ist das immer noch keine externe Konfiguration. Aber es ist bereits ein wichtiger Schritt: Wir hören auf, Einstellungen mitten in der Logik zu verstecken, und beginnen, sie als Daten zu betrachten, die „von außerhalb“ der Klasse kommen. In Spring passt dies sehr gut zur Idee des Environment: Später werden dort die Eigenschaften aus verschiedenen Quellen zusammenfließen; vorerst lernen wir nur, sie nicht in Methoden hartzucodieren.
2. Abhängigkeiten und Einstellungen
Normalerweise sagt ein Anfänger: „Mein Dienst hat Abhängigkeiten“ und wirft in denselben Korb auch noch Einstellungen: Zeichenketten, Zahlen, Modus-Flags. Das ist ein bisschen, als würde man Wohnungsschlüssel und heißen Kaffee in eine Tasche stecken: Beides wird „gebraucht“, aber die Folgen der Vermischung sind plötzlich dramatisch. Es ist wichtig, diese Konzepte zu trennen, denn Spring arbeitet hervorragend mit Objekt-Abhängigkeiten, aber Einstellungen erfordern eine eigene Disziplin.
Versuchen wir es in menschlicher Sprache zu erklären.
Abhängigkeit – das ist ein anderes Objekt, mit dem Ihre Klasse zusammenarbeitet. Zum Beispiel AuditWriter oder OrderStore. Sie können die Implementierung austauschen, aber der Sinn bleibt: Es ist ein „Arbeitspartner“.
Einstellung – das ist ein Wert, der das Verhalten ändert, ohne die Struktur der Anwendung zu verändern. Zum Beispiel „wohin soll der Bericht geschrieben werden“ oder „welcher Benachrichtigungskanal als Standard gilt“. Das ähnelt eher nicht einem Kollegen, sondern den „Büroregeln“: Heute arbeiten wir remote, morgen im Büro; die Kollegen sind dieselben, der Modus ist ein anderer.
Im Beispiel mit ContextFlow wird der Unterschied recht anschaulich:
| Was der Dienst braucht | Beispiel | Warum genau das |
|---|---|---|
| Abhängigkeit (Objekt) | OrderStore, , |
Das ist Verhalten in Form von Code. Wir tauschen die Implementierung aus, machen aber weiterhin „Audit“, „speichern Bestellungen“, „senden Benachrichtigungen“. |
| Einstellung (Wert) | contextflow.app-name, contextflow.report.output-dir | Das sind Umgebungs- und Startparameter. Sie sollen ohne Änderung des Java-Codes geändert werden können. |
Und hier ist eine wichtige praktische Schlussfolgerung. Abhängigkeiten injizieren wir als Objekte (das können wir bereits). Einstellungen injizieren wir ebenfalls – aber als Werte, und zwar so, dass sie im Vertrag der Klasse (im Konstruktor) sichtbar sind und nicht mitten in der Methode versteckt werden.
Allgemeiner betrachtet fasst Spring solche Werte später im Environment zusammen – einem einheitlichen Ort, an dem die Anwendung nach Einstellungen suchen kann, ohne vorher zu wissen, ob sie aus einer Datei, einer Umgebungsvariable oder Startargumenten kommen. Aber bevor wir von dort lesen, müssen wir lernen, grundsätzlich ein „Objekt“ von einem „Parameter“ zu unterscheiden.
3. Konfiguration in @Configuration
Nach dem vorherigen Abschnitt könnte das falsche Gefühl entstehen: „Okay, wir lassen die Zeichenkette nicht in der Methode. Wir lagern sie in die Konfiguration aus – und das Problem ist gelöst.“ Das ist tatsächlich eine Verbesserung, aber nicht das Ende der Geschichte. Es ist, als würde man aufhören, Geld unter der Matratze zu verstecken, und anfangen, es in einer Kiste mit der Aufschrift „Geld“ aufzubewahren – ordentlicher, aber eine Bank ist es noch nicht.
Machen wir den Zwischenschritt richtig: Einstellungen sollten nicht innerhalb von Methoden leben, deshalb übergeben wir sie über den Konstruktor, und den Ort, an dem wir sie festlegen, verlegen wir in den Composition Root – also in die Spring-Konfigurationsklasse.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CoreConfig {
@Bean
public NotificationDispatchService notificationDispatchService() {
// Composition root: Hier ist sichtbar, wie die Anwendung „zusammengebaut“ ist und welche Werte gewählt wurden.
// Aber das ist immer noch KEINE externe Konfiguration: Um EMAIL/3 zu ändern, muss Java-Code geändert werden.
return new NotificationDispatchService("EMAIL", 3);
}
}
Aus architektonischer Sicht ist das deutlich besser. Jetzt haben wir einen einzigen Punkt, an dem sichtbar ist, „wie die Anwendung zusammengebaut ist“ und welche Werte gewählt wurden. Das hilft bereits beim Lesen des Projekts: Man muss nicht durch die Dienste springen und suchen, wo der Autor einmal "EMAIL" „für fünf Minuten“ platziert hat.
Aber das Problem bleibt: Jede Änderung erfordert trotzdem eine Änderung des Java-Codes. Das bedeutet, Sie bauen immer noch einen neuen Build, nur um einen Parameter zu ändern, der seinem Wesen nach ein Startparameter ist. In der Praxis wird das schnell schmerzhaft: Verschiedene Umgebungen wollen unterschiedliche Werte. Und selbst wenn Sie derzeit nur eine Umgebung haben (Ihr Laptop), wird eines Tages eine zweite (Laptop eines Kollegen), eine dritte (CI), eine vierte (Demo-Maschine) hinzukommen. Dann beginnt der Spaß.
4. Gefährliche Literale: Pfade und Umgebung
Es gibt eine besondere Klasse von hartcodierten Werten, die die Portabilität fast garantiert zerstören: alles, was mit dem Dateisystem, Pfaden, Verzeichnisnamen, absoluten Adressen und „bei mir funktioniert es so“ zu tun hat.
Wenn Sie jemals so etwas wie "C:\\Users\\Ivan\\Desktop\\report.txt" gesehen haben – herzlichen Glückwunsch, Sie kennen dieses Genre bereits. In ContextFlow gibt es mindestens zwei Stellen, an denen ein Pfad leicht „mal schnell“ hartcodiert werden könnte: die Berichtsausgabe und das Datei-Audit.
public class ReportOutputManager {
private final String outputDir;
public ReportOutputManager() {
// Gefährlich: Ein absoluter Pfad bindet den Code an eine bestimmte Maschine und einen bestimmten Benutzer.
// In einer anderen Umgebung existiert dieses Verzeichnis möglicherweise nicht – und alles „funktioniert plötzlich nicht“.
this.outputDir = "C:\\Users\\Ivan\\Desktop\\reports"; // so funktionierte es bei Ivan
}
}
Das Problem ist nicht, dass Ivan ein schlechter Mensch ist (vielleicht ist Ivan ein wunderbarer Mensch). Das Problem ist, dass Ivans Pfad nicht Teil der Geschäftslogik der Anwendung ist. Es ist Teil der Umgebung, in der die Anwendung läuft. Und die Umgebung ändert sich häufiger, als wir möchten.
In unserem Kurs gibt es eine zusätzliche Disziplin: Alle Dateien sollten in build/ landen, damit das Projekt reproduzierbar ist und den Rechner des Studenten nicht überfüllt. Das bedeutet, dass wir selbst „Standard“-Werte sorgfältig entwerfen müssen. Zum Beispiel build/reports oder build/contextflow/reports. Aber auch das sollte man besser als Einstellung halten, denn morgen möchte man vielleicht ein anderes Verzeichnis.
5. Satz von Einstellungen für ContextFlow
Jetzt machen wir einen sehr praktischen Schritt: Wir legen fest, welche Werte in unserem Projekt Einstellungen sind, und vereinbaren ihre Schlüssel. Denn Konfigurationsschlüssel sind ebenfalls ein Vertrag, fast wie die Namen öffentlicher Methoden. Wenn Sie einen schlechten Schlüsselnamen wählen, werden Sie nicht sofort leiden, aber später … und dann pünktlich.
Im Rahmen von ContextFlow reicht uns ein Satz, der das Verhalten tatsächlich beeinflusst und der sich natürlich zwischen verschiedenen Läufen ändert:
| Schlüssel | Beispielwert | Wo wirkt es |
|---|---|---|
| contextflow.app-name | ContextFlow | Technische Mitteilungen, Berichtsköpfe, Skriptausgabe |
| contextflow.notifications.default-channel | console | Welcher Kanal als „Standard“ gilt, wenn das Skript nichts angibt |
| contextflow.audit.mode | console / file | Welche Auditstrategie im aktuellen Lauf verwendet wird |
| contextflow.report.output-dir | build/reports | Wohin die Berichte abgelegt werden |
| contextflow.locale.default | de / en | Standard-Locale für Nachrichten und Vorlagen (vorerst nur eine Zeichenkette) |
Für operative Werte wie default-channel und audit.mode bietet es sich an, gleich Kleinbuchstaben zu verwenden: Sie sind so leichter lesbar und nicht mit Java-Konstanten zu verwechseln.
Beachten Sie den Namensstil. Das Präfix contextflow. ist unser „Koordinatensystem“. Es löst gleich zwei Probleme: Erstens kollidieren die Schlüssel nicht mit fremden; zweitens sehen Sie beim Öffnen einer Konfigurationsdatei sofort, was zu unserer Anwendung gehört.
Und noch eine Nuance. Wir könnten die Schlüssel „kürzer“ nennen – wie appName oder dir. Aber das ist fast immer eine schlechte Idee. Konfigurationsdateien werden von Menschen gelesen (manchmal müden), und die Schlüssel sollten helfen, nicht Rätsel aufgeben.
Disziplin der Schlüssel contextflow.*
Bevor wir lernen, Eigenschaften tatsächlich aus externen Quellen zu lesen, müssen wir uns auf eine Schlüsseldisziplin einigen. Das klingt langweilig, spart aber in der Realität viele Nerven.
Erstens: Der Schlüssel muss stabil sein. Wenn Sie heute den Schlüssel contextflow.report.output-dir nennen und morgen in contextflow.report.dir umbenennen, haben Sie alle Umgebungen zerstört, die den alten Schlüssel verwendeten. Schlüssel sind ein Startvertrag.
Zweitens: Der Schlüssel muss „selbsterklärend“ sein. Das bedeutet nicht, dass er so lang wie ein Roman sein muss, aber er sollte ohne einen Ausflug in den Code verständlich sein. audit.mode ist bereits besser als mode. contextflow.audit.mode ist noch besser, weil ersichtlich ist, wozu es gehört.
Drittens: Sie sollten die Standardwerte im Voraus durchdenken. Einige Einstellungen können „standardmäßig“ vorhanden sein (z. B. ContextFlow als Anwendungsname). Andere sollten besser obligatorisch sein, damit die Anwendung beim Start abstürzt, wenn der Wert nicht gesetzt ist. Wir nennen dies Fail-Fast-Verhalten, und es schützt normalerweise vor „stillen“ Fehlstarts.
Im Kurs streben wir eine einfache Logik an: Für den Lehrbetrieb gibt es vernünftige Standardwerte; wenn eine Einstellung jedoch für das Verhalten kritisch ist (z. B. der Dateipfad), sollten wir nicht stillschweigend in einen unklaren Fallback gehen. Besser, mit einer verständlichen Fehlermeldung beim Start zu fallen, als still Berichte „irgendwo hin“ zu schreiben.
6. Was nicht in die Konfiguration ausgelagert werden sollte
An dieser Stelle entsteht oft das andere Extrem: „Wenn Hartcodierung schlecht ist, dann lagern wir einfach alles aus.“ Und hier ist es wichtig, den gesunden Menschenverstand nicht zu verlieren, denn Konfiguration ist kein Friedhof aller Konstanten.
Das klassische Kriterium sieht so aus: Wenn ein Wert Teil einer Geschäftsregel ist, die den Sinn des Systems definiert, bleibt er normalerweise im Code. Wenn ein Wert operativ ist, ein Umgebungsparameter, der sich von Lauf zu Lauf ändert, wird er nach außen verlagert.
Zum Beispiel sind OrderStatus.NEW und OrderStatus.CANCELLED keine Einstellungen. Wenn Sie „NEW“ in eine Properties-Datei auslagern, wird die Anwendung nicht „flexibler“, sondern seltsamer. Gleiches gilt für die Domänenstruktur selbst: Felder von Order, die Liste der Items, Invarianten – das ist keine Konfiguration.
Aber der Ausgabepfad eines Berichts, der Audit-Modus „Konsole oder Datei“, der Anwendungsname, der Standard-Benachrichtigungskanal – das sind typische Umgebungseinstellungen. Sie ändern nicht die Fachlichkeit, sie ändern, „wie genau sich die Anwendung in diesem Lauf verhält“.
Als Bild: Code ist das Rezept eines Gerichts, Konfiguration ist „wie viel Salz heute verwendet wird“. Wenn Sie die Zutatenliste und die Zubereitungsreihenfolge selbst in die Konfiguration auslagern, erhalten Sie kein Rezept, sondern einen Chaos-Generator.
7. Einstellungen über den Konstruktor
Jetzt machen wir das, was man tun kann und sollte, noch bevor wir lernen, Einstellungen aus Dateien zu lesen: Wir schreiben die Klassen so um, dass die Einstellungen Parameter des Konstruktors sind. Das ist eine wichtige Ingenieursgewohnheit. Sie macht den Code ehrlicher: Wenn eine Klasse einen Wert benötigt, ist das sofort sichtbar, ohne archäologische Ausgrabungen in der Methode dispatch().
Hier derselbe NotificationDispatchService, aber bereits in normaler Form:
public class NotificationDispatchService {
private final String defaultChannel;
private final int retryLimit;
public NotificationDispatchService(String defaultChannel, int retryLimit) {
// Einstellungen werden Teil des Klassenvertrags: Sie sind bei der Objekterstellung sichtbar.
// Das macht die Klasse ehrlicher und einfacher zu testen/wiederzuverwenden.
this.defaultChannel = defaultChannel;
this.retryLimit = retryLimit;
}
}
Ja, hier gibt es noch keine Spring-Magie. Und das ist gut so. Wir versuchen nicht, „alles sofort mit Annotationen“ zu machen. Wir machen die Klasse einfach normal: Sie verbirgt ihre Konfiguration nicht.
An diesem Punkt erhalten Sie bereits einen kleinen Bonus für die Testbarkeit. Sie können den Dienst mit verschiedenen Werten im Test erstellen, ohne den Code der Klasse umschreiben zu müssen und ohne in private Felder per Reflexion einzusteigen. Selbst wenn Sie noch keine Tests schreiben – Sie machen bereits Code, den zu testen nicht widerstrebt.
Anschaulicher Effekt: unterschiedliches Verhalten ohne Codeänderung
Es ist nützlich, einmal den „Wow-Effekt“ an einem einfachen Beispiel zu sehen. Derselbe Klasse verhält sich unterschiedlich, weil wir ihr unterschiedliche Parameter übergeben haben. Die Klasse weiß nicht, woher sie kommen – und das ist ideales Design für Konfiguration.
public class Demo {
public static void main(String[] args) {
// Dieselbe Logik, aber unterschiedliche „Startparameter“.
NotificationDispatchService prod = new NotificationDispatchService("EMAIL", 3);
NotificationDispatchService demo = new NotificationDispatchService("CONSOLE", 0);
// Zur Demonstration zeigen wir nur, dass die Konfigurationen unterschiedlich sind.
System.out.println("Prod konfiguriert"); // Prod konfiguriert
System.out.println("Demo konfiguriert"); // Demo konfiguriert
}
}
Natürlich „sendet“ dieses Beispiel noch keine Benachrichtigungen – wir wollen den Code nicht aufblähen. Der Punkt ist ein anderer: Unterschiedliche Konfigurationen sind bereits von der Klassenlogik getrennt. Und dann bringen wir Spring einfach bei, diese Werte nicht aus Java-Code, sondern von außen zu liefern. Aber der grundlegende Schritt ist bereits getan: Die Klasse ist konfigurierbar geworden.
Und hier ein wichtiger Gedanke, der an allen folgenden Tagen nützlich sein wird: Spring macht Ihren Code nicht automatisch konfigurierbar. Spring hilft nur, die Anwendung aus Teilen zusammenzubauen. Wenn die Teile von Anfang an ehrlich sind (Abhängigkeiten und Einstellungen sichtbar), arbeitet der Container für Sie. Wenn die Teile alles in Methoden verstecken, wird der Container sie zwar zusammenbauen, aber die Wartung wird schwer.
8. Typische Fehler im Umgang mit hartcodierten Werten
Fehler Nr. 1: Jede Konstante in eine Einstellung verwandeln.
Nach dem ersten Sieg über hartcodierte Werte kommt oft der Wunsch, „einfach alles auszulagern“. In der Folge wächst die Konfiguration an und beginnt, den Geschäftssinn zu steuern, der im Code leben sollte. Wenn ein Wert die Fachlichkeit beschreibt (Bestellstatus, Invarianten, Lebenszyklusregeln), ist das keine Umgebungseinstellung – es ist Teil der Logik.
Fehler Nr. 2: Abhängigkeit und Einstellung verwechseln.
Manchmal sieht der Entwickler, dass der Dienst „irgendetwas“ benötigt, und anstatt eine Objekt-Abhängigkeit (z. B. AuditWriter) zu injizieren, beginnt er, einen String "auditMode" zu injizieren und im Dienst mit if("file") ... else ... zu arbeiten. Das verschlechtert das Design: Der Dienst wird klüger als der Container, und die Verdrahtung breitet sich im Geschäftscode aus.
Fehler Nr. 3: Einstellungen in Methoden verstecken.
Die schlechteste Variante: Wenn der Wert direkt in einer Methode lebt (insbesondere wenn er das Verhalten beeinflusst). Solche Einstellungen vermehren sich schnell, sind schwer zu finden und vermischen sich mit der Logik. Wenn ein Wert wirklich eine „Einstellung“ ist, sollte er ein Konstruktorparameter oder zumindest ein Feld sein, keine lokale Variable mitten in einem Skript.
Fehler Nr. 4: Absolute Pfade und „funktioniert bei mir“.
Sobald im Code ein absoluter Pfad auftaucht, wird die Anwendung an eine bestimmte Maschine gebunden. Selbst wenn Sie einen „hübschen“ Pfad verwenden, existiert er in einer anderen Umgebung möglicherweise nicht mehr. Für den Kurs – schreiben Sie Artefakte strikt in build/; später lernen wir, das über externe Konfiguration zu tun.
Fehler Nr. 5: Schlechte Schlüsselnamen und fehlendes Präfix.
Schlüssel wie dir, mode, value1 erscheinen „praktisch“, solange die Datei klein ist. Sobald es mehr Einstellungen gibt, verwandeln solche Schlüssel die Konfiguration in ein Ratespiel. Das Präfix contextflow.* und aussagekräftige Namen sind keine Bürokratie, sondern eine Möglichkeit, das Projekt lesbar zu halten.
GO TO FULL VERSION