1. Bundles und die Brücke zum Container
Wenn Sie messages.properties in src/main/resources ablegen, gibt es im Projekt mehr Dateien, aber es geschieht keine Magie: Spring wird nicht „telepathisch“ Texte daraus in Ihre Dienste ziehen. Damit Nachrichten Teil der Anwendung werden, benötigt der Container ein Objekt, das anhand eines Schlüssels und eines Locale einen String liefern kann. Ein solches Objekt ist MessageSource.
Stellen Sie sich vor, die Bundle-Dateien sind ein „Buch“ im Regal. Schön, dass das Buch da ist. Aber ohne einen „Bibliothekar“, der weiß, wo es steht, wie man es öffnet und welche Seite man findet, werden Sie trotzdem Strings von Hand schreiben. MessageSource ist genau dieser Bibliothekar.
Unten sehen Sie ein vereinfachtes Schema dessen, was wir zur Laufzeit erreichen wollen:
flowchart TD
S["Service OrderTextService"] --> MS["bean MessageSource"]
MS --> B["message bundles messages_ru.properties messages.properties"]
B --> T["Fertiger Text: Bestellung ORD-1 erstellt"]
Ein wichtiger Punkt: ApplicationContext kann Nachrichten suchen, weil er „breiter“ ist als ein reiner DI-Container. Aber er speichert keine Texte in sich – er delegiert an MessageSource. Und genau hier kommt der Hauptakteur dieser Vorlesung ins Spiel: ein Bean mit dem richtigen Namen und der richtigen Konfiguration.
2. Der Bean-Name messageSource als Vertrag
Das Wort „Vertrag“ klingt etwas streng, aber hier passt es. Spring hat eine Vereinbarung: Wenn es im Kontext einen Bean mit dem Namen messageSource gibt, wird ApplicationContext diesen für die Message-Lookup verwenden. Wenn es einen solchen Bean nicht gibt, erstellt Spring eine eigene Standardvariante (normalerweise so etwas wie eine „delegierende“ Quelle), die Ihnen bei i18n kaum helfen wird.
Die unangenehmste Falle für Anfänger sieht so aus: Sie haben Ihren eigenen ResourceBundleMessageSource erstellt, aber die Methode zum Beispiel messages() genannt – und plötzlich erhalten Sie keine praktische Lokalisierung, sondern einen mysteriösen Autowiring-Fehler, weil „plötzlich“ zwei MessageSource im Container sind: Ihres und das Standard-.
Damit das nicht abstrakt bleibt, hier eine kleine Verhaltenstabelle:
| Was Sie in der Konfiguration gemacht haben | Was im Kontext passiert | Typisches Symptom |
|---|---|---|
| Nichts konfiguriert | Spring erstellt einen standardmäßigen messageSource (dienstlich) | Texte werden nicht gefunden, getMessage verhält sich „leer“/seltsam |
| Bean erstellt und ihn messageSource genannt | Spring verwendet Ihre Nachrichtenquelle | Alles funktioniert, Nachrichten werden aus den Bundles gelesen |
| ResourceBundleMessageSource erstellt, aber den Bean anders genannt | Spring erstellt trotzdem einen dienstlichen messageSource + Sie haben einen zweiten | NoUniqueBeanDefinitionException bei der Injektion von MessageSource |
Hier steckt keine „Magie“ im schlechten Sinne. Es ist einfach eine Regel: Der Name des Beans ist Teil der Konfiguration. Manchmal ist der Name einfach „wie Sie ihn finden werden“. Und manchmal (wie hier) ist es ein integrierter Integrationspunkt von Spring.
3. ResourceBundleMessageSource: Minimum
Jetzt brauchen wir einen teaching default: die einfachste und verständlichste Methode. Für classpath-basierte Bundles eignet sich ResourceBundleMessageSource hervorragend. Er arbeitet auf dem vertrauten Modell von messages.properties, messages_ru.properties, messages_en.properties.
Wichtig: basename wird ohne Locale-Suffixe und ohne .properties angegeben. Das heißt, wenn die Dateien messages*.properties heißen, dann ist der Basename einfach "messages".
Minimaler @Bean (Sie können ihn in Ihre aktuelle AppConfig legen – egal, Hauptsache die Methode heißt messageSource):
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.ResourceBundleMessageSource;
@Bean
MessageSource messageSource() {
// Nachrichtenquelle, die Bundles aus dem classpath liest (src/main/resources)
ResourceBundleMessageSource source = new ResourceBundleMessageSource();
// Basisname der Bundles: ohne _ru/_en und ohne .properties
source.setBasename("messages");
// Explizit Kodierung setzen, um nicht von Umgebung/Einstellungen abhängig zu sein
source.setDefaultEncoding("UTF-8");
// Als MessageSource zurückgeben: Diensten ist die Implementierung egal
return source;
}
Ein paar Erklärungen, damit Sie nicht das Gefühl haben, „nur einen Zauberspruch kopiert“ zu haben.
setBasename("messages") sagt: „Suche Bundles im Classpath, beginnend mit messages“. Das bedeutet, Spring wird die richtige Datei für das Locale und die Fallback-Regeln auswählen, die wir in der letzten Vorlesung besprochen haben.
setDefaultEncoding("UTF-8") ist in modernem Java normalerweise nicht mehr kritisch (ab Java 9 werden Properties in ResourceBundle in UTF-8 gelesen), aber in einem Lernprojekt mache ich das gerne explizit. Erstens ist es beruhigend. Zweitens, wenn Sie jemals zu altem Code oder einer alten Umgebung zurückkehren, müssen Sie kein „\u042f\u0437\u044b\u043a\u0430-Fest“ veranstalten.
Und hier ein Beispiel, wie die Ressourcenseite aussieht (das haben wir bereits in Vorlesung 2 gemacht, ich erinnere nur an den Kontext):
# src/main/resources/messages.properties
order.created=Order {0} created
# src/main/resources/messages_ru.properties
order.created=Bestellung {0} erstellt
Beachten Sie, der Schlüssel ist derselbe. Nur die Werte ändern sich.
4. Überprüfung beim Start
Bevor wir MessageSource in Dienste einbinden, ist es nützlich, eine kleine „Rohrprüfung“ (nicht im Sinne von Klempnerei, sondern im Sinne von Integration) durchzuführen: sicherzustellen, dass der Kontext die Bundles tatsächlich sieht.
Ja, ApplicationContext kann getMessage(...) aufrufen, weil er MessageSource implementiert. Das ist praktisch für den Bootstrap-Teil der Anwendung, wo Sie den Kontext ohnehin in der Hand haben. Innerhalb von Geschäftsdiensten sollte man das nicht tun (das besprechen wir gleich).
Mini-Überprüfung in main():
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.Locale;
try (var ctx = new AnnotationConfigApplicationContext(AppConfig.class)) {
// defaultMessage ("N/A") wird benötigt, um beim Start nicht wegen eines Tippfehlers im Schlüssel abzustürzen
String msg = ctx.getMessage(
"order.created",
new Object[]{"ORD-1"}, // Argumente für {0}, {1}...
"N/A",
Locale.ENGLISH // explizit eine bestimmte Locale prüfen
);
System.out.println(msg); // Order ORD-1 created
}
Hier verwenden wir die Überladung von getMessage mit defaultMessage. Das ist ein hervorragender „Lern-Stabilisator“: Wenn Sie sich im Schlüssel vertippt oder eine Datei vergessen haben, stürzt die Anwendung nicht mit einem riesigen Stacktrace ab, sondern liefert eine verständliche Absicherung.
Wenn Sie das „strenge“ Verhalten bewusst sehen möchten, können Sie die Überladung ohne defaultMessage verwenden. Bei fehlendem Schlüssel erhalten Sie dann NoSuchMessageException. In Produktionscode ist das manchmal nützlich (Fail-Fast), aber für die Anfangsphase ist ein Standardwert praktischer.
5. Injektion von MessageSource in einen Dienst
Jetzt machen wir das Wichtigste: Wir beginnen, MessageSource als normale Abhängigkeit per Constructor Injection zu verwenden. Das ist der Moment, in dem viele Anfänger versehentlich in die falsche Richtung abbiegen und anfangen, ApplicationContext in den Dienst zu ziehen, weil „da ja alles drin ist“.
Ja, da ist alles drin. Aber das ist, als würde man einen ganzen Supermarkt mit sich herumtragen, weil man Brot braucht. Es funktioniert, aber es ist seltsam. Und vor allem macht es Ihren Dienst zu einem Service Locator, obwohl wir im gesamten Kurs das Gegenteil gelernt haben: Abhängigkeiten sollten sichtbar und festgelegt sein.
In ContextFlow ist es praktisch, einen kleinen Dienst auszugliedern, der nur für Texte zuständig ist: zum Beispiel OrderTextService. Er wird kurze Nachrichten für die Konsole, Benachrichtigungen oder Audits erstellen (genau kurze Phrasen; große E-Mail-Vorlagen sind immer noch Resource aus dem letzten Tag).
Beispiel für das „Herz“ eines solchen Dienstes:
import org.springframework.context.MessageSource;
import java.util.Locale;
public class OrderTextService {
private final MessageSource messages;
public OrderTextService(MessageSource messages) {
// MessageSource als normale Abhängigkeit injizieren, ohne ApplicationContext
this.messages = messages;
}
public String orderCreated(String orderId, Locale locale) {
// {0} in properties wird durch orderId ersetzt
return messages.getMessage(
"order.created",
new Object[]{orderId},
"Order created", // Standard, falls Schlüssel nicht gefunden
locale
);
}
}
Beachten Sie ein paar Dinge.
Erstens: Der Dienst weiß nicht, wo die Dateien liegen. Er liest nicht resources. Er öffnet keine Streams. Er sagt: „Ich brauche einen Text per Schlüssel.“ Das macht den Code sauber und leicht.
Zweitens: Locale übergeben wir vorerst als Parameter. Ja, das ist etwas „wortreicher“, als wenn wir die Locale irgendwo global verstecken würden. Aber dafür sehen Sie sofort, dass der Text von der Locale abhängt und nicht „irgendwie manchmal auf Deutsch“ ist.
Drittens: Der Schlüssel "order.created" ist vorerst ein String. Das ist für die ersten Schritte in Ordnung, aber gegen Ende des Tages werden wir Disziplin einführen, damit die Schlüssel nicht wie Glitzer nach einer Silvesterparty im ganzen Projekt verstreut sind (man findet sie dann sogar im Juli).
6. LocaleSelector und Standard-Locale
In Webanwendungen kommt Locale oft „von selbst“ (Browser, Header, LocaleResolver). Wir haben eine Non-Web-Anwendung, daher ist es ehrlich: Die Locale wird entweder in den Daten gespeichert (z. B. bei Customer) oder als Standard in der Konfiguration festgelegt (contextflow.locale.default). Und das Wichtigste: Diese Regel zur Locale-Auswahl sollte nicht an zehn Stellen dupliziert werden.
Dafür ist es praktisch, einen kleinen Helper einzurichten: LocaleSelector. Seine Aufgabe ist einfach: Wenn der Kunde eine preferredLocale hat, verwenden wir sie, sonst nehmen wir die Standard-Locale.
Hier eine sehr geradlinige Implementierung:
import java.util.Locale;
public class LocaleSelector {
private final Locale defaultLocale;
public LocaleSelector(Locale defaultLocale) {
// Standard-Locale – eine zentrale Regel, nicht "in jedem Dienst anders"
this.defaultLocale = defaultLocale;
}
public Locale choose(Locale preferredLocale) {
// Wenn die Locale des Benutzers nicht gesetzt ist, fallen wir auf den Standard zurück
return preferredLocale != null ? preferredLocale : defaultLocale;
}
}
Und hier ein Beispiel, wie man die Standard-Locale aus Properties holt (wir stützen uns auf die vergangenen Tage zu @Value und Conversion; falls Conversion für Locale bei Ihnen nicht konfiguriert ist – können Sie den String Language Tag halten und manuell parsen):
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import java.util.Locale;
@Bean
Locale defaultLocale(@Value("${contextflow.locale.default:en}") String tag) {
// Z.B.: "en", "ru", "ru-RU" – das sind alles Language Tags
return Locale.forLanguageTag(tag);
}
Danach kann LocaleSelector ebenfalls als Bean erstellt werden (in derselben Konfiguration):
import org.springframework.context.annotation.Bean;
@Bean
LocaleSelector localeSelector(Locale defaultLocale) {
// Erstellen LocaleSelector aus der Standard-Locale, sodass die Auswahlregel einheitlich ist
return new LocaleSelector(defaultLocale);
}
Jetzt kann OrderTextService etwas ordentlicher gemacht werden: preferredLocale entgegennehmen und die endgültige Locale innerhalb des Dienstes über LocaleSelector auswählen. So muss der Geschäftscode nicht jedes Mal daran denken „was ist, wenn preferredLocale null ist?“
7. Schlüsseldisziplin: MessageKeys
Wenn das Projekt klein ist, scheint ein String-Schlüssel wie "order.created" harmlos. Wenn das Projekt wächst, werden Sie eines Tages die Situation haben: an einer Stelle "order.created", an einer anderen "order.create", an einer dritten "orderCreated", und dann suchen Sie zwei Stunden, warum „gestern noch alles funktioniert hat“.
Daher ist eine minimale Disziplin nützlich: Schlüssel an einem Ort halten. Man muss keine „Super-Architektur der Schlüssel“ mit Generierung und Reflection bauen. Ein einfacher Konstanten-Klasse reicht aus.
Zum Beispiel so:
public final class MessageKeys {
// Einheitliche Wahrheitsquelle für Schlüssel (um zu vermeiden, dass String-Literale im Projekt vervielfacht werden)
public static final String ORDER_CREATED = "order.created";
public static final String ORDER_CANCELLED = "order.cancelled";
private MessageKeys() {
// Instanzerstellung verbieten: dies ist eine Utility-Klasse
}
}
Und dann wird der Aufruf weniger fragil:
return messages.getMessage(
MessageKeys.ORDER_CREATED, // verwenden Konstante anstelle eines "manuellen" Strings
new Object[]{orderId},
"Order created",
locale
);
Ja, es sind immer noch Strings. Aber jetzt sind sie eine einzige Wahrheitsquelle, nicht zwanzig „fast gleiche“ Literale im Projekt.
Eine kleine Tabelle „was wir behandeln“:
| Wie es aussieht | Was ist schlecht | Wie besser im Lernprojekt |
|---|---|---|
| "order.created" in jeder zweiten Klasse | Tippfehler, Inkonsistenz, schwer zu ändern | MessageKeys.ORDER_CREATED |
| Schlüssel als ganze Phrasen ("Order created successfully") | Text ändert sich → Schlüssel bricht, schwer zu finden | Kurzer semantischer Code (order.created) |
| String-Verkettung im Code | Schlecht lokalisierbar, Mischung aus Logik und Text | Parameter {0}, {1} + Object[] args |
Wir versuchen nicht, „perfekt“ zu sein. Wir versuchen, das Projekt lesbar zu halten und i18n nicht in einen Mini-Krimi zu verwandeln.
8. Typische Fehler bei der Einbindung von MessageSource
Wenn Sie zum ersten Mal MessageSource einbinden, sind die Fehler meist nicht komplex, sondern … ärgerlich. Wie ein Commit mit „fix typo“ über 200 Zeilen. Im Folgenden die häufigsten Stolpersteine, die gerade in einer reinen Spring-Anwendung ohne Boot auftreten, und wie man sie anhand der Symptome erkennt.
Fehler Nr. 1: Der Bean heißt nicht messageSource, und Spring erstellt selbst einen weiteren messageSource.
Das ist besonders tückisch: Sie haben ResourceBundleMessageSource erstellt, aber die Anwendung stürzt beim Start plötzlich mit Mehrdeutigkeit ab, weil es jetzt zwei MessageSource gibt. Ein häufiges Symptom ist NoUniqueBeanDefinitionException für MessageSource. Die Lösung ist banal: Die Methode @Bean muss messageSource() heißen (oder den Namen explizit setzen @Bean("messageSource")), damit Spring keine parallele Standard-Service-Implementierung erstellt.
Fehler Nr. 2: Sie haben einen falschen basename angegeben.
Wenn die Dateien messages_ru.properties heißen und Sie source.setBasename("message") geschrieben haben, findet der Container die Bundles nicht, und Sie erhalten nur den defaultMessage (falls Sie ihn übergeben haben) oder eine NoSuchMessageException. Hier ist es nützlich, die Regel zu beachten: Der Basename ist der gemeinsame Wurzelname der Datei ohne Locale und ohne Erweiterung. In unserem Tag ist das streng "messages".
Fehler Nr. 3: Sie erwarten, dass MessageSource Resource ersetzt und große Vorlagen speichert.
MessageSource ist für kurze Nachrichten nach Schlüssel gedacht, normalerweise 1–2 Zeilen. Ja, technisch gesehen könnte man dort ein „Blatt“ mit einer Seite Text hineinpacken, aber das wird schnell zur Qual: Properties-Dateien sind für große Vorlagen schlecht geeignet, und Parameter {0} sowie das Escaping von Sonderzeichen werden zu Ihrem Hobby. Für große Texte, Benachrichtigungsvorlagen und Berichtsköpfe haben wir bereits das richtige Werkzeug: Resource und Dateien in templates/.
Fehler Nr. 4: Locale wird in jedem Dienst anders ausgewählt, und das Verhalten wird unvorhersehbar.
Es ist sehr leicht, an einer Stelle „wenn preferredLocale null – nehmen wir Locale.ENGLISH“ zu schreiben, an einer anderen „wenn null – nehmen wir ru“, und an einer dritten gar nicht auf null zu prüfen. Dann sehen Sie, dass ein Teil der Nachrichten auf Russisch, ein Teil auf Englisch ist, und denken, Ihre „Lokalisierung sei kaputt“. In Wirklichkeit ist die Disziplin der Locale-Auswahl kaputt. Lagern Sie die Regel in LocaleSelector aus und verwenden Sie sie überall gleich.
Fehler Nr. 5: Schlüssel verteilen sich als Strings über das Projekt, und Sie erwischen Tippfehler nur zur Laufzeit.
Im Gegensatz zu Java-Code prüft der Compiler Schlüssel in Properties nicht. Daher ist ein Tippfehler kein rotes Highlight in der IDE, sondern eine Überraschung beim Start. Das minimale Heilmittel ist MessageKeys (oder eine andere einheitliche Klasse/Stelle für Schlüssel). Fortgeschrittenere Ansätze gibt es, aber für den aktuellen Kurs reicht es, keine String-Literale zu vervielfachen.
GO TO FULL VERSION