4. Aktionen: das Modell
Kehren wir zur Architektur einer Spring-MVC-Anwendung zurück:
![]() |
Im vorigen Kapitel haben wir uns den Prozess angesehen, der die Anfrage [1] an den Controller und die Aktion [2a] weiterleitet, die sie bearbeiten wird – ein Mechanismus, der als Routing bekannt ist. Außerdem haben wir die verschiedenen Antworten vorgestellt, die eine Aktion an den Browser zurücksenden kann. Bislang haben wir Aktionen vorgestellt, die die an sie gerichtete Anfrage nicht verarbeitet haben. Eine Anfrage [1] enthält verschiedene Informationen, die Spring MVC [2a] der Aktion in Form eines Modells zur Verfügung stellt. Dieser Begriff sollte nicht mit dem M-Modell einer V-Ansicht [2c] verwechselt werden, die von der Aktion erzeugt wird:
![]() |
- Die HTTP-Anfrage des Clients trifft bei [1] ein;
- in [2] werden die in der Anfrage enthaltenen Informationen in ein Aktionsmodell [3] umgewandelt, oft, aber nicht unbedingt eine Klasse, das als Eingabe für die Aktion [4] dient;
- an [4] generiert die Aktion auf der Grundlage dieses Modells eine Antwort. Diese Antwort besteht aus zwei Komponenten: einer Ansicht V [6] und dem Modell M dieser Ansicht [5];
- die Ansicht V [6] verwendet ihr Modell M [5], um die für den Client bestimmte HTTP-Antwort zu generieren.
Im MVC-Modell ist die Aktion [4] Teil des C (Controllers), das Ansichtsmodell [5] ist das M und die Ansicht [6] ist das V.
Dieses Kapitel untersucht die Mechanismen zur Verknüpfung der in der Anfrage enthaltenen Informationen – bei denen es sich naturgemäß um Zeichenfolgen handelt – mit dem Aktionsmodell, bei dem es sich um eine Klasse mit Eigenschaften verschiedener Typen handeln kann.
Hinweis: Der Begriff [Aktionsmodell] ist kein anerkannter Begriff.
Wir erstellen einen neuen Controller für diese neuen Aktionen:
![]() |
Der [ActionModelController] sieht vorerst wie folgt aus:
package istia.st.springmvc.controllers;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ActionModelController {
}
- Zeile 5: Beachten Sie, dass die Annotation [@RestController] bewirkt, dass die an den Client gesendete Antwort die String-Serialisierung der Aktionsergebnisse des Controllers ist;
4.1. [/m01]: GET-Parameter
Wir fügen die folgende [/m01]-Aktion hinzu:
// ----------------------- retrieve parameters with GET------------------------
@RequestMapping(value = "/m01", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
public String m01(String nom, String age) {
return String.format("Hello [%s-%s]!, Greetings from Spring Boot!", nom, age);
}
- Zeile 4: Die Aktion akzeptiert zwei Parameter namens [name] und [age]. Sie werden mit Parametern initialisiert, die in der HTTP-GET-Anfrage dieselben Namen tragen;
Die Ergebnisse in Chrome lauten wie folgt [1-3]:
![]() |
- in [1] die GET-Anfrage mit den Parametern [name] und [age];
- in [3] sehen wir, dass die Aktion [/m01] diese Parameter erfolgreich abgerufen hat;
4.2. [/m02]: POST-Parameter
Wir fügen die folgende [/m02]-Aktion hinzu:
// ----------------------- retrieve parameters with POST------------------------
@RequestMapping(value = "/m02", method = RequestMethod.POST, produces = "text/plain;charset=UTF-8")
public String m02(String nom, String age) {
return String.format("Hello [%s-%s]!, Greetings from Spring Boot!", nom, age);
}
- Zeile 4: Die Aktion akzeptiert zwei Parameter namens [name] und [age]. Sie werden mit den gleichnamigen Parametern aus der HTTP-POST-Anfrage initialisiert;
Die Ergebnisse mit [Advanced REST Client] lauten wie folgt:
![]() |
- In [1-3] die POST-Anfrage mit den Parametern [name] und [age];
- In [4-5] haben wir den HTTP-Header [Content-Type] für die POST-Anfrage festgelegt. Er muss lauten: [Content-Type: application/x-www-form-urlencoded];
- in [6] enthält [Form Data] die Liste der Parameter für einen POST-Vorgang. Hier sehen wir die Parameter [name] und [age];
- in [7] zeigt die Serverantwort, dass die Aktion [/m02] die Parameter [name] und [age] erfolgreich abgerufen hat;
4.3. [/m03]: Parameter mit denselben Namen
In Abschnitt 2.5.2.8 haben wir gesehen, dass die Mehrfachauswahlliste Parameter mit denselben Namen an den Server senden kann. Schauen wir uns an, wie eine Aktion diese abrufen kann. Wir fügen die folgende [/m03]-Aktion hinzu:
// ----------------------- retrieve parameters with the same names-----------------
@RequestMapping(value = "/m03", method = RequestMethod.POST, produces = "text/plain;charset=UTF-8")
public String m03(String nom[]) {
return String.format("Hello [%s]!, Greetings from Spring Boot!", String.join("-", nom));
}
- Zeile 2: Die Aktion akzeptiert einen Parameter namens [name[]]. Er wird hier mit allen Parametern initialisiert, die diesen Namen tragen, unabhängig davon, ob es sich um eine GET- oder POST-Anfrage handelt, da der Anfragetyp hier nicht angegeben wurde;
Die Ergebnisse lauten wie folgt:
![]() |
- Mit einem POST [1] senden wir die Parameter [2];
- Parameter sind auch in der URL enthalten [3];
- in [4] die vier Parameter mit dem gleichen Namen [name]: [Query-String-Parameter] sind die URL-Parameter, [Formulardaten] sind die gesendeten Parameter;
- in [5] sehen wir, dass die Aktion [/m03] die vier Parameter mit dem Namen [name] abgerufen hat;
4.4. [/m04]: Zuordnung der Parameter der Aktion zu einem Java-Objekt
Betrachten Sie die folgende neue Aktion [/m04]:
// ------ map parameters to a Command Object ---------------
@RequestMapping(value = "/m04", method = RequestMethod.POST)
public Personne m04(Personne personne) {
return person;
}
- Zeile 3: Die Aktion nimmt eine Person des folgenden Typs als Parameter entgegen:
public class Personne {
// identifier
private Integer id;
// name
private String nom;
// age
private int age;
....
// getters and setters
...
}
- Um den Parameter [Person] zu erstellen, ruft Spring MVC [new Person()] auf;
- wenn dann Parameter vorhanden sind, die nach den Feldern [id, name, age] des erstellten Objekts benannt sind, instanziiert es diese mithilfe ihrer Setter;
- Zeile 4: Die Aktion gibt einen Typ [Person] zurück, der daher in eine Zeichenkette serialisiert wird, bevor er an den Client gesendet wird. Wir haben gesehen, dass standardmäßig eine JSON-Serialisierung durchgeführt wird. Der Client sollte daher die JSON-Zeichenkette einer Person erhalten;
Hier ist ein Beispiel:
![]() |
- in [1] die Parameter [id, name, age] zum Erstellen eines [Person]-Objekts;
- in [2] die JSON-Zeichenkette für diese Person;
Was passiert, wenn wir nicht alle Felder für eine Person senden? Probieren wir es aus:
![]() |
- In [2] wurde nur der Parameter [id] initialisiert;
4.5. [/m05]: Elemente von einer URL abrufen
Betrachten Sie die folgende neue Aktion [/m05]:
// ----------------------- retrieve elements from URL ------------------------
@RequestMapping(value = "/m05/{a}/x/{b}", method = RequestMethod.GET)
public Map<String, String> m05(@PathVariable("a") String a, @PathVariable("b") String b) {
Map<String, String> map = new HashMap<String, String>();
map.put("a", a);
map.put("b", b);
return map;
}
- Zeile 2: Die zu verarbeitende URL hat das Format [/m05/{a}/x/{b}], wobei {param} ein URL-Parameter ist;
- Zeile 3: Die URL-Parameterelemente werden mithilfe der Annotation [@PathVariable] abgerufen;
- Zeilen 4–6: Die abgerufenen Elemente [a] und [b] werden in ein Dictionary eingefügt;
- Zeile 7: Die Antwort ist die JSON-Zeichenkette dieses Wörterbuchs;
Die Ergebnisse lauten wie folgt:
![]() |
4.6. [/m06]: Abrufen von URL-Elementen und Parametern
Betrachten Sie die folgende neue Aktion [/m06]:
// -------- retrieve elements from URL and parameters---------------
@RequestMapping(value = "/m06/{a}/x/{b}", method = RequestMethod.GET)
public Map<String, Object> m06(@PathVariable("a") Integer a, @PathVariable("b") Double b, Double c) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("a", a);
map.put("b", b);
map.put("c", c);
return map;
}
- Zeile 3: Wir rufen sowohl die URL-Elemente [Integer a, Double b] als auch einen Parameter (GET oder POST) [Double c] ab;
- Zeilen 4–7: Diese Elemente werden in ein Wörterbuch eingefügt;
- Zeile 8: Dies bildet die Antwort des Clients, der somit die JSON-Zeichenkette aus diesem Wörterbuch erhält;
Hier sind die Ergebnisse:
![]() |
Beachten Sie den Schrägstrich am Ende des Pfads [http://localhost:8080/m06/100/x/200.43/]. Ohne ihn erhalten wir das folgende falsche Ergebnis:
![]() |
4.7. [/m07]: Zugriff auf die gesamte Anfrage
Hier ist die neue [/m07]-Aktion:
// ------ access the HttpServletRequest query ------------------------
@RequestMapping(value = "/m07", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
public String m07(HttpServletRequest request) {
// HTTP headers
Enumeration<String> headerNames = request.getHeaderNames();
StringBuffer buffer = new StringBuffer();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
buffer.append(String.format("%s : %s\n", name, request.getHeader(name)));
}
return buffer.toString();
}
- Zeile 3: Wir weisen Spring MVC an, das Objekt [HttpServletRequest request] zu injizieren, das alle verfügbaren Informationen über die Anfrage kapselt;
- Zeilen 5–10: Wir holen alle HTTP-Header aus der Anfrage ab, um sie zu einer Zeichenkette zusammenzufügen, die wir an den Client senden (Zeile 11);
Die Ergebnisse lauten wie folgt:
![]() |
- in [1] die HTTP-Header der Anfrage;
![]() |
- in [2] die Antwort. Alle HTTP-Header aus der Anfrage sind dort tatsächlich vorhanden.
4.8. [/m08]: Zugriff auf das [Writer]-Objekt
Betrachten Sie die folgende Aktion:
// ----------------------- injection de writer ------------------------
@RequestMapping(value = "/m08", method = RequestMethod.GET)
public void m08(Writer writer) throws IOException {
writer.write("Bonjour le monde !");
}
- Zeile 3: Spring MVC injiziert das Objekt [Writer writer], das das Schreiben in den Antwort-Stream an den Client ermöglicht;
- Zeile 3: Die Aktion gibt den Typ [void] zurück, was bedeutet, dass sie die Antwort an den Client selbst erstellen muss;
- Zeile 4: Hinzufügen von Text zum Antwortstrom an den Client;
Die Ergebnisse lauten wie folgt:
![]() |
- In [2] sehen wir, dass der HTTP-Header [Content-Type] nicht gesendet wurde;
- in [3] die Antwort;
4.9. [/m09]: Zugriff auf einen HTTP-Header
Betrachten Sie die folgende Aktion:
// ----------------------- injection of RequestHeader ------------------------
@RequestMapping(value = "/m09", method = RequestMethod.GET)
public String m09(@RequestHeader("User-Agent") String userAgent) {
return userAgent;
}
- Zeile 3: Die Annotation [@RequestHeader("User-Agent")] ruft den HTTP-Header [User-Agent] ab;
- Zeile 4: Der Text dieses Headers wird zurückgegeben;
Die Ergebnisse lauten wie folgt:
![]() |
- in [2], der HTTP-Header [User-Agent];
![]() |
- in [3] hat die Aktion [/m08] diesen Header korrekt abgerufen;
4.10. [/m10, /m11]: Zugriff auf ein Cookie
Ein Cookie ist im Allgemeinen ein HTTP-Header, den der:
- Server beim ersten Mal an den Client sendet;
- der Client dann systematisch an den Server zurücksendet;
Zunächst erstellen wir eine Aktion, die das Cookie erstellt:
// ----------------------- Cookie creation ------------------------
@RequestMapping(value = "/m10", method = RequestMethod.GET)
public void m10(HttpServletResponse response) {
response.addCookie(new Cookie("cookie1", "remember me"));
}
- Zeile 3: Wir injizieren das Objekt [HttpServletResponse response], um die volle Kontrolle über die Antwort zu haben;
- Zeile 4: Wir erstellen ein Cookie mit dem Schlüssel [cookie1] und dem Wert [remember me] (Hinweis: Akzentzeichen im Wert eines Cookies verursachen Fehler);
- Zeile 3: Die Aktion gibt nichts zurück. Außerdem schreibt sie nichts in den Antworttext. Der Client erhält daher ein leeres Dokument. Die Antwort wird nur verwendet, um den HTTP-Header für ein Cookie hinzuzufügen;
Sehen wir uns die Ergebnisse an:
![]() |
- in [1]: die Anfrage;
- in [2]: Die Antwort ist leer;
- in [3]: das durch die Aktion erstellte Cookie;
Erstellen wir nun eine Aktion, um dieses Cookie abzurufen, das der Browser nun mit jeder Anfrage sendet:
// ----------------------- Cookie injection ------------------------
@RequestMapping(value = "/m11", method = RequestMethod.GET)
public String m10(@CookieValue("cookie1") String cookie1) {
return cookie1;
}
- Zeile 3: Die Annotation [@CookieValue("cookie1")] ruft das Cookie mit dem Schlüssel [cookie1] ab;
- Zeile 4: Dieser Wert wird als Antwort an den Client gesendet;
Schauen wir uns die Ergebnisse an:
![]() |
- In [2] sehen wir, dass der Browser das Cookie zurückgibt;
- in [3] hat die Aktion es erfolgreich abgerufen;
4.11. [/m12]: Zugriff auf den Body eines POST
POST-Parameter werden in der Regel vom HTTP-Header [Content-Type: application/x-www-form-urlencoded] begleitet. Auf die gesamte gesendete Zeichenkette kann zugegriffen werden. Wir erstellen die folgende Aktion:
// ----------- retrieve the body of a POST of type String------------------------
@RequestMapping(value = "/m12", method = RequestMethod.POST)
public String m12(@RequestBody String requestBody) {
return requestBody;
}
- Zeile 3: Mit der Annotation [@RequestBody] können Sie den POST-Body abrufen. Hier gehen wir davon aus, dass er vom Typ [String] ist;
- Zeile 4: Wir geben diesen Body an den Client zurück;
Hier ist ein erstes Beispiel:
![]() |
- in [2] die übermittelten Werte;
- in [3] der HTTP-Header [Content-Type] der Anfrage;
- in [4] die Antwort des Servers;
POST-Parameter haben nicht immer die einfache Form [p1=v1&p2=v2], die wir bisher oft verwendet haben. Betrachten wir einen komplexeren Fall:
![]() |
- in [2-3]: Wir geben die übermittelten Werte in der Form [Schlüssel:Wert] ein;
- in [5] die übermittelte Zeichenkette;
Beim Typ [Content-Type: application/x-www-form-urlencoded] muss die übermittelte Zeichenfolge die Form [p1=v1&p2=v2] haben. Wenn wir etwas übermitteln möchten, verwenden wir den Typ [Content-Type: text/plain]. Hier ein Beispiel:
![]() |
- In [2-3] erstellen wir den HTTP-Header [Content-Type]. Standardmäßig [5] wird dieser anstelle des in [6] definierten verwendet. Das Attribut [charset=utf-8] ist wichtig. Ohne dieses Attribut gehen die Zeichen mit Akzenten in der gesendeten Zeichenfolge verloren;
- in [4] die gesendete Zeichenfolge, die wir in [7] korrekt abrufen;
4.12. [/m13, /m14]: Abrufen von in JSON gesendeten Werten
Es ist möglich, Parameter mit dem HTTP-Header [Content-Type: application/json] zu senden. Wir erstellen die folgende Aktion:
// ----------------------- retrieve the jSON body from a POST
@RequestMapping(value = "/m13", method = RequestMethod.POST, consumes = "application/json")
public String m13(@RequestBody Personne personne) {
return personne.toString();
}
- Zeile 2: [consumes = "application/json"] gibt an, dass die Aktion einen JSON-Body erwartet;
- Zeile 3: [@RequestBody] steht für diesen Body. Diese Annotation wurde einem Objekt vom Typ [Person] zugeordnet. Der JSON-Body wird automatisch in dieses Objekt deserialisiert;
- Zeile 4: Wir verwenden die Methode [Person].toString(), um etwas anderes als die gesendete JSON-Zeichenkette zurückzugeben;
Hier ein Beispiel:
![]() |
- in [2] die gesendete JSON-Zeichenkette;
- in [3] der [Content-Type] der Anfrage;
- in [4] die Antwort des Servers;
Sie können dasselbe auch anders machen:
// ----------------------- retrieve the jSON body from a POST 2 -------------------
@RequestMapping(value = "/m14", method = RequestMethod.POST, consumes = "text/plain")
public String m14(@RequestBody String requestBody) throws JsonParseException, JsonMappingException, IOException {
Personne personne = new ObjectMapper().readValue(requestBody, Personne.class);
return personne.toString();
}
- Zeile 2: Wir haben festgelegt, dass die Methode einen Stream vom Typ [text/plain] erwartet. Spring MVC behandelt den Request-Body dann als Typ [String] (Zeile 3);
- Zeile 4: Die JSON-Zeichenkette wird in ein [Person]-Objekt deserialisiert (siehe Abschnitt 9.7, Seite 542);
Die Ergebnisse lauten wie folgt:
![]() |
- Stellen Sie in [3] sicher, dass Sie [text/plain] verwenden;
4.13. [/m15]: Abrufen der Sitzung
Werfen wir noch einmal einen Blick auf die Ausführungsarchitektur einer Aktion:
![]() |
Die Controller-Klasse wird zu Beginn der Client-Anfrage instanziiert und am Ende der Anfrage zerstört. Daher kann sie nicht zum Speichern von Daten zwischen Anfragen verwendet werden, selbst wenn sie wiederholt aufgerufen wird. Möglicherweise möchten Sie zwei Arten von Daten speichern:
- Daten, die von allen Benutzern der Webanwendung gemeinsam genutzt werden. Dabei handelt es sich in der Regel um schreibgeschützte Daten;
- Daten, die zwischen Anfragen desselben Clients gemeinsam genutzt werden. Diese Daten werden in einem Objekt namens „Session“ gespeichert. Wir bezeichnen dies als Client-Session, um den Speicher des Clients zu kennzeichnen. Alle Anfragen eines Clients haben Zugriff auf diese Session. Sie können Informationen darin speichern und auslesen.
![]() |
Oben zeigen wir die Arten von Speicher, auf die eine Aktion Zugriff hat:
- den Speicher der Anwendung, der größtenteils schreibgeschützte Daten enthält und für alle Benutzer zugänglich ist;
- den Speicher eines bestimmten Benutzers oder die Sitzung, die Lese-/Schreibdaten enthält und für aufeinanderfolgende Anfragen desselben Benutzers zugänglich ist;
- oben nicht dargestellt ist, gibt es einen Anforderungsspeicher oder Anforderungskontext. Die Anforderung eines Benutzers kann von mehreren aufeinanderfolgenden Aktionen verarbeitet werden. Der Anforderungskontext ermöglicht es Aktion 1, Informationen an Aktion 2 weiterzugeben.
Betrachten wir ein erstes Beispiel, das diese verschiedenen Speichertypen veranschaulicht:
// ----------------------- retrieve session ------------------------
@RequestMapping(value = "/m15", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
public String m15(HttpSession session) {
// retrieve the [counter] key object from the session
Object objCompteur = session.getAttribute("compteur");
// convert it to an integer to increment it
int iCompteur = objCompteur == null ? 0 : (Integer) objCompteur;
iCompteur++;
// put it back in the session
session.setAttribute("compteur", iCompteur);
// we return it as the result of action
return String.valueOf(iCompteur);
}
Spring MVC verwaltet die Sitzung des Benutzers in einem Objekt vom Typ [HttpSession].
- Zeile 3: Wir weisen Spring MVC an, das [HttpSession]-Objekt in die Aktionsparameter einzufügen;
- Zeile 5: Wir rufen daraus ein Attribut namens [counter] ab. Eine Sitzung verhält sich wie ein Wörterbuch, eine Sammlung von [Schlüssel, Wert]-Paaren. Wenn der Schlüssel [counter] in der Sitzung nicht vorhanden ist, erhalten wir einen Null-Zeiger;
- Zeile 7: Der mit dem Schlüssel [counter] verknüpfte Wert ist vom Typ [Integer];
- Zeile 8: Erhöhen Sie den Zähler;
- Zeile 10: Aktualisieren des Zählers in der Sitzung;
- Zeile 12: Der Wert des Zählers wird an den Client gesendet;
Wenn [/m15] zum
- erstes Mal, Zeile 12, hat der Zähler den Wert 1;
- beim zweiten Mal, in Zeile 5, wird dieser Wert 1 abgerufen und auf 2 gesetzt;
- ...
Hier ist ein Beispiel für die Ausführung:
![]() |
- In [1] erhalten wir tatsächlich den ersten Wert des Zählers;
- in [2] hat der Server ein Session-Cookie gesendet. Es hat den Schlüssel [JSESSIONID] und einen Wert, der für jeden Benutzer eine eindeutige Zeichenfolge ist. Denken Sie daran, dass der Browser die empfangenen Cookies immer zurücksendet. Wenn wir also die Aktion [/m15] ein zweites Mal anfordern, sendet der Client dieses Cookie zurück, wodurch der Server es erkennen und mit seiner Sitzung verknüpfen kann. Auf diese Weise wird die Sitzung des Benutzers aufrechterhalten;
Betrachten wir nun die zweite Anfrage:
![]() |
- In [3] sehen wir, dass der Client das Session-Cookie sendet. Beachten Sie, dass dieses Session-Cookie in der Antwort des Servers nicht mehr vorhanden ist. Nun ist es der Client, der es sendet, um erkannt zu werden;
- In [4] der zweite Wert des Zählers. Er wurde tatsächlich erhöht;
4.14. [/m16]: Abrufen eines Objekts im [session]-Bereich
Möglicherweise möchten wir alle Daten aus der Sitzung eines Benutzers in ein einziges Objekt packen und nur dieses Objekt in die Sitzung einfügen. Wir werden diesen Ansatz verfolgen. Wir werden den Zähler in das folgende [SessionModel]-Objekt einfügen:
![]() |
package istia.st.sprinmvc.models;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionModel {
private int compteur;
public int getCompteur() {
return compteur;
}
public void setCompteur(int compteur) {
this.compteur = compteur;
}
}
- Zeile 7: Die Annotation [@Component] ist eine Spring-Annotation (Zeile 5), die die Klasse [SessionModel] zu einer Komponente macht, deren Lebenszyklus von Spring verwaltet wird;
- Zeile 8: Die Annotation [@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)] ist ebenfalls eine Spring-Annotation (Zeilen 3–4). Wenn Spring MVC darauf stößt, wird die entsprechende Klasse erstellt und in die Sitzung des Benutzers aufgenommen. Das Attribut [proxyMode = ScopedProxyMode.TARGET_CLASS] ist wichtig. Dank diesem Attribut erstellt Spring MVC eine Instanz pro Benutzer statt einer einzigen Instanz für alle Benutzer (Singleton);
- Zeile 11: der Zähler;
Damit diese neue Spring-Komponente erkannt wird, muss die Anwendungskonfiguration in der Klasse [Application] überprüft werden:
package istia.st.springmvc.main;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan({"istia.st.springmvc.controllers"})
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
- Zeile 9: Nach Spring-Komponenten wird im Paket [istia.st.springmvc.controllers] gesucht. Das reicht nicht mehr aus. Wir ändern diese Zeile wie folgt:
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
Wir haben das Paket hinzugefügt, das die Klasse [SessionModel] enthält.
Nun fügen wir die folgende Aktion hinzu:
@Autowired
private SessionModel session;
// ------ manage a scope object session [Autowired] -----------
@RequestMapping(value = "/m16", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
public String m16() {
session.setCompteur(session.getCompteur() + 1);
return String.valueOf(session.getCompteur());
}
- Zeilen 1–2: Die Spring-Komponente [SessionModel] wird [@Autowired] in den Controller injiziert. Erinnern Sie sich daran, dass ein Spring-Controller ein Singleton ist. Es ist daher paradox, eine Komponente mit einem engeren Gültigkeitsbereich – in diesem Fall dem [Session]-Gültigkeitsbereich – in ihn zu injizieren. Hier kommt die Annotation [@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)] auf der [SessionModel]-Komponente ins Spiel. Jedes Mal, wenn der Controller-Code in Zeile 2 auf das Feld [session] zugreift, wird eine Proxy-Methode ausgeführt, um die Session der Anfrage zurückzugeben, die gerade vom Controller verarbeitet wird;
- Zeile 6: Das [HttpSession]-Objekt wird in den Aktionsparametern nicht mehr benötigt;
- Zeile 7: Der Zähler wird abgerufen und inkrementiert;
- Zeile 8: Sein Wert wird zurückgegeben;
Hier ist ein Beispiel für die Ausführung:
Beim ersten Mal
![]() |
Das zweite Mal
![]() |
Nehmen wir nun einen weiteren Browser, um einen zweiten Benutzer darzustellen. Hier verwenden wir den Opera-Browser:
![]() |
Oben in [1] ruft dieser zweite Benutzer einen Zählerwert von 1 ab. Dies zeigt, dass seine Sitzung und die des ersten Benutzers unterschiedlich sind. Wenn wir uns den Client-Server-Austausch ansehen (auch bei Opera Strg-Umschalt-I), sehen wir in [2], dass dieser zweite Benutzer ein anderes Sitzungscookie hat als der erste Benutzer. Dies gewährleistet die Unabhängigkeit der Sitzungen.
4.15. [/m17]: Abrufen eines Objekts im [Anwendungs-]Bereich
Werfen wir noch einmal einen Blick auf die Ausführungsarchitektur einer Aktion:
![]() |
Wir wissen, wie man die Benutzersitzung erstellt. Nun erstellen wir ein Objekt im [application]-Scope, dessen Inhalt schreibgeschützt und für alle Benutzer zugänglich ist. Wir führen die Klasse [ApplicationModel] ein, die als Objekt im [application]-Scope dienen wird:
![]() |
package istia.st.springmvc.models;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.stereotype.Component;
@Component
public class ApplicationModel {
// meter
private AtomicLong compteur = new AtomicLong(0);
// getters and setters
public AtomicLong getCompteur() {
return compteur;
}
public void setCompteur(AtomicLong compteur) {
this.compteur = compteur;
}
}
- Zeile 5: Die Annotation [@Component] stellt sicher, dass die Klasse [ApplicationModel] eine von Spring verwaltete Komponente ist. Die Standard-Natur von Spring-Komponenten ist der Typ [singleton]: Die Komponente wird als einzelne Instanz erstellt, wenn der Spring-Container instanziiert wird, d. h. in der Regel beim Start der Anwendung. Wir können diesen Lebenszyklus nutzen, um Konfigurationsinformationen im Singleton zu speichern, auf die alle Benutzer zugreifen können;
- Zeile 11: Ein Zähler vom Typ [AtomicLong]. Dieser Typ verfügt über eine atomare Methode namens [incrementAndGet]. Das bedeutet, dass für einen Thread, der diese Methode ausführt, garantiert ist, dass kein anderer Thread den Wert des Zählers (Get) zwischen seinem eigenen Lesevorgang (Get) und der Inkrementierung (increment) durch den ersten Thread liest, was zu Fehlern führen würde, da zwei Threads denselben Zählerwert lesen würden und der Zähler statt um zwei nur um eins inkrementiert würde;
Wir erstellen die folgende neue Aktion [/m17]:
@Autowired
private ApplicationModel application;
// ----- manage an application scope object [Autowired] ------------------------
@RequestMapping(value = "/m17", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
public String m17() {
return String.valueOf(application.getCompteur().incrementAndGet());
}
- Zeilen 1–2: Wir injizieren die Komponente [ApplicationModel] in den Controller. Es handelt sich um ein Singleton. Daher verfügt jeder Benutzer über eine Referenz auf dasselbe Objekt;
- Zeile 7: Wir geben den Zähler des [application]-Scopes zurück, nachdem wir ihn erhöht haben;
Hier sind zwei Beispiele, eines mit Chrome und das andere mit Opera:
![]() | ![]() |
Oben sehen wir, dass beide Browser mit demselben Zähler gearbeitet haben, was bei der Sitzung nicht der Fall war. Diese beiden Browser repräsentieren zwei verschiedene Benutzer, die beide Zugriff auf die Daten im [application]-Bereich haben. Generell sollten wir es vermeiden, Lese-/Schreibinformationen in Objekte des [application]-Bereichs zu platzieren, wie es oben mit dem Zähler geschehen ist. Tatsächlich greifen die Ausführungsthreads verschiedener Benutzer gleichzeitig auf Daten im [application]-Bereich zu. Wenn es beschreibbare Informationen gibt, muss der Schreibzugriff synchronisiert werden, wie oben beim Typ [AtomicLong] geschehen ist. Gleichzeitiger Zugriff ist eine Quelle für Programmierfehler. Daher ist es vorzuziehen, nur schreibgeschützte Informationen in Objekte des [application]-Bereichs zu platzieren.
4.16. [/m18]: Abrufen eines Objekts im [session]-Bereich mit [@SessionAttributes]
Es gibt eine weitere Möglichkeit, Informationen im [session]-Bereich abzurufen. Wir werden das folgende Objekt in die Sitzung einfügen:
package istia.st.springmvc.models;
public class Container {
// the meter
public int compteur=10;
// getters and setters
public int getCompteur() {
return compteur;
}
public void setCompteur(int compteur) {
this.compteur = compteur;
}
}
Wir werden dieses Objekt mit den folgenden beiden Aktionen verwenden:
// use of [@SessionAttribute] ----------------------
@RequestMapping(value = "/m18", method = RequestMethod.GET)
public void m18(HttpSession session) {
// here we put the key [container] in the session
session.setAttribute("container", new Container());
}
// use of [@ModelAttribute] ----------------------
// the session's [container] key will be injected here
@RequestMapping(value = "/m19", method = RequestMethod.GET)
public String m19(@ModelAttribute("container") Container container) {
container.setCompteur(1 + container.getCompteur());
return String.valueOf(container.getCompteur());
}
- Zeilen 3–6: Die Aktion [/m18] gibt kein Ergebnis zurück. Sie dient ausschließlich dazu, ein Objekt in der Sitzung mit dem Schlüssel [container] zu erstellen;
- Zeile 11: In der Aktion [/m19] wird die Annotation [@ModelAttribute] verwendet. Das Verhalten dieser Annotation ist recht komplex. Der Parameter [container] dieser Annotation kann sich auf verschiedene Dinge beziehen, insbesondere auf ein Session-Objekt. Damit dies funktioniert, muss das Objekt mit einer [@SessionAttributes]-Annotation in der Klasse selbst deklariert worden sein:
@RestController
@SessionAttributes({"container"})
public class ActionModelController {
- Zeile 2 oben legt den Schlüssel [container] als Teil der Sitzungsattribute fest;
Zusammenfassend lässt sich sagen:
- In [/m18] wird der Schlüssel [container] in die Sitzung gesetzt;
- die Annotation [@SessionAttributes({"container"})] stellt sicher, dass dieser Schlüssel in einen mit [@ModelAttribute("container")] annotierten Parameter injiziert werden kann;
- im folgenden Ausführungsbeispiel nicht sichtbar, aber mit [@ModelAttribute] annotierte Informationen sind automatisch Teil des M-Modells, das an die V-Ansicht übergeben wird;
Hier ist ein Ausführungsbeispiel. Zunächst legen wir den Schlüssel [container] mit der Aktion [/m18] [1] in die Sitzung. Als Nächstes rufen wir die Aktion [/m19] zweimal auf, um zu sehen, wie sich der Zähler erhöht.
![]() |
4.17. [/m20-/m23]: Einfügen von Daten mit [@ModelAttribute]
Betrachten Sie die folgende neue Aktion:
// the p attribute will be included in all [Model] view models ----------------
@ModelAttribute("p")
public Personne getPersonne() {
return new Personne(7,"abcd", 14);
}
// ---------------instanciation of @ModelAttribute --------------------------
// will be injected if it is in the
// will be injected if the controller has defined a method for this attribute
// can come from the URL fields if a String --> type converter exists for the attribute
// otherwise is built with the default constructor
// then the model attributes are initialized with the parameters of GET or POST
// the final result will be part of the model produced by the action
// the p attribute is injected into the arguments------------------------
@RequestMapping(value = "/m20", method = RequestMethod.GET)
public Personne m20(@ModelAttribute("p") Personne personne) {
return personne;
}
- Zeilen 2–5: Definieren ein Modellattribut namens [p]. Dies ist das Modell M einer Ansicht V, das in Spring MVC durch einen [Model]-Typ dargestellt wird. Ein Modell verhält sich wie ein Wörterbuch aus [Schlüssel, Wert]-Paaren. Hier ist der Schlüssel [p] mit dem [Person]-Objekt verknüpft, das von der Methode [getPerson] erstellt wurde. Der Methodenname kann beliebig sein;
- Zeile 17: Das Modellattribut mit dem Schlüssel [p] wird in die Parameter der Aktion injiziert. Diese Injektion folgt den Regeln in den Zeilen 8–12. Hier handelt es sich um den in Zeile 9 definierten Fall. Daher ist in Zeile 17 der Parameter [Person person] das Objekt [Person(7, 'abcd', 14)];
- Zeile 18: Das Objekt [person] wird zur Validierung zurückgegeben. Es wird in JSON serialisiert, bevor es an den Client gesendet wird.
Hier ist ein Beispiel:
![]() |
Betrachten wir nun die folgende Aktion:
// --------- attribute p is automatically included in the M model of view V
@RequestMapping(value = "/m21", method = RequestMethod.GET)
public String m21(Model model) {
return model.toString();
}
Eine Aktion, die eine Ansicht V anzeigen möchte, muss ihr Modell M erstellen. Spring MVC verwaltet dies mithilfe eines [Model]-Typs, der in die Parameter der Aktion injiziert werden kann. Zu Beginn ist dieses Modell leer oder enthält Informationen, die mit der Annotation [@ModelAttribute] gekennzeichnet sind. Die Aktion kann dieses Modell vor der Übergabe an eine Ansicht ergänzen oder auch nicht.
- Zeile 3: Injektion des Modells M;
- Zeile 4: Wir wollen sehen, was darin enthalten ist. Wir serialisieren es in eine Zeichenkette, um es an den Client zu senden. Hier wird die Methode [Person.toString] verwendet. Sie muss daher vorhanden sein;
Hier ist eine Ausführung:
![]() |
Oben sehen wir, dass die Anweisungen:
@ModelAttribute("p")
public Personne getPersonne() {
return new Personne(7,"abcd", 14);
}
haben einen Eintrag [p, Person(7, "abcd", 14)] im Modell erstellt. Dies ist immer der Fall.
Betrachten wir nun den folgenden Fall:
// sinon est construit avec le constructeur par défaut
// ensuite les attributs du modèle sont initialisés avec les paramètres du GET ou du POST
mit der folgenden Aktion:
// --------- model attribute [param1] is part of the model but is not initialized
@RequestMapping(value = "/m22", method = RequestMethod.GET)
public String m22(@ModelAttribute("param1") String p1, Model model) {
return model.toString();
}
- Zeile 3: Das Schlüsselattribut [param1] des Modells existiert nicht. In diesem Fall muss der zugehörige Typ über einen Standardkonstruktor verfügen. Dies ist hier beim Typ [String] der Fall, aber wir können nicht [@ModelAttribute("param1") Integer p1] schreiben, da die Klasse [Integer] keinen Standardkonstruktor hat;
- Zeile 4: Wir geben das Modell zurück, um zu prüfen, ob das Schlüsselattribut [param1] Teil davon ist;
Hier ist ein Ausführungsbeispiel:
![]() |
Das Modellattribut [param1] ist zwar im Modell vorhanden, aber die Methode [toString] des zugehörigen Werts liefert keine Informationen über diesen Wert.
Betrachten wir nun die folgende Aktion, bei der wir Informationen explizit in das Modell einfügen:
// --------- the model attribute [param2] is explicitly set in the model
@RequestMapping(value = "/m23", method = RequestMethod.GET)
public String m23(String p2, Model model) {
model.addAttribute("param2",p2);
return model.toString();
}
- Zeile 4: Der in Zeile 3 abgerufene Wert [p2] wird dem Modell unter dem Schlüssel [param2] hinzugefügt:
Hier ein Beispiel für die Ausführung:
![]() |
Die Regeln ändern sich, wenn der Aktionsparameter ein Objekt ist. Hier ist ein erstes Beispiel:
// ------ the template attribute [unePersonne] is automatically set in the template
@RequestMapping(value = "/m23b", method = RequestMethod.GET)
public String m23b(@ModelAttribute("unePersonne") Personne p1, Model model) {
return model.toString();
}
Die Aktion verändert das ihr übergebene Modell nicht. Das Ergebnis lautet wie folgt:
![]() |
Wir sehen, dass die Annotation [@ModelAttribute("unePersonne") Personne p1] die Person [p1] zum Modell hinzugefügt hat, verknüpft mit dem Schlüssel [unePersonne].
Betrachten wir nun die folgende Aktion:
// --------- person p1 is automatically included in the model
// -------- with class name as key, 1st character lowercase
@RequestMapping(value = "/m23c", method = RequestMethod.GET)
public String m23c(Personne p1, Model model) {
return model.toString();
}
- Zeile 4: Wir haben die Annotation [@ModelAttribute] nicht hinzugefügt;
Das Ergebnis lautet wie folgt:
![]() |
Wir sehen, dass durch das Vorhandensein des Parameters [Person p1] die Person [p1] in das Modell eingefügt wurde, verknüpft mit dem Schlüssel [person], der dem Namen der Klasse [Person] entspricht, wobei das erste Zeichen klein geschrieben ist.
4.18. [/m24]: Validierung des Aktionsmodells
Betrachten Sie das folgende Aktionsmodell [ActionModel01]:
![]() |
package istia.st.springmvc.models;
import javax.validation.constraints.NotNull;
public class ActionModel01 {
// data
@NotNull
private Integer a;
@NotNull
private Double b;
// getters and setters
...
}
- Zeilen 8 und 9: Die Annotation [@NotNull] ist eine Validierungsbeschränkung, die festlegt, dass die mit dieser Annotation versehenen Daten nicht null sein dürfen;
Betrachten wir nun die folgende Aktion:
// ----------------------- model validation ------------------------
@RequestMapping(value = "/m24", method = RequestMethod.GET)
public Map<String, Object> m24(@Valid ActionModel01 data, BindingResult result) {
Map<String, Object> map = new HashMap<String, Object>();
// mistakes?
if (result.hasErrors()) {
StringBuffer buffer = new StringBuffer();
// browsing the error list
for (FieldError error : result.getFieldErrors()) {
buffer.append(String.format("[%s:%s:%s:%s:%s]", error.getField(), error.getRejectedValue(),
String.join(" - ", error.getCodes()), error.getCode(),error.getDefaultMessage()));
}
map.put("errors", buffer.toString());
} else {
// no errors
Map<String, Object> mapData = new HashMap<String, Object>();
mapData.put("a", data.getA());
mapData.put("b", data.getB());
map.put("data", mapData);
}
return map;
}
- Zeile 3: Ein [ActionModel01]-Objekt wird instanziiert und seine Felder [a, b] werden mit den gleichnamigen Parametern initialisiert. Die Annotation [@Valid] gibt an, dass Validierungsbeschränkungen überprüft werden müssen. Die Ergebnisse dieser Validierung werden im Parameter [BindingResult] (zweiter Parameter) gespeichert. Folgende Validierungen finden statt:
- Aufgrund der [@NotNull]-Annotationen müssen die Parameter [a] und [b] vorhanden sein;
- Aufgrund des Typs [Integer a] muss der Parameter [a], der von Natur aus vom Typ [String] ist, in den Typ [Integer] konvertierbar sein;
- Aufgrund des Typs [Double b] muss der Parameter [b], der von Natur aus vom Typ [String] ist, in den Typ [Double] konvertierbar sein;
Mit der Annotation [@Valid] werden Validierungsfehler im Parameter [BindingResult result] gemeldet. Ohne die Annotation [@Valid] führen Validierungsfehler zum Absturz der Aktion, und der Server sendet dem Client eine HTTP-Antwort mit dem Status 500 (Internal Server Error).
- Zeile 3: Das Ergebnis der Aktion ist vom Typ [Map]. Die JSON-Zeichenkette dieses Ergebnisses wird an den Client gesendet. Wir erstellen zwei Arten von Wörterbüchern:
- im Falle eines Fehlers ein Wörterbuch mit dem Eintrag ['errors', value], wobei [value] eine Zeichenkette ist, die alle Fehler beschreibt (Zeile 13);
- im Erfolgsfall ein Wörterbuch mit dem Eintrag ['data', value], wobei [value] selbst ein Wörterbuch mit zwei Einträgen ist: ['a', value], ['b', value] (Zeile 19);
- Zeilen 9–12: Für jeden erkannten Fehler [error] wird die Zeichenkette [error.getField(), error.getRejectedValue(), error.Codes, error.getDefaultMessage()] erstellt:
- das erste Element ist das fehlerhafte Feld, [a] oder [b],
- das zweite Element ist der abgelehnte Wert, zum Beispiel [x],
- das dritte Element ist eine Liste von Fehlercodes. Wir werden uns ihre Funktionen in Kürze ansehen;
- das vierte Element ist der Fehlercode. Er ist Teil der vorherigen Liste;
- das letzte Element ist die Standardfehlermeldung. Tatsächlich kann es mehrere Fehlermeldungen geben;
Hier sind einige Beispiele für die Ausführung:
![]() |
Oben sehen wir, dass:
- die Zuweisung von „x“ zum Feld [ActionModel01.a] fehlgeschlagen ist und die Fehlermeldung erklärt, warum;
- die Zuweisung von „y“ zum Feld [ActionModel01.b] fehlgeschlagen ist und die Fehlermeldung erklärt, warum;
Beachten Sie die Fehlercodes für das Feld [a]: [typeMismatch.actionModel01.a – typeMismatch.a – typeMismatch.java.lang.Integer – typeMismatch]. Wir werden auf diese Fehlercodes zurückkommen, wenn es darum geht, die Fehlermeldung anzupassen. Beachten Sie, dass der Fehlercode [typeMismatch] lautet.
Ein weiteres Beispiel:
![]() |
Hier wurden die Parameter [a] und [b] nicht übergeben. Die [@NotNull]-Validatoren im Aktionsmodell [ActionModel01] haben dann ihre Aufgabe erfüllt;
Schließlich die korrekten Werte:
![]() |
4.19. [m/24]: Fehlermeldungen anpassen
Kehren wir zu einem Screenshot aus dem vorherigen Beispiel zurück:
![]() |
Oben sehen wir die Standardfehlermeldungen. Es ist klar, dass wir diese in einer echten Anwendung nicht beibehalten können. Es ist möglich, diese Fehlermeldungen anzupassen. Dazu verwenden wir die Fehlercodes. Oben sehen wir, dass der Fehler für das Feld [a] die folgenden Codes hat: [typeMismatch.actionModel01.a – typeMismatch.a – typeMismatch.java.lang.Integer – typeMismatch]. Diese Fehlercodes reichen vom spezifischsten zum am wenigsten spezifischen:
- [typeMismatch.actionModel01.a]: Typfehler im Feld [a] vom Typ [ActionModel01];
- [typeMismatch.a]: Typfehler in einem Feld namens [a];
- [typeMismatch.java.lang.Integer]: Typfehler bei einem Integer-Typ;
- [typeMismatch]: Typfehler;
Wir stellen außerdem fest, dass der über [error.getCode()] abgerufene Fehlercode für das Feld [a] [typeMismatch] lautet (siehe Screenshot oben).
Wir werden die Fehlermeldungen in einer Eigenschaftsdatei ablegen:
![]() |
Die oben genannte Datei [messages.properties] sieht wie folgt aus:
NotNull=Le champ ne peut être vide
typeMismatch=Format invalide
typeMismatch.model01.a=Le paramètre [a] doit être entier
Jede Zeile hat das folgende Format:
Hier ist der Schlüssel ein Fehlercode und die Nachricht die zu diesem Code gehörende Fehlermeldung.
Sehen wir uns die Fehlercodes für die beiden Felder an:
- [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch], wenn der Parameter [a] ungültig ist;
- [typeMismatch.actionModel01.b - typeMismatch.b - typeMismatch.java.lang.Double - typeMismatch:typeMismatch ], wenn der Parameter [b] ungültig ist;
- [NotNull.actionModel01.a - NotNull.a - NotNull.java.lang.Integer - NotNull], wenn der Parameter [a] fehlt;
- [NotNull.actionModel01.b - NotNull.b - NotNull.java.lang.Double - NotNull], wenn der Parameter [b] fehlt;
Die Datei [messages.properties] muss für alle möglichen Fehlerfälle eine Fehlermeldung enthalten. Falls
- die Parameter [a] und [b] fehlen, wird der Code [NotNull] verwendet;
- wenn der Parameter [a] falsch ist, haben wir Meldungen für zwei Codes [typeMismatch.actionModel01.a, typeMismatch] eingefügt. Wir werden sehen, welcher davon verwendet wird;
- Wenn der Parameter [b] falsch ist, wird der Code [typeMismatch] verwendet;
Um die Datei [messages.properties] zu verwenden, müssen Sie Spring konfigurieren:
![]() |
Wir entfernen die Konfigurationsanmerkungen aus der [Application]-Klasse:
package istia.st.springmvc.main;
import org.springframework.boot.SpringApplication;
public class Application {
public static void main(String[] args) {
SpringApplication.run(Config.class, args);
}
}
- Zeile 8: Die Spring Boot-Anwendung wird gestartet. Der erste Parameter der statischen Methode [SpringApplication.run] ist die Klasse, die nun die Anwendung konfiguriert;
Die [Config]-Klasse sieht wie folgt aus:
package istia.st.springmvc.main;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("i18n/messages");
return messageSource;
}
}
- Zeilen 11–13: Die Konfigurationsanmerkungen, die zuvor in der Klasse [Application] standen, befinden sich nun hier;
- Zeile 14: Um eine Spring-MVC-Anwendung zu konfigurieren, müssen Sie die Klasse [WebMvcConfigurerAdapter] erweitern;
- Zeile 15: Die Annotation [@Bean] definiert eine Spring-Komponente, ein Singleton;
- Zeile 16: Wir definieren eine Bean namens [messageSource] (der Methodenname). Diese Bean wird zur Definition der Nachrichtendateien der Anwendung verwendet und muss diesen Namen tragen;
- Zeilen 17–19: Teilen Sie Spring mit, dass sich die Meldungsdatei:
- sich im Ordner [i18n] innerhalb des Klassenpfads des Projekts befindet (Zeile 18),
- den Namen [messages.properties] trägt (Zeile 18). Tatsächlich ist der Begriff [messages] der Stamm der Namen der Meldungsdateien und nicht der Name selbst. Wir werden sehen, dass es im Zusammenhang mit der Internationalisierung mehrere Meldungsdateien geben kann, eine pro unterstützter Sprachumgebung. So könnten wir beispielsweise [messages_fr.properties] für Französisch und [messages_en.properties] für Englisch haben. Die an den Stamm [messages] angehängten Suffixe sind standardisiert. Man kann nicht einfach irgendetwas verwenden;
Im STS-Projekt muss der Ordner [i18n] im Ordner „resources“ abgelegt werden, da er zum Klassenpfad des Projekts hinzugefügt wird:
![]() |
Um diese Datei zu verwenden, erstellen wir die folgende neue Aktion:
// model validation, error message handling ------------------------
@RequestMapping(value = "/m25", method = RequestMethod.GET)
public Map<String, Object> m25(@Valid ActionModel01 data, BindingResult result, HttpServletRequest request)
throws Exception {
// results dictionary
Map<String, Object> map = new HashMap<String, Object>();
// spring application context
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
// local
Locale locale = RequestContextUtils.getLocale(request);
// mistakes?
if (result.hasErrors()) {
StringBuffer buffer = new StringBuffer();
for (FieldError error : result.getFieldErrors()) {
// search for error msg using error codes
// the msg is searched in the message files
// error codes in table format
String[] codes = error.getCodes();
// in chain form
String listCodes = String.join(" - ", codes);
// research
String msg = null;
int i = 0;
while (msg == null && i < codes.length) {
try {
msg = ctx.getMessage(codes[i], null, locale);
} catch (Exception e) {
}
i++;
}
// have we found?
if (msg == null) {
throw new Exception(String.format("Indiquez un message pour l'un des codes [%s]", listCodes));
}
// found - add error msg to error msg chain
buffer.append(String.format("[%s:%s:%s:%s]", locale.toString(), error.getField(), error.getRejectedValue(),
String.join(" - ", msg)));
}
map.put("errors", buffer.toString());
} else {
// ok
Map<String, Object> mapData = new HashMap<String, Object>();
mapData.put("a", data.getA());
mapData.put("b", data.getB());
map.put("data", mapData);
}
return map;
}
Dieser Code ähnelt dem der Aktion [/m24]. Hier sind die Unterschiede:
- Zeile 3: Wir injizieren die Anfrage [HttpServletRequest request] in die Parameter der Aktion. Wir werden sie benötigen;
- Zeilen 7–8: Wir rufen den Spring-Kontext ab. Dieser Kontext enthält alle Spring-Beans in der Anwendung. Er bietet auch Zugriff auf die Nachrichtendateien;
- Zeile 10: Wir rufen die Anwendungslokalisierung ab. Dieser Begriff wird weiter unten näher erläutert;
- Zeilen 15–31: Für jeden Fehler suchen wir nach einer Meldung, die einem dieser Fehlercodes entspricht. Die Suche erfolgt in der Reihenfolge der Codes, die in [error.getCodes()] zu finden sind. Sobald eine Meldung gefunden wird, brechen wir ab;
- Zeile 26: So rufen Sie eine Meldung aus [messages.properties] ab:
- Der erste Parameter ist der in [messages.properties] gesuchte Code,
- der zweite ist ein Array von Parametern, da Meldungen manchmal parametrisiert sind. Dies ist hier nicht der Fall,
- der dritte ist die verwendete Locale (erhalten in Zeile 10). Die Locale gibt die verwendete Sprache an, [fr_FR] für Französisch (Frankreich), [en_US] für Englisch (USA). Die Meldung wird in messages_[locale].properties gesucht, also beispielsweise in [messages_fr_FR.properties]. Wenn diese Datei nicht existiert, wird die Meldung in [messages_fr.properties] gesucht. Wenn diese Datei nicht existiert, wird die Meldung in [messages.properties] gesucht. Es ist dieser letzte Fall, der für uns funktioniert;
- Zeilen 25–29: Etwas unerwartet wird bei der Suche nach einem nicht vorhandenen Code in einer Meldungsdatei eine Ausnahme ausgelöst statt eines Null-Zeigers;
- Zeilen 33–35: Wir behandeln den Fall, in dem keine Fehlermeldung vorhanden ist;
- Zeilen 37–38: Wir erstellen die Fehlerzeichenfolge. Darin fügen wir die Locale und die gefundene Fehlermeldung ein;
Hier sind einige Beispiele für die Ausführung:
![]() |
Wir sehen, dass:
- die Anwendungssprache [fr_FR] ist. Dies ist ein Standardwert, da wir noch nichts unternommen haben, um sie zu initialisieren;
- die für beide Felder verwendete Meldung lautet wie folgt:
NotNull=Le champ ne peut être vide
Ein weiteres Beispiel:
![]() |
Wir sehen, dass:
- Die Fehlermeldung für den Parameter [a] lautet wie folgt:
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier
- Die Fehlermeldung für den Parameter [b] lautet wie folgt:
typeMismatch=Format invalide
Warum gibt es zwei verschiedene Meldungen? Für den Parameter [a] gab es zwei mögliche Meldungen:
typeMismatch=Format invalide
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier
Die Fehlercodes wurden in der Reihenfolge geprüft, die durch das Array [error.getCodes()] vorgegeben ist. Es zeigt sich, dass diese Reihenfolge vom spezifischsten zum allgemeinsten Code verläuft. Deshalb wurde der Code [typeMismatch.model01.a] zuerst gefunden.
4.20. [/m25]: Internationalisierung einer Spring-MVC-Anwendung
Da wir nun wissen, wie man Fehlermeldungen auf Französisch anpasst, möchten wir sie auch auf Englisch bereitstellen, was uns zur Internationalisierung einer Spring MVC-Anwendung führt. Dazu erweitern wir die Konfigurationsklasse [Config] wie folgt:
package istia.st.springmvc.main;
import java.util.Locale;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("i18n/messages");
return messageSource;
}
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("lang");
return localeChangeInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
@Bean
public CookieLocaleResolver localeResolver() {
CookieLocaleResolver localeResolver = new CookieLocaleResolver();
localeResolver.setCookieName("lang");
localeResolver.setDefaultLocale(new Locale("fr"));
return localeResolver;
}
}
- Zeilen 28–32: Wir erstellen einen Request-Interceptor. Ein Request-Interceptor erweitert die Schnittstelle [HandlerInterceptor]. Eine solche Klasse überprüft die eingehende Anfrage, bevor sie von einer Aktion verarbeitet wird. Hier sucht der [localeChangeInterceptor] in der eingehenden GET- oder POST-Anfrage nach einem Parameter namens [lang] und ändert die Spracheinstellung der Anwendung basierend auf diesem Parameter. Wenn der Parameter also [lang=en_US] lautet, wird die Spracheinstellung der Anwendung auf US-Englisch gesetzt;
- Zeilen 34–37: Wir überschreiben die Methode [WebMvcConfigurerAdapter.addInterceptors], um den oben genannten Interceptor hinzuzufügen;
- Zeilen 39–45: dienen dazu, zu konfigurieren, wie die Locale in einem Cookie gekapselt wird. Wir wissen, dass ein Cookie als Benutzerspeicher dienen kann, da der Client-Browser es systematisch an den Server zurücksendet. Der vorherige Interceptor [localeChangeInterceptor] erstellt ein Cookie, das die Locale kapselt. In Zeile 42 erhält dieses Cookie den Namen [lang]. Das Cookie wird auch verwendet, um die Locale zu ändern;
- Zeile 43: legt fest, dass die Locale [fr] ist, falls das [lang]-Cookie fehlt;
Zusammenfassend lässt sich sagen, dass die Locale für eine Anfrage auf zwei Arten festgelegt werden kann:
- durch Übergabe eines Parameters namens [lang];
- durch das Senden eines Cookies namens [lang]. Dieses Cookie wird automatisch erstellt, nachdem die vorherige Methode ausgeführt wurde;
Um diese Spracheinstellung zu verwenden, erstellen wir Meldungsdateien für die Sprachen [fr] und [en]:
![]() |
Die Datei [messages_fr.properties] sieht wie folgt aus:
NotNull=Le champ ne peut être vide
typeMismatch=Format invalide
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier
Die Datei [messages_en.properties] sieht wie folgt aus:
NotNull=The field can't be empty
typeMismatch=Invalid format
typeMismatch.actionModel01.a=Parameter [a] must be an integer
Die Datei [messages.properties] ist eine Kopie der Datei [messages_en.properties]. Beachten Sie, dass die Datei [messages.properties] verwendet wird, wenn keine Datei gefunden wird, die der Spracheinstellung der Anfrage entspricht. In unserem Fall wird, wenn der Benutzer einen [lang=en]-Parameter sendet, die Datei [messages.properties] verwendet, da die Datei [messages_en.properties] nicht existiert. Der Benutzer sieht daher Meldungen auf Englisch.
Probieren wir es aus. Überprüfen Sie zunächst in den Entwicklertools von Chrome (Strg-Umschalt-I) Ihre Cookies:
![]() |
Wenn Sie ein Cookie mit dem Namen [lang] haben, löschen Sie es. Navigieren Sie dann in Chrome zur URL [http://localhost:8080/m25]:
![]() |
Der Browser hat die folgenden HTTP-Header gesendet:
Wir sehen, dass in diesen Headern kein [lang]-Cookie vorhanden ist. In diesem Fall verwendet unser Code die Locale [fr]. Dies ist im Screenshot zu sehen. Versuchen wir es mit einem anderen Fall:
![]() |
- In [1] haben wir den Parameter [lang=en] übergeben, um die Locale auf [en] zu setzen;
- in [2] sehen wir die neue Locale;
- in [3] ist die Meldung nun auf Englisch;
Sehen wir uns nun den HTTP-Datenaustausch an:
![]() |
Wie oben zu sehen ist, hat der Server ein [lang]-Cookie zurückgesendet. Dies hat eine wichtige Konsequenz: Die Spracheinstellung für die nächste Anfrage wird aufgrund des [lang]-Cookies, das vom Browser zurückgesendet wird, wieder [en] sein. Wir sollten die Meldungen daher auf Englisch belassen. Lassen Sie uns dies überprüfen:
![]() |
Oben sehen wir, dass die Spracheinstellung [en] geblieben ist. Aufgrund des Cookies, das der Browser systematisch sendet, bleibt dies so, bis der Benutzer es ändert, indem er den [lang]-Parameter wie folgt sendet:
![]() |
4.21. [/m26]: Einfügen der Locale in die Aktionsvorlage
Im vorherigen Beispiel haben wir eine Möglichkeit gesehen, die Locale aus der Anfrage abzurufen:
@RequestMapping(value = "/m25", method = RequestMethod.GET)
public Map<String, Object> m25(@Valid ActionModel01 data, BindingResult result, HttpServletRequest request)
throws Exception {
...
// local
Locale locale = RequestContextUtils.getLocale(request);
// mistakes?
Die Locale kann direkt in die Aktionsparameter eingefügt werden. Hier ein Beispiel:
@RequestMapping(value = "/m26", method = RequestMethod.GET)
public String m26(Locale locale) {
return String.format("locale=%s", locale.toString());
}
![]() | ![]() |
![]() |
Wie oben gezeigt, wird die Gültigkeit der angeforderten Locale nicht überprüft. Die nachfolgende Anfrage des Browsers löst jedoch eine serverseitige Ausnahme aus, da das empfangene Locale-Cookie falsch ist.
4.22. [/m27]: Validierung eines Modells mit Hibernate Validator
Betrachten Sie die folgende neue Aktion:
//model validation with Hibernate Validator ------------------------
@RequestMapping(value = "/m27", method = RequestMethod.POST)
public Map<String, Object> m27(@Valid ActionModel02 data, BindingResult result) {
Map<String, Object> map = new HashMap<String, Object>();
// mistakes?
if (result.hasErrors()) {
// browsing the error list
for (FieldError error : result.getFieldErrors()) {
map.put(error.getField(),
String.format("[message=%s, codes=%s]", error.getDefaultMessage(), String.join("|", error.getCodes())));
}
} else {
// no errors
map.put("data", data);
}
return map;
}
Hier sehen wir einen Code, den wir bereits mehrmals gesehen haben:
- Zeile 3: Die Aktion [/m27] wird über einen POST-Request aufgerufen;
- Zeilen 8–11: Jeder Fehler wird durch [Feld, Meldung] identifiziert, wobei:
- Feld: das Feld, in dem der Fehler auftritt,
- message: die zugehörige Fehlermeldung und die Liste der Fehlercodes;
- Zeile 14: Wenn keine Fehler vorliegen, wird die JSON-Zeichenkette der gesendeten Werte zurückgegeben;
Zeile 3: Es wird das folgende Aktionsmodell [ActionModel02] verwendet:
![]() |
package istia.st.springmvc.models;
import java.util.Date;
import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.URL;
public class ActionModel02 {
@NotNull(message = "La donnée est obligatoire")
@AssertFalse(message = "Seule la valeur [false] est acceptée")
private Boolean assertFalse;
@NotNull(message = "La donnée est obligatoire")
@AssertTrue(message = "Seule la valeur [true] est acceptée")
private Boolean assertTrue;
@NotNull(message = "La donnée est obligatoire")
@Future(message = "Il faut une date postérieure à aujourd'hui")
private Date dateInFuture;
@NotNull(message = "La donnée est obligatoire")
@Past(message = "Il faut une date antérieure à aujourd'hui")
private Date dateInPast;
@NotNull(message = "La donnée est obligatoire")
@Max(value = 100, message = "Maximum 100")
private Integer intMax100;
@NotNull(message = "La donnée est obligatoire")
@Min(value = 10, message = "Minimum 10")
private Integer intMin10;
@NotNull(message = "La donnée est obligatoire")
@NotBlank(message = "La chaîne doit être non blanche")
private String strNotBlank;
@NotNull(message = "La donnée est obligatoire")
@Size(min = 4, max = 6, message = "La chaîne doit avoir entre 4 et 6 caractères")
private String strBetween4and6;
@NotNull(message = "La donnée est obligatoire")
@Pattern(regexp = "^\\d{2}:\\d{2}:\\d{2}$", message = "Le format doit être hh:mm:ss")
private String hhmmss;
@NotNull(message = "La donnée est obligatoire")
@Email(message = "Adresse invalide")
private String email;
@NotNull(message = "La donnée est obligatoire")
@Length(max = 4, min = 4, message = "La chaîne doit avoir 4 caractères exactement")
private String str4;
@Range(min = 10, max = 14, message = "La valeur doit être dans l'intervalle [10,14]")
@NotNull(message = "La donnée est obligatoire")
private Integer int1014;
@URL(message = "URL invalide")
private String url;
// getters and setters
...
}
Die Klasse verwendet Validierungsbeschränkungen aus zwei Paketen:
- [javax.validation.constraints] in den Zeilen 5–13;
- [org.hibernate.validator.constraints] in den Zeilen 15–19;
Die Maven-Abhängigkeiten für diese beiden Pakete sind im Projekt enthalten:
![]() |
Hier verwenden wir keine internationalisierten Meldungen, sondern Meldungen, die innerhalb der Einschränkung mithilfe des Attributs [message] definiert wurden. Um diese Aktion zu testen, verwenden wir [Advanced Rest Client]:
![]() |
- in [1-2] die POST-Anfrage;
- in [3] den zu verwendenden HTTP-Header [Content-Type];
- in [4] können Sie über den Link [Neuen Wert hinzufügen] ein [Parameter, Wert]-Paar hinzufügen;
- in [5] geben Sie ein Feld aus [ActionModel02] ein, hier das Feld [assertFalse]:
@NotNull(message = "La donnée est obligatoire")
@AssertFalse(message = "Seule la valeur [false] est acceptée")
private Boolean assertFalse;
- Geben Sie in [6] einen falschen Wert ein, um eine Fehlermeldung anzuzeigen. Oben verlangt die Einschränkung [@AssertFalse], dass das Feld [assertFalse] den Wert [false] hat;
![]() |
- in [7] die Antwort des Servers: Die Einschränkung [@NotNull] für leere Felder wurde ausgelöst, und die zugehörige Fehlermeldung wurde zurückgegeben;
- in [8] die Meldung für das Feld [assertFalse], für das die Einschränkung [@AssertFalse] nicht erfüllt wurde, zusammen mit den Fehlercodes. Beachten Sie, dass diese Codes mit internationalisierten Meldungen verknüpft sein können;
Hier ist ein weiteres Beispiel:
![]() |

Der Leser ist eingeladen, die verschiedenen Fehlerfälle zu testen, bis alle gültigen Daten eingegeben wurden:
![]() | ![]() |
Hinweis: Das Datumsformat ist das angelsächsische Format: mm/tt/jjjj.
4.23. [/m28]: Auslagerung von Fehlermeldungen
In der Klasse [ActionModel02] haben wir die Meldungen fest codiert. Es ist vorzuziehen, sie in Meldungsdateien auszulagern. Wir folgen dem Beispiel der Aktion [/m25]. Wir erstellen das folgende neue Aktionsmodell [ActionModel03]:
![]() |
package istia.st.springmvc.models;
import java.util.Date;
import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.URL;
public class ActionModel03 {
@NotNull
@AssertFalse
private Boolean assertFalse;
@NotNull
@AssertTrue
private Boolean assertTrue;
@NotNull
@Future
private Date dateInFuture;
@NotNull
@Past
private Date dateInPast;
@NotNull
@Max(value = 100)
private Integer intMax100;
@NotNull
@Min(value = 10)
private Integer intMin10;
@NotNull
@NotBlank
private String strNotBlank;
@NotNull
@Size(min = 4, max = 6)
private String strBetween4and6;
@NotNull
@Pattern(regexp = "^\\d{2}:\\d{2}:\\d{2}$")
private String hhmmss;
@NotNull
@Email
private String email;
@NotNull
@Length(max = 4, min = 4)
private String str4;
@Range(min = 10, max = 14)
@NotNull
private Integer int1014;
@URL
private String url;
// getters and setters
...
}
Fehlermeldungen werden in den [messages.properties]-Dateien ausgelagert:
![]() |
Die Datei [messages_fr.properties] sieht wie folgt aus:
NotNull=Le champ ne peut être vide
typeMismatch=Format invalide
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier
Range.actionModel03.int1014=La valeur doit être dans l'intervalle [10,14]
NotBlank.actionModel03.strNotBlank=La chaîne doit être non blanche
AssertFalse.actionModel03.assertFalse=Seule la valeur [false] est acceptée
Pattern.actionModel03.hhmmss=Le format doit être hh:mm:ss
Past.actionModel03.dateInPast=Il faut une date antérieure ou égale à celle d'aujourd'hui
Future.actionModel03.dateInFuture=Il faut une date postérieure à celle d'aujourd'hui
Length.actionModel03.str4=La chaîne doit avoir 4 caractères exactement
Min.actionModel03.intMin10=Minimum 10
Max.actionModel03.intMax100=Maximum 100
AssertTrue.actionModel03.assertTrue=Seule la valeur [true] est acceptée
Email.actionModel03.email=Adresse invalide
Size.actionModel03.strBetween4and6=La chaîne doit avoir entre 4 et 6 caractères
URL.actionModel03.url=URL invalide
In den Zeilen 4–16 wurden Fehlermeldungen hinzugefügt. Sie haben das folgende Format:
Die Codes können nicht beliebig gewählt werden. Es handelt sich um diejenigen, die in der vorherigen [/m27]-Aktion angezeigt wurden. Zum Beispiel:
![]()
In den Nachrichtendateien müssen Sie für das Feld [int1014] einen der vier oben genannten Codes verwenden.
Die Datei [messages_en.properties] sieht wie folgt aus:
NotNull=The field can't be empty
typeMismatch=Invalid format
typeMismatch.actionModel01.a=Parameter [a] must be an integer
Range.actionModel03.int1014=Value must be in [10,14] interval
NotBlank.actionModel03.strNotBlank=String can't be empty
AssertFalse.actionModel03.assertFalse=Only boolean [false] is allowed
Pattern.actionModel03.hhmmss=String format is hh:mm:ss
Past.actionModel03.dateInPast=Date must be before or equal to today's date
Future.actionModel03.dateInFuture=Date must be after today's date
Length.actionModel03.str4=String must be four characters long
Min.actionModel03.intMin10=Minimum 10
Max.actionModel03.intMax100=Maximum 100
AssertTrue.actionModel03.assertTrue=Only boolean [true] is allowed
Email.actionModel03.email=Invalid email
Size.actionModel03.strBetween4and6=String must be between four and six characters long
URL.actionModel03.url=Invalid URL
Das Aktionsmodell [ActionModel03] wird von der folgenden Aktion verwendet:
// ----------------------- externalization of error messages ------------------------
@RequestMapping(value = "/m28", method = RequestMethod.POST)
public Map<String, Object> m28(@Valid ActionModel03 data, BindingResult result, HttpServletRequest request) {
Map<String, Object> map = new HashMap<String, Object>();
// spring application context
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
// local
Locale locale = RequestContextUtils.getLocale(request);
// mistakes?
if (result.hasErrors()) {
for (FieldError error : result.getFieldErrors()) {
// search for error msg using error codes
// the msg is searched in the message files
// error codes in table format
String[] codes = error.getCodes();
// in chain form
String listCodes = String.join(" - ", codes);
// research
String msg = null;
int i = 0;
while (msg == null && i < codes.length) {
try {
msg = ctx.getMessage(codes[i], null, locale);
} catch (Exception e) {
}
i++;
}
// have we found?
if (msg == null) {
msg = String.format("Indiquez un message pour l'un des codes [%s]", listCodes);
}
// we have found - we add the error to the dictionary
map.put(error.getField(), msg);
}
} else {
// no errors
map.put("data", data);
}
return map;
}
Wir haben diese Art von Code bereits besprochen. Das Einzige, was wirklich zählt, ist Zeile 23: Die zurückgegebene Fehlermeldung hängt von der Ländereinstellung der Anfrage ab.
Hier ist ein Beispiel auf Französisch:
![]() | ![]() |
und nun auf Englisch:
![]() | ![]() |









































































