CodeGym /Kurse /Spring Core /ConverterFactory und BeanWrapper

ConverterFactory und BeanWrapper

Spring Core
Level 13, Lektion 3
Verfügbar

1. Wenn Converter nicht mehr reicht

Wenn Sie brav einen einzigen Converter<String, NotificationChannel> geschrieben haben, werden Sie mit ziemlicher Sicherheit in ein paar Tagen einen zweiten wollen. In unserem ContextFlow taucht neben NotificationChannel eine weitere typisierte Auswahl aus der Konfiguration auf – zum Beispiel ReportFormat (TEXT/CSV). Und genau hier hat ein Anfänger eine sehr menschliche Reaktion: „Na gut, ich kopiere einfach den Converter, ändere den Typ, und fertig.“ Funktioniert? Funktioniert. Aber das Gefühl, das schon einmal gesehen zu haben, funktioniert auch.

Stellen wir uns eine typische Situation vor. Wir haben zwei Enums:

public enum NotificationChannel {
    // Kanal, über den wir Benachrichtigungen senden
    EMAIL, SMS, CONSOLE
}
public enum ReportFormat {
    // Format, in dem wir den Bericht generieren
    TEXT, CSV
}

Und in den Properties kommt etwa Folgendes an:

contextflow.notifications.default-channel=sms
contextflow.report.format=csv

Wenn Sie die Aufgabe streng über Converter lösen, erhalten Sie schnell zwei fast identische Klassen. Der einzige Unterschied ist, welcher Enum rechts vom Pfeil steht. Und die gleichen Teile sind die String-Normalisierung (trim, toUpperCase) sowie valueOf/switch. Das ist keine Katastrophe (ehrlich), aber es ist der Moment, in dem ein sich wiederholendes Muster entsteht, und Spring bietet ein Werkzeug nicht „für Magie“, sondern zur Reduzierung von Copy-Paste: ConverterFactory.

Es ist wichtig, von vornherein den richtigen Rahmen zu setzen. ConverterFactory ist in kleinen Projekten nicht zwingend erforderlich und sollte nicht als „richtiger, weil komplexer“ wahrgenommen werden. Sie wird erst dann nützlich, wenn Sie die Wiederholung derselben Konvertierungsregel für eine Gruppe ähnlicher Zieltypen sehen. In unserem Fall klingt diese Regel lächerlich einfach: „Nimm einen String, entferne Leerzeichen, wandle in Großbuchstaben um und suche eine gleichnamige Enum-Konstante.“

Converter-Fabrik, nicht „noch ein Converter“

Wenn Sie zum ersten Mal ConverterFactory sehen, könnte es den Anschein haben, dass es sich um „nur eine weitere Variation eines Converters mit komplizierteren Generics“ handelt. Tatsächlich ist die Idee einfacher, als es die Signaturen vermuten lassen: Die Fabrik konvertiert nicht direkt, sondern gibt einen spezifischen Converter für einen bestimmten Zieltyp aus. Das heißt, es ist eine Möglichkeit, Spring mitzuteilen: „Ich kann Converter nach einem Schema erstellen – gib mir die Zielklasse, und ich baue die Konvertierung dafür.“

Damit wir uns nicht verwirren, vergleichen wir das in einer Tabelle (Tabellen sind unsere legale Methode, um keine Listen zu schreiben und gleichzeitig den Text nicht zu Brei werden zu lassen):

Werkzeug Was Sie im Container registrieren Wann ist es praktisch? Wie man denkt
Converter<S, T> Eine einzelne Konvertierung „S → T“ Wenn es einen bestimmten Fall gibt und Sie die volle Kontrolle darüber haben möchten (Aliase, spezielle Regeln, benutzerdefinierte Fehler) „Ich übersetze genau dies in genau das“
ConverterFactory<S, R> Konvertierungsschablone „S → (Familie von Typen, die R sind)“ Wenn es viele Zieltypen gibt und die Konvertierungsregel identisch ist „Ich gebe einen Converter für jedes T aus der Familie aus“

Und hier gibt es eine wichtige praktische Schlussfolgerung. Wenn Sie eine einzigartige Logik haben (z. B. NotificationChannel akzeptiert Synonyme wie stdout → CONSOLE, oder Sie möchten text/plain → TEXT unterstützen), dann ist ein spezifischer Converter in der Regel verständlicher. Wenn Sie jedoch feststellen, dass Sie bereits ReportFormat, AuditMode, LoyaltyLevel und noch fünf weitere Enums haben und alle auf die gleiche Weise aus Strings kommen, dann wird die Fabrik sinnvoll.

2. ConverterFactory für Enums

Bevor wir Code schreiben, eine kleine Warnung. Enums in Java sind mächtig, aber ihre Typisierung über Generics erinnert manchmal an den Versuch, einen IKEA-Schrank ohne Anleitung, dafür mit überflüssigen Schrauben, zusammenzubauen. Das ist nicht Ihre Schuld. Das ist normal. Wir werden die Implementierung so lesbar wie möglich gestalten und ehrlich zugeben, wo wir Typumwandlungen verwenden müssen.

Unser Ziel: eine Factory, die einen String in jeden beliebigen Enum umwandeln kann, Groß-/Kleinschreibung und überflüssige Leerzeichen ignorierend. Hier eine kompakte Version:

import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;

// Konverter-Factory: Liefert für einen Ziel-Enum-Typ einen Converter<String, T>
public class StringToEnumIgnoringCaseConverterFactory
        implements ConverterFactory<String, Enum> {

    @SuppressWarnings({"rawtypes", "unchecked"})
    @Override
    public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
        // source kommt aus den Properties als String
        // targetType ist der konkrete Enum, z.B. NotificationChannel.class oder ReportFormat.class
        return source -> (T) Enum.valueOf((Class) targetType, source.trim().toUpperCase());
    }
}

Was sollte man hier auf menschliche Weise verstehen, nicht nur „auswendig lernen“:

Erstens bedeutet ConverterFactory<String, Enum>: „Quelle ist String, Zieltypen sind alle, die Enum sind“. Das ist also genau eine „Familie“.

Zweitens ist die Methode getConverter(Class<T> targetType) der Moment, in dem die Factory das konkrete Ziel erhält. Heute wird es NotificationChannel.class sein, morgen ReportFormat.class, übermorgen jeder andere Enum, den Sie über @Value einbinden möchten.

Drittens erwartet Enum.valueOf einen String, der mit dem Namen der Konstante übereinstimmt, deshalb führen wir trim() und toUpperCase() aus. Und ja, das ist genau das sich wiederholende Muster, weswegen das Ganze gemacht wird.

Viertens ist @SuppressWarnings hier nicht nur zur Zierde da, sondern um Studenten nicht mit roten Wellenlinien zu erschrecken. In einem echten Projekt können Sie die Warnungen behalten, sorgfältiger typisieren oder ganz auf die Factory verzichten und zwei separate Converter schreiben – das ist eine technische Entscheidung, keine Religion.

Wenn Sie den Fehler freundlicher gestalten möchten, können Sie IllegalArgumentException abfangen und eine eigene mit einer verständlichen Meldung werfen. Wichtig ist, den Converter nicht in einen „intelligenten Dienst“ zu verwandeln, aber eine verständliche Nachricht ist in Ordnung:

import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;

// Dieselbe Factory, aber mit verständlicherer Fehlermeldung für die Konfiguration
public class StringToEnumIgnoringCaseConverterFactory
        implements ConverterFactory<String, Enum> {

    @SuppressWarnings({"rawtypes", "unchecked"})
    @Override
    public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
        return source -> {
            try {
                // Normalisiere den String aus der Konfiguration
                String normalized = source.trim().toUpperCase();
                // Suche Enum-Konstante mit gleichem Namen
                return (T) Enum.valueOf((Class) targetType, normalized);
            } catch (IllegalArgumentException ex) {
                // Konfigurationsfehler sollten laut und deutlich sein
                throw new IllegalArgumentException(
                        "Kann '" + source + "' nicht in " + targetType.getSimpleName() + " konvertieren", ex);
            }
        };
    }
}

Hier ist das Wichtigste: Wir ersetzen keinen unbekannten Wert durch einen „Standard“ (z. B. CONSOLE als Fallback). Wir betrachten einen unbekannten Wert als Konfigurationsfehler und lassen es explizit scheitern. Das entspricht dem Stil des Kurses: Besser laut starten scheitern als leise „irgendwie funktionieren“ und später seltsame Bugs zu jagen.

3. Einbindung der ConverterFactory in den ConversionService

Nachdem wir die Factory haben, ist es wichtig, keine Klasse zu erstellen, die „einfach nur im Projekt rumliegt“ und niemandem nützt. Unsere Regel bleibt: Alle Konvertierungen sind Teil der allgemeinen Container-Infrastruktur. Also muss die Factory in dem ConversionService registriert werden, der innerhalb des ApplicationContext lebt.

Die minimale Konfiguration sieht so aus:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;

@Configuration
public class ConversionConfig {

    @Bean
    public ConversionService conversionService() {
        // Allgemeiner ConversionService für den ApplicationContext
        DefaultConversionService cs = new DefaultConversionService();

        // Registriere die Factory für die Schablone: String -> beliebiger Enum
        cs.addConverterFactory(new StringToEnumIgnoringCaseConverterFactory());

        return cs;
    }
}

Beachten Sie die fast „magische“, aber tatsächlich sehr pragmatische Sache: Der Methodenname conversionService(). In Spring ist dies eine übliche Konvention: Wenn es im Kontext einen Bean mit dem Namen conversionService gibt, versucht der Container, ihn als allgemeinen Konvertierungsmechanismus an verschiedenen Stellen zu verwenden (auch dort, wo Werte als Strings kommen und etwas Intelligenteres werden sollen).

Es gibt noch eine Nuance, die man im Kopf behalten sollte, auch wenn Sie heute nicht tiefer einsteigen werden. Wenn Sie eine allgemeine Schablone (Factory) registrieren und gleichzeitig irgendwo einen spezifischen Converter für einen einzelnen Enum registrieren, kann es zu einem Konflikt kommen: Beide können String -> NotificationChannel. Im DefaultConversionService zählt die Reihenfolge der Registrierung, und meistens gewinnt der „spätere/spezifischere“. In der Praxis geht man daher meist so vor: zuerst die allgemeine Factory hinzufügen, dann ein oder zwei spezifische Converter für die Typen, bei denen besondere Regeln benötigt werden. Im Rahmen dieser Vorlesung reicht es zu merken: „Allgemeine Regeln zuerst, spezifische später“.

4. Conversion und Binding

Bisher haben wir immer gesagt: „Ein String wird in einen Typ umgewandelt“. Das ist Conversion. Aber in Wirklichkeit macht der Container zwei verschiedene Arbeiten, und sie zu verwechseln ist eine der beliebtesten Quellen von Verwirrung. Conversion ist wie ein Übersetzer: Er sagt Ihnen, dass "csv" ReportFormat.CSV bedeutet. Binding hingegen ist wie ein Kurier: Er muss diese Bedeutung an die richtige Stelle liefern, also in den Konstruktorparameter oder in eine Bean-Eigenschaft. Wenn der Kurier die Adresse verwechselt, ist die Übersetzung perfekt, aber Sie erhalten trotzdem einen Fehler.

Für unseren Kurs ist es wichtig, eine einfache Kette zu behalten:

1) Spring findet den Textwert in den Property-Quellen (z. B. "csv").
2) Spring konvertiert ihn in den erforderlichen Typ (z. B. ReportFormat.CSV).
3) Spring bindet (bindet) diesen Wert an die Injektionsstelle (dependency injection point).

Die Injektionsstelle kann ein Konstruktorparameter (unser geliebter Teaching Default) oder ein Setter/Feld sein (was wir im Business-Code zu vermeiden versuchen, was es aber immer noch in der Welt gibt).

Um das Bild anschaulicher zu machen, hier ein kleines Diagramm:

flowchart TD
    A["contextflow.report.format = 'csv' (String)"] --> B["Placeholder-Auflösung"]
    B --> C["ConversionService String -> ReportFormat"]
    C --> D["Binding"]
    D --> E["Konstruktorparameter ReportFormat format"]

Und genau hier kommt BeanWrapper ins Spiel. Es ist einer der infrastrukturellen Mechanismen von Spring, der speziell beim zweiten Teil hilft – beim Binding, insbesondere wenn es um Eigenschaften (properties) eines Objekts geht, also um das Setter-basierte Modell. In unserem Projekt verwenden wir hauptsächlich Konstruktor-Injektion, aber BeanWrapper zu verstehen ist dennoch aus zwei Gründen nützlich. Erstens werden Sie besser verstehen, wie Spring „ein Objekt mit Werten füllen“ kann. Zweitens werden Sie Binding nicht mehr als Magie betrachten – es ist einfach Infrastruktur-Code, der auf Eigenschaften eines Objekts zugreifen kann und bei Bedarf Conversion verwendet.

5. BeanWrapper als Binding-Mechanismus

Jetzt kommt ein wichtiger Moment: Wir schauen uns BeanWrapper „manuell“ an, nicht weil wir möchten, dass Sie damit Business-Logik schreiben, sondern weil es so ist, als ob man unter die Motorhaube eines Autos schaut. Danach müssen Sie den Motor nicht in der Küche zusammenbauen, aber Sie verstehen zumindest, warum er manchmal Geräusche macht.

Zuerst erstellen wir eine einfache Klasse mit „richtigen“ Java-Properties (also mit Setter und Getter). Dies sollen die Berichtseinstellungen sein:

public class ReportSettings {

    // Ordner, in den wir Berichte ablegen (einfacher String – keine Konvertierung)
    private String outputDir;

    // Berichtsformat (Enum – erfordert Konvertierung aus String "csv"/"text")
    private ReportFormat format;

    public void setOutputDir(String outputDir) {
        // BeanWrapper findet diesen Setter über den Eigenschaftsnamen "outputDir"
        this.outputDir = outputDir;
    }

    public void setFormat(ReportFormat format) {
        // Hier sollte bereits ein konvertierter Enum ankommen
        this.format = format;
    }

    public ReportFormat getFormat() {
        return format;
    }
}

Jetzt nehmen wir BeanWrapperImpl und versuchen, String-Werte so zu „binden“, wie es die Container-Infrastruktur tun könnte:

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.core.convert.support.DefaultConversionService;

public class BeanWrapperDemo {
    public static void main(String[] args) {
        // 1) Erstelle ConversionService und registriere die Factory für Enums
        DefaultConversionService cs = new DefaultConversionService();
        cs.addConverterFactory(new StringToEnumIgnoringCaseConverterFactory());

        // 2) Zielobjekt, in das wir die Properties "binden" werden
        ReportSettings settings = new ReportSettings();

        // 3) BeanWrapper arbeitet über dem Objekt und kann Properties über den Namen setzen
        BeanWrapper wrapper = new BeanWrapperImpl(settings);

        // 4) Wichtig: Ohne ConversionService kann er String -> ReportFormat nicht durchführen
        wrapper.setConversionService(cs);

        // 5) Einfache Zuweisung eines Strings an ein String-Property
        wrapper.setPropertyValue("outputDir", "build/reports");

        // 6) Zuweisung eines Strings an ein Enum-Property: hier kommt Conversion zum Einsatz
        wrapper.setPropertyValue("format", "csv");

        System.out.println(settings.getFormat()); // CSV
    }
}

Was passiert hier Schritt für Schritt:

wrapper.setPropertyValue("outputDir", "build/reports") findet den Setter setOutputDir(String) und ruft ihn auf. Keine Magie – es ist ein normaler Methodenaufruf, der über JavaBeans-Introspection gefunden wurde.

wrapper.setPropertyValue("format", "csv") erkennt, dass der Setter ReportFormat erwartet, wir aber einen String übergeben haben. Und hier verwendet er ConversionService (den wir explizit über setConversionService(cs) gesetzt haben), um "csv" in ReportFormat.CSV umzuwandeln, und ruft erst dann setFormat(ReportFormat) auf.

Wenn Sie versuchen, einen Fehler im Eigenschaftsnamen zu machen, zeigt sich das sehr deutlich:

wrapper.setPropertyValue("formatt", "csv"); // Tippfehler

Das Ergebnis ist eine Exception (z. B. NotWritablePropertyException). Und das ist gut. Aus Sicht des Containers ist das ein Wiring-/Konfigurationsfehler: Sie versuchen, einen Wert an eine Stelle zu binden, an die er nicht gebunden werden kann.

Und hier kommen wir zur „begrenzten Rolle“ von BeanWrapper im Kurs. Er hilft zu verstehen, dass Binding eine Infrastrukturarbeit ist: Eigenschaft finden, Setter finden, ggf. konvertieren, Wert setzen. Aber wenn Sie anfangen, BeanWrapper in Business-Services zu verwenden, erhalten Sie mit ziemlicher Sicherheit Code, der sich schlecht refaktorisieren, schlecht lesen und schlecht vom Compiler prüfen lässt. Denn der String "format" ist kein typisierter Verweis, sondern eine fragile Vereinbarung.

Im Kontext von ContextFlow klingt diese Regel sehr projektbezogen: Lassen Sie @Value und die Konvertierung in Settings/Config-Beans leben, und die Services erhalten bereits normale Typen (ReportFormat, NotificationChannel) über den Konstruktor. Dann weiß die Business-Schicht nicht einmal, dass dort irgendwo Strings und Leerzeichen waren.

6. Typische Fehler: Conversion und Binding

Fehler Nr. 1: Die ConverterFactory in eine „Allzweckmaschine für alles“ verwandeln.
Es ist sehr leicht, sich zu verleiten: Da die Factory Converter ausgeben kann, machen wir eine Factory, die sowohl Enums als auch Daten, „ja/nein“ und „vielleicht“ (insbesondere „vielleicht“) versteht. In der Praxis führt das schnell zu einer unvorhersehbaren Logik, die schwer zu debuggen ist. Für ein Studienprojekt gilt die einfache Regel: Eine Factory sollte eine einzige verständliche Schablone abdecken, z. B. „String → Enum, Groß-/Kleinschreibung ignorierend“.

Fehler Nr. 2: Eine ConverterFactory zu früh einsetzen, wenn ein einzelner Converter ausgereicht hätte.
Wenn Sie nur einen einzigen NotificationChannel haben, ist ein separater Converter<String, NotificationChannel> einfacher zu lesen und bietet mehr Kontrolle (z. B. kann man explizit Aliase unterstützen oder schöne Fehlermeldungen ausgeben). ConverterFactory wird erst sinnvoll, wenn die Wiederholung tatsächlich auftritt. Bis dahin ist eine Verkomplizierung einfach nur eine Verkomplizierung.

Fehler Nr. 3: „Stille Defaults“ bei unbekanntem Wert setzen.
Manchmal neigt man dazu zu schreiben: „Wenn der Wert unbekannt ist, nehmen wir CONSOLE“. Das klingt „zuverlässig“, aber in Wirklichkeit verstecken Sie einen Konfigurationsfehler. Es ist viel besser, gleich beim Start des Containers mit einer verständlichen Meldung zu scheitern. In der Konfiguration sollte ein Fehler laut sein – sonst debuggen Sie das Verhalten statt der Ursache.

Fehler Nr. 4: BeanWrapper als normales Alltags-Werkzeug für die Anwendungs-/Service-Schicht betrachten.
BeanWrapper ist ein infrastrukturelles Ding. Es arbeitet mit Eigenschaften über String-Namen, was bedeutet: IDE-Refactoring schützt Sie nicht, der Compiler hilft nicht, und ein Fehler erscheint erst zur Laufzeit. Im Business-Code unseres Kurses halten wir bewusst an der Konstruktor-Injektion und typisierten Abhängigkeiten fest. BeanWrapper ist als mentales Modell nützlich, nicht als „das machen wir jetzt überall so“.

Fehler Nr. 5: Erwarten, dass BeanWrapper „irgendwie von alleine draufkommt“, wenn es keinen Setter gibt oder der Eigenschaftsname nicht übereinstimmt.
BeanWrapper ist kein Gedankenleser. Wenn das Objekt keinen setFormat(...) hat, kann kein Wert gebunden werden. Wenn die Eigenschaft outputDir heißt, Sie aber "output-dir" schreiben, ist das auch keine Übereinstimmung. Spring kann an verschiedenen Stellen zusätzliche Naming-Tricks anwenden, aber im Basismodell (das wir in diesem Kurs aufbauen) basiert alles auf dem klaren JavaBeans-Vertrag: Eigenschaftsname ↔ Setter/Getter.

1
Aufgabe
Spring Core, Level 13, Lektion 3
Gesperrt
Eine ConverterFactory für zwei Enum-Einstellungen
Eine ConverterFactory für zwei Enum-Einstellungen
1
Aufgabe
Spring Core, Level 13, Lektion 3
Gesperrt
Manuelles Binding über BeanWrapperImpl
Manuelles Binding über BeanWrapperImpl
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION