CodeGym /Kurse /Spring Core /Von Strings zu typisierten Werten

Von Strings zu typisierten Werten

Spring Core
Level 13, Lektion 0
Verfügbar

1. String-Werte in properties

Ehrlich gesagt, die Konfiguration in Textform ist keine Verschwörung gegen Java-Entwickler. Es ist einfach historisch das bequemste und portabelste Format: .properties – Text, Umgebungsvariablen – Strings, Kommandozeilenargumente – wieder Strings. Selbst wenn Sie von einer Welt träumen, in der Einstellungen schöne Objekte sind, sagt die Realität: „Halt „sms“ und sei nicht wählerisch.“ Daher tritt das Problem in jeder Anwendung auf, selbst wenn sie klein und konsolenbasiert ist.

Lassen Sie uns einen wichtigen Gedanken festhalten: Strings in der Konfiguration sind normal. Abnormal ist etwas anderes: wenn diese Strings beginnen, in den Geschäftscode einzudringen, sich zu vermehren und sich in Mini-Geschwüre im ganzen Projekt zu verwandeln.

Eine typische Einstellung in unserem ContextFlow könnte so aussehen:

# src/main/resources/contextflow.properties
# Ein Mensch liest das leicht, aber für die Anwendung sind es vorerst nur Strings.
contextflow.app-name=ContextFlow
contextflow.notifications.default-channel=sms

Der Wert sms ist eine absolut angemessene Notation für einen Menschen. Aber für Java ist es nicht NotificationChannel.SMS. Es sind nur drei Zeichen, die mit Leerzeichen, in anderer Groß-/Kleinschreibung, mit Tippfehlern oder als „sms, bitte“ kommen können.

2. Wie String in die Services gelangt

Das Problem tritt selten „in Reinkultur“ auf. Normalerweise beginnt alles harmlos: Wir lesen einen String über @Value und speichern ihn irgendwo in einem Bean, das für die Einstellungen zuständig ist. Und dann … dann entscheidet jemand (manchmal Sie selbst): „Ach was, ich parse es im Service selbst.“ Und dieses „selber parsen“ ist wie der kleine Schneeball, der nach ein paar Wochen zu einer Lawine wird.

Zuerst scheint wirklich alles ok. Hier ist ein einfaches Settings-Bean, das den Rohwert erhält:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class RawNotificationSettings {

    // Rohe Einstellungen: Vorerst ist dies nur ein String aus der Konfiguration.
    private final String defaultChannel;

    public RawNotificationSettings(
            @Value("${contextflow.notifications.default-channel}") String defaultChannel) {
        // Hier gibt es keine Validierung oder Konvertierung – wir speichern den String einfach so.
        this.defaultChannel = defaultChannel;
    }

    public String defaultChannel() {
        return defaultChannel;
    }
}

An dieser Stelle beruhigen sich viele. Der Container hat den String eingesetzt – das Bean wurde erstellt – die Anwendung startet. Aber später im Projekt stellt sich die Frage: „Wie wählen wir den NotificationSender aus, wenn wir mehrere Implementierungen haben (email/sms/console)?“ Und dann kommt die Versuchung, den Service selbst erraten zu lassen, was dieser String bedeutet.

3. Anti-Pattern: Parsing in der Geschäftsmethode

Jetzt wird es etwas grausam, aber nützlich: Werfen wir einen Blick auf das häufigste Anti-Pattern. Der Service erhält einen String und wandelt ihn selbst in einen Domänentyp um. Scheinbar „nur eine Zeile“. Scheinbar „was kann schon schiefgehen“. Spoiler: Es kann buchstäblich alles schiefgehen.

Beginnen wir mit dem klassischen valueOf().

// Rohe Daten "wie sie ankommen".
String rawValue = "sms";

// Enum.valueOf(...) erfordert den EXAKTEN Namen der Konstante, inklusive Groß-/Kleinschreibung.
NotificationChannel channel = NotificationChannel.valueOf(rawValue);
// IllegalArgumentException: No enum constant ... NotificationChannel.sms

Der Kommentar ist hier wichtiger als der Code: valueOf() erwartet eine exakte Übereinstimmung mit dem Namen der Enum-Konstante, inklusive Groß-/Kleinschreibung. Das heißt, „SMS“ funktioniert, aber „sms“ schlägt fehl. Und Ihre Konfiguration liebt es, in Kleinbuchstaben zu sein, weil Menschen keine Compiler sind.

Die nächste „Evolutionsstufe“ – der Entwickler fügt eine Normalisierung hinzu, und der Code wird etwas länger, lebt aber immer noch innerhalb des Services:

// Jetzt haben wir einen "Schutz" vor Leerzeichen und Groß-/Kleinschreibung.
String rawValue = " sms ";

// Aber diese Normalisierungslogik lebt jetzt im Geschäftscode.
NotificationChannel channel =
        NotificationChannel.valueOf(rawValue.trim().toUpperCase());
// channel = SMS

An dieser Stelle stellt sich oft ein Siegesgefühl ein. Wir haben Leerzeichen und Groß-/Kleinschreibung besiegt. Aber es ist ein Sieg auf dem Niveau von „Ich habe den Müll aus dem Zimmer getragen – und schon ist das Leben leichter“. Ja, es ist leichter. Aber der Müll liegt jetzt im Flur.

Die Probleme dieses Ansatzes sind fast immer die gleichen.

Erstens beginnt sich das Parsing zu duplizieren. Ein Service macht trim().toUpperCase(), ein anderer macht strip().toUpperCase(Locale.ROOT), ein dritter unterstützt aus irgendeinem Grund Aliase „text“ → „TEXT“, ein vierter hat es einfach vergessen und valueOf() gelassen.

Zweitens zeigt sich der Konfigurationsfehler spät. Wenn das Parsing in der Geschäftsmethode lebt, kann die Anwendung starten, einen Teil der Szenarien verarbeiten und dann „irgendwo in der Mitte“ aufgrund einer falschen Einstellung abstürzen. Das ist besonders unangenehm, weil der Fehler nicht mehr wie ein „Startfehler“ aussieht – er sieht aus wie ein „zufälliger Laufzeitfehler“.

Drittens beginnt das rohe Konfigurationsformat, dem Geschäftscode Regeln zu diktieren. Die Geschäftsmethode weiß plötzlich, dass die Werte in Kleinbuchstaben kommen, dass es Leerzeichen gibt, dass „sms“ manchmal als „text“ geschrieben wird (weil jemand es verwechselt). Das ist keine Geschäftslogik mehr. Es ist die „Logik des Überlebens neben Texteinstellungen“.

Um dies ganz deutlich zu machen, vergleichen wir die beiden Ansätze in einer kleinen Tabelle:

Frage Wenn wir im Service parsen Wenn wir an der Konfigurationsgrenze parsen
Wo lebt das Wissen über das String-Format? Im Geschäftscode (schlecht sichtbar, leicht zu vervielfältigen) In der Konfigurationsinfrastruktur (ein Ort)
Wann zeigt sich der Fehler? Spät, zur Laufzeit, mitten im Szenario Früh, bei der Erstellung der Beans (Fail-Fast)
Wie oft schreiben Sie das Parsing? Normalerweise oft (und jedes Mal etwas anders) Normalerweise einmal (oder ein Mechanismus)
Was bekommt der Service? „rohen“ String Domänentyp (NotificationChannel, ReportFormat)

4. Conversion und Binding

Es ist Zeit, zwei Begriffe des Tages einzuführen, aber ohne akademischen Nebel. Type Conversion ist, wenn wir einen Wert eines Typs in einen Wert eines anderen Typs umwandeln. In unserem Fall: den String „sms“ in NotificationChannel.SMS umwandeln. Binding ist, wenn wir einen bereits fertigen Wert an einen Punkt im Objekt „binden“ (verknüpfen): an einen Konstruktorparameter oder ein Feld (über Setter), damit das Bean seinen Wert als Teil der Erstellung erhält.

Wenn man eine Analogie möchte: Conversion ist der Übersetzer, und Binding ist der Kurier, der den übersetzten Text ins richtige Büro bringt. Der Übersetzer sollte nicht durch das ganze Büro laufen und Leute suchen, und der Kurier sollte nicht selbst im Kopf aus dem Japanischen übersetzen. Die Trennung der Rollen macht das System weniger chaotisch.

So sieht die Kette aus, die wir im Kopf (und später im Projekt) haben wollen:

flowchart TD
    A["contextflow.properties
contextflow.notifications.default-channel=sms"] --> B["PropertySource / Environment"] --> C["@Value(${...}) placeholder"] --> D["roher String: "sms""] --> E["Typkonvertierung"] --> F["typisierter Wert: NotificationChannel.SMS"] --> G["Binding an den Konstruktorparameter"] --> H["fertiges Bean"]

Beachten Sie einen wichtigen Punkt: Auf Service-Ebene interessiert uns nur H, das fertige Bean, das bereits in der Welt der normalen Typen lebt. Alles „Dreckige“ – Strings, Leerzeichen, Groß-/Kleinschreibung, Tippfehler – sollte früher, an der Konfigurationsgrenze, gestoppt werden.

Und hier kommt die gute Nachricht: Spring als Container kann tatsächlich bereits Conversion und Binding im Rahmen der Bean-Erstellung durchführen. Wir haben es nur bisher nicht bewusst aktiviert und nicht für unsere Domänentypen erweitert. Vorerst ist es wichtig, die architektonische Absicht zu verstehen: Typen sollten so früh wie möglich erscheinen.

5. Zielstil: Typen statt Strings

Lassen Sie uns nun festhalten, wie der Zielzustand im Code aussieht. Wir möchten, dass es in der Anwendung „Einstellungs-Beans“ gibt, die für das Lesen der Konfiguration verantwortlich sind, und dass diese Beans bereits typisierte Werte nach außen geben. Die Serviceschicht sollte dabei nicht wissen, dass es irgendwo einen Schlüssel contextflow.notifications.default-channel gab, und sie sollte auf keinen Fall trim() oder toUpperCase() ausführen.

Zunächst zeigen wir, wie der „rohe“ Zustand aussieht (wir haben ihn bereits oben gesehen):

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class RawNotificationSettings {

    // Die Services müssen selbst entscheiden, was "sms" ist und wie man es parst.
    private final String defaultChannel;

    public RawNotificationSettings(
            @Value("${contextflow.notifications.default-channel}") String defaultChannel) {
        this.defaultChannel = defaultChannel;
    }

    public String defaultChannel() {
        return defaultChannel;
    }
}

Und jetzt die Zielvariante. Sie ist fast gleich … aber der Unterschied ist grundlegend:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class TypedNotificationSettings {

    // Hier ist bereits ein Domänentyp: Die Services erhalten einen fertigen Wert, keinen String.
    private final NotificationChannel defaultChannel;

    public TypedNotificationSettings(
            @Value("${contextflow.notifications.default-channel}")
            NotificationChannel defaultChannel) {
        // Wenn die Konvertierung nicht funktioniert – besser beim Start fehlschlagen (Fail-Fast).
        this.defaultChannel = defaultChannel;
    }

    public NotificationChannel defaultChannel() {
        return defaultChannel;
    }
}

Hier haben wir eine Sache gemacht: Wir haben den Container gebeten, ein Bean zu erstellen, in dessen Konstruktor nicht String, sondern NotificationChannel eingesetzt werden soll. Das bedeutet, dass irgendwo im Container die Fähigkeit existieren muss, eine Conversion „String → NotificationChannel“ durchzuführen.

Wichtig: Jetzt könnten Sie denken: „Okay, funktioniert das wirklich ohne zusätzliche Konfiguration?“ Und das ist ein sehr richtiger Gedanke. Wir haben noch nicht besprochen, wie Spring genau Konverter auswählt und anwendet. Aber hier reicht es, das Bild des Ergebnisses festzuhalten: Der Container muss in der Lage sein, String in NotificationChannel umzuwandeln, bevor der Wert in die Serviceschicht gelangt.

6. Fail-Fast für Konfiguration

Es gibt eine technische Gewohnheit, die „Code, der manchmal funktioniert“ von „einer Anwendung, die man starten kann, ohne sich zu schämen“ unterscheidet: besser beim Start fehlschlagen als still falsch leben. Bei Spring ist dies in die Denkweise eingebaut: Der Container versucht, den gesamten Objektgraphen zusammenzustellen, Abhängigkeiten zu überprüfen und sicherzustellen, dass beim ersten Aufruf der Geschäftsmethode alles bereit ist.

Wenn Ihre Konfiguration als String kommt und irgendwo in einer Geschäftsmethode geparst wird, verlieren Sie diesen Vorteil. Die Anwendung kann starten und dann plötzlich bei der ersten Bestellung abstürzen, weil jemand im Konfig smms statt sms geschrieben hat. Solche Fehler sind besonders tückisch, weil sie normalerweise nicht von Tests (wenn der Test nicht diesen Pfad durchlaufen hat), sondern durch einen echten Start erwischt werden.

Wenn Sie die Conversion an die Grenze (zum Zeitpunkt des Bindings in den Bean-Konstruktor) verschieben, wird der Fehler „laut und früh“. Der Kontext wird nicht aufgebaut, und die Anwendung sagt: „Leute, meine Einstellung ist falsch. Ich werde nicht einmal so tun, als ob alles gut ist.“ In einer Lern-Konsolenanwendung ist das genauso nützlich wie in der Produktion: Sie starten das Projekt und sehen sofort das Problem.

In ContextFlow ist dies besonders angebracht, weil Einstellungen wie der standardmäßige Benachrichtigungskanal oder das Berichtsformat keine „Dekoration“ sind. Sie beeinflussen tatsächlich das Verhalten: wohin die Benachrichtigung geht, welches Format der Bericht erhält, wohin die Datei geschrieben wird. Je früher wir diese Konfiguration überprüfen, desto weniger Überraschungen wird es in den Szenarien geben.

7. Typische Fehler bei der Konvertierung

Fehler Nr. 1: Den String im Service „für später“ lassen.
Häufige Logik klingt so: „Jetzt nehmen wir den String, und später, wenn wir die Benachrichtigung senden, parsen wir ihn.“ Das Problem ist, dass „später“ normalerweise an drei verschiedenen Stellen, von drei verschiedenen Personen und mit drei verschiedenen trim()-Aufrufen passiert. Infolgedessen vermehrt sich das Parsing und das String-Format wird zu einem impliziten Vertrag für den Geschäftscode.

Fehler Nr. 2: Einmal parsen, aber immer noch innerhalb der Geschäftsmethode.
Manchmal versteht der Entwickler, dass Duplizierung böse ist, und lagert das Parsing in eine private Methode des Services aus. Das ist eine Verbesserung, aber kein Sieg: Das Konfigurationsformat dringt immer noch in die Serviceschicht ein. Der Service beginnt zu viel darüber zu wissen, was ein „roher Input“ ist, obwohl er in der Welt der Domänentypen leben sollte.

Fehler Nr. 3: „Absicherung“ durch einen stillen Standardwert, der den Fehler verbirgt.
${key:DEFAULT} einzustellen ist praktisch, aber leicht zu einer Möglichkeit, das Problem zu verstecken. Wenn zum Beispiel der Benachrichtigungskanal nicht festgelegt ist, kann man „für alle Fälle“ CONSOLE machen. Und Sie erhalten tatsächlich einen funktionierenden Start … nur um dann lange herauszufinden, warum im Demo-Modus Benachrichtigungen „irgendwie“ nicht an SMS gesendet werden. Standardwerte sind gut, wenn sie sinnvoll und erwartet sind, nicht wenn sie eine falsche Konfiguration maskieren.

Fehler Nr. 4: Konfigurationslesen und Geschäftsaktion in einer Klasse mischen.
Man möchte sehr gerne einen „intelligenten Service“ machen: Er liest @Value, wählt die Implementierung aus, sendet die Benachrichtigung, schreibt das Audit und, wenn nötig, beruhigt er sich selbst. Am Ende hat man eine Kombi-Klasse, die schwer zu testen, schwer zu lesen und schwer weiterzuentwickeln ist. Viel ruhiger ist es, wenn es ein separates Settings-Bean gibt, das nur für die typisierte Konfiguration verantwortlich ist, und die Serviceschicht die fertigen Typen verwendet und sich um das Szenario kümmert.

Fehler Nr. 5: Properties-Schlüssel im Projekt verstreuen.
Wenn Schlüssel wie contextflow.notifications.default-channel in drei Klassen auftauchen, erhalten Sie eine „magische Zeichenfolge“ auf einer anderen Ebene: einen Schlüssel zu ändern bedeutet, alle Stellen zu finden, an denen er erwähnt wird. Daher ist es eine gute Angewohnheit, @Value und Schlüssel nahe bei den Settings-Beans zu halten und den Services Abhängigkeiten auf „Einstellungen als Objekt“ zu geben, nicht auf „Einstellungen als String“.

1
Aufgabe
Spring Core, Level 13, Lektion 0
Gesperrt
Roher Lieferkanal im Service
Roher Lieferkanal im Service
1
Aufgabe
Spring Core, Level 13, Lektion 0
Gesperrt
Typisiertes Berichtsformat an der Konfigurationsgrenze
Typisiertes Berichtsformat an der Konfigurationsgrenze
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION