14. MVC-Webanwendung in einer 3-Tier-Architektur – Beispiel 1
14.1. Einleitung
Bislang haben wir uns auf Beispiele beschränkt, die zu Lehrzwecken dienen. Aus diesem Grund mussten sie einfach gehalten sein. Wir stellen nun eine einfache Anwendung vor, die dennoch über mehr Funktionen verfügt als alle bisher vorgestellten. Sie zeichnet sich dadurch aus, dass sie die drei Schichten einer 3-Tier-Architektur nutzt:

Leser, die die Grundlagen einer MVC-Webanwendung in einer 3-Tier-Architektur vergessen haben, sollten diese in Abschnitt 4 noch einmal nachlesen.
Die Webanwendung, die wir erstellen werden, ermöglicht es uns, eine Gruppe von Personen mithilfe von vier Operationen zu verwalten:
- Liste der Personen in der Gruppe
- eine Person zur Gruppe hinzufügen
- Person in der Gruppe bearbeiten
- Person aus der Gruppe entfernen
Dies sind die vier Grundoperationen einer Datenbanktabelle. Wir werden zwei Versionen dieser Anwendung schreiben:
- In Version 1 wird die [DAO]-Schicht keine Datenbank verwenden. Die Gruppenmitglieder werden in einem einfachen [ArrayList]-Objekt gespeichert, das intern von der [DAO]-Schicht verwaltet wird. Dies ermöglicht es dem Leser, die Anwendung ohne die Einschränkungen einer Datenbank zu testen.
- In Version 2 werden wir die Gruppe von Personen in einer Datenbanktabelle ablegen. Wir werden zeigen, dass dies möglich ist, ohne die Webschicht von Version 1 zu beeinträchtigen, die unverändert bleibt.
Die folgenden Screenshots zeigen die Seiten, die die Anwendung mit dem Benutzer austauscht.



![]() |
![]() |
14.2. Das Eclipse-Projekt
Das Anwendungsprojekt trägt den Namen [people-01]:

Dieses Projekt umfasst die drei Schichten der 3-Schichten-Architektur der Anwendung:
![]() |
- Die [DAO]-Schicht ist im Paket [istia.st.mvc.personnes.dao] enthalten
- Die [business]- oder [service]-Schicht ist im Paket [istia.st.mvc.personnes.service] enthalten
- Die [web]- oder [ui]-Schicht ist im Paket [istia.st.mvc.personnes.web] enthalten
- Das Paket [istia.st.mvc.personnes.entities] enthält Objekte, die von verschiedenen Schichten gemeinsam genutzt werden
- Das Paket [istia.st.mvc.people.tests] enthält die JUnit-Tests für die [DAO]- und [Service]-Schichten
Wir werden die drei Schichten [dao], [service] und [web] nacheinander untersuchen. Da das Schreiben zu lange dauern und das Lesen zu mühsam sein könnte, werden wir die Erklärungen manchmal schnell durchgehen, außer wenn der vorgestellte Stoff neu ist.
14.3. Darstellung einer Person
Die Anwendung verwaltet eine Gruppe von Personen. Die Screenshots in Abschnitt 14.1 zeigten einige der Merkmale einer Person. Formal werden diese durch eine [Person]-Klasse dargestellt:
![]()
Die Klasse [Person] sieht wie folgt aus:
- Eine Person wird anhand der folgenden Informationen identifiziert:
- id: eine eindeutige Kennung für eine Person
- last_name: der Nachname der Person
- Vorname: ihr Vorname
- dateOfBirth: ihr Geburtsdatum
- maritalStatus: ob sie verheiratet ist oder nicht
- nbChildren: die Anzahl der Kinder
- Das Attribut [version] ist ein Attribut, das für die Zwecke der Anwendung künstlich hinzugefügt wurde. Aus objektorientierter Sicht wäre es wahrscheinlich vorzuziehen gewesen, dieses Attribut einer von [Person] abgeleiteten Klasse hinzuzufügen. Seine Notwendigkeit wird deutlich, wenn man Anwendungsfälle für die Webanwendung betrachtet. Ein solcher Anwendungsfall ist der folgende:
Zum Zeitpunkt T1 beginnt Benutzer U1 mit der Bearbeitung einer Person P. Zu diesem Zeitpunkt beträgt die Anzahl der Kinder 0. U1 ändert diese Zahl auf 1, doch bevor die Änderung bestätigt wird, beginnt Benutzer U2 mit der Bearbeitung derselben Person P. Da U1 seine Änderung noch nicht bestätigt hat, sieht U2 die Anzahl der Kinder als 0 an. U2 ändert den Namen der Person P in Großbuchstaben. Dann speichern U1 und U2 ihre Änderungen in dieser Reihenfolge. Die Änderung von U2 hat Vorrang: Der Name wird in Großbuchstaben angezeigt und die Anzahl der Kinder bleibt bei Null, obwohl U1 glaubt, sie auf 1 geändert zu haben.
Das Konzept der Personenversion hilft uns, dieses Problem zu lösen. Betrachten wir denselben Anwendungsfall noch einmal:
Zum Zeitpunkt T1 beginnt ein Benutzer U1 mit der Bearbeitung einer Person P. In diesem Moment beträgt die Anzahl der Kinder 0 und die Version ist V1. Er ändert die Anzahl der Kinder auf 1, doch bevor er seine Änderung festschreibt, wechselt ein Benutzer U2 in den Bearbeitungsmodus für dieselbe Person P. Da U1 seine Änderung noch nicht festgeschrieben hat, sieht U2 die Anzahl der Kinder als 0 und die Version als V1. U2 ändert den Namen der Person P in Großbuchstaben. Dann speichern U1 und U2 ihre Änderungen in dieser Reihenfolge. Vor dem Speichern einer Änderung überprüfen wir, ob der Benutzer, der die Person P ändert, dieselbe Version hat wie die aktuell gespeicherte Version der Person P. Dies ist bei Benutzer U1 der Fall. Seine Änderung wird daher akzeptiert, und wir ändern dann die Version der geänderten Person von V1 auf V2, um anzuzeigen, dass die Person eine Änderung erfahren hat. Bei der Validierung der Änderung von U2 stellen wir fest, dass U2 die Version V1 der Person P hat, während die aktuelle Version V2 ist. Wir können den Benutzer U2 dann darüber informieren, dass jemand anderes vor ihm gehandelt hat und dass er mit der neuen Version der Person P beginnen muss. Er wird dies tun, die Version V2 der Person P abrufen, die nun ein Kind hat, den Namen großschreiben und die Änderung validieren. Seine Änderung wird akzeptiert, wenn die registrierte Person P weiterhin die Version V2 hat. Letztendlich werden die von U1 und U2 vorgenommenen Änderungen berücksichtigt, während im Anwendungsfall ohne Versionen eine der Änderungen verloren gegangen wäre.
- Zeilen 32–40: Ein Konstruktor, der die Felder einer Person initialisieren kann. Das Feld [version] wird weggelassen.
- Zeilen 43–51: Ein Konstruktor, der eine Kopie der als Parameter übergebenen Person erstellt. Wir haben nun zwei Objekte mit identischem Inhalt, auf die jedoch zwei verschiedene Zeiger verweisen.
- Zeile 55: Die Methode [toString] wird neu definiert, um eine Zeichenkette zurückzugeben, die den Zustand der Person darstellt
14.4. Die [DAO]-Schicht
Die [DAO]-Schicht besteht aus den folgenden Klassen und Schnittstellen:
![]()
- [IDao] ist die Schnittstelle, die von der [dao]-Schicht bereitgestellt wird
- [DaoImpl] ist eine Implementierung dieser Schnittstelle, bei der die Gruppe von Personen in einem [ArrayList]-Objekt gekapselt ist
- [DaoException] ist ein Typ einer ungeprüften Ausnahme, die von der [dao]-Schicht ausgelöst wird
Die Schnittstelle [ IDao] sieht wie folgt aus:
- Die Schnittstelle verfügt über vier Methoden für die vier Operationen, die wir an der Personengruppe durchführen möchten:
- getAll: zum Abrufen einer Sammlung von Personen
- getOne: zum Abrufen einer Person mit einer bestimmten ID
- saveOne: zum Hinzufügen einer Person (id=-1) oder zum Ändern einer bestehenden Person (id ≠ -1)
- deleteOne: zum Löschen einer Person mit einer bestimmten ID
Die [DAO]-Schicht kann Ausnahmen auslösen. Diese sind vom Typ [ DaoException]:
- Zeile 3: Die Klasse [DaoException], die von [RuntimeException] abgeleitet ist, ist ein nicht behandelter Ausnahmetyp: Der Compiler verlangt nicht, dass wir:
- diesen Ausnahmetyp beim Aufruf einer Methode, die ihn auslösen könnte, mit einem try/catch-Block abzufangen
- das Schlüsselwort „throws DaoException“ in die Signatur einer Methode aufzunehmen, die die Ausnahme auslösen könnte
Diese Technik verhindert, dass wir die Methoden der Schnittstelle [IDao] mit Ausnahmen eines bestimmten Typs signieren müssen. Jede Implementierung, die ungeprüfte Ausnahmen auslöst, ist dann zulässig, was der Architektur Flexibilität verleiht.
- Zeile 6: ein Fehlercode. Die [dao]-Schicht löst verschiedene Ausnahmen aus, die durch unterschiedliche Fehlercodes identifiziert werden. Dies ermöglicht es der für die Behandlung der Ausnahme zuständigen Schicht, die genaue Fehlerquelle zu ermitteln und entsprechende Maßnahmen zu ergreifen. Es gibt andere Wege, um das gleiche Ergebnis zu erzielen. Einer davon besteht darin, für jeden möglichen Fehlertyp einen Ausnahmetyp zu erstellen, zum Beispiel MissingLastNameException, MissingFirstNameException, IncorrectAgeException, ...
- Zeilen 13–16: Der Konstruktor, mit dem Sie eine Ausnahme erstellen können, die durch einen Fehlercode und eine Fehlermeldung identifiziert wird.
- Zeilen 8–10: Die Methode, mit der der Ausnahmebehandler den Fehlercode abrufen kann.
Die Klasse [ DaoImpl] implementiert die Schnittstelle [IDao]:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | |
Wir werden diesen Code nur kurz skizzieren. Auf die kniffligeren Teile werden wir jedoch etwas näher eingehen.
- Zeile 13: das [ArrayList]-Objekt, das die Gruppe von Personen enthält
- Zeile 16: die ID der zuletzt hinzugefügten Person. Jedes Mal, wenn eine neue Person hinzugefügt wird, wird diese ID um 1 erhöht.
Die Klasse [DaoImpl] wird als einzelne Instanz instanziiert. Dies wird als Singleton bezeichnet. Eine Webanwendung bedient ihre Benutzer gleichzeitig. Zu jedem Zeitpunkt laufen mehrere Threads auf dem Webserver. Diese Threads teilen sich die Singletons:
- das aus der [dao]-Schicht
- das in der [service]-Schicht
- die der verschiedenen Controller, Datenvalidatoren usw. in der Web-Schicht
Wenn ein Singleton private Felder hat, sollten Sie sich sofort fragen, warum es diese hat. Sind sie gerechtfertigt? Tatsächlich werden sie von verschiedenen Threads gemeinsam genutzt. Wenn sie schreibgeschützt sind, ist dies kein Problem, sofern sie zu einem Zeitpunkt initialisiert werden können, zu dem Sie sicher sind, dass nur ein aktiver Thread vorhanden ist. Wir wissen in der Regel, wie wir diesen Moment erkennen können. Das ist der Zeitpunkt, an dem die Webanwendung startet, aber noch nicht damit begonnen hat, Clients zu bedienen. Sind sie schreib- und lesbar, muss eine Zugriffssynchronisation für die Felder implementiert werden; andernfalls ist eine Katastrophe unvermeidlich. Wir werden dieses Problem veranschaulichen, wenn wir die [dao]-Schicht testen.
- Die Klasse [DaoImpl] hat keinen Konstruktor. Daher wird ihr Standardkonstruktor verwendet.
- Zeilen 19–38: Die Methode [init] wird aufgerufen, wenn das Singleton der [dao]-Schicht instanziiert wird. Sie erstellt eine Liste mit drei Personen.
- Zeilen 41–43: Implementiert die [getAll]-Methode der [IDao]-Schnittstelle. Sie gibt eine Referenz auf die Liste der Personen zurück.
- Zeilen 46–55: Implementiert die Methode [getOne] der Schnittstelle [IDao]. Ihr Parameter ist die ID der gesuchten Person.
Um diese abzurufen, rufen wir in den Zeilen 113–126 eine private Methode [getPosition] auf. Diese Methode gibt die Position der gesuchten Person in der Liste zurück oder -1, falls die Person nicht gefunden wurde.
Wird die Person gefunden, gibt die Methode [getOne] eine Referenz (Zeile 51) auf eine Kopie dieser Person zurück, nicht auf die Person selbst. Wenn ein Benutzer eine Person bearbeiten möchte, werden die Informationen zu dieser Person tatsächlich von der [dao]-Schicht angefordert und zur Bearbeitung in Form einer Referenz auf ein [Person]-Objekt an die [web]-Schicht weitergeleitet. Diese Referenz dient als Eingabecontainer im Bearbeitungsformular. Wenn der Benutzer seine Änderungen in der Webschicht übermittelt, wird der Inhalt des Eingabecontainers geändert. Wenn der Container ein Verweis auf die tatsächliche Person in der [ArrayList] der [dao]-Schicht ist, wird diese Person geändert, obwohl die Änderungen noch nicht an die [service]- und [dao]-Schichten weitergeleitet wurden. Letztere ist die einzige Schicht, die zur Verwaltung der Personenliste berechtigt ist. Daher muss die Web-Schicht mit einer Kopie der zu ändernden Person arbeiten. Hier stellt die [dao]-Schicht diese Kopie bereit.
Wird die gesuchte Person nicht gefunden, wird eine [DaoException] mit dem Fehlercode 2 ausgelöst (Zeile 53).
- Zeilen 94–104: Implementiert die Methode [deleteOne] der Schnittstelle [IDao]. Ihr Parameter ist die ID der zu löschenden Person. Wenn die zu löschende Person nicht existiert, wird eine [DaoException] mit dem Fehlercode 2 ausgelöst.
- Zeilen 58–91: Implementiert die Methode [saveOne] der Schnittstelle [IDao]. Ihr Parameter ist ein [Person]-Objekt. Wenn dieses Objekt die ID -1 hat, handelt es sich um eine neue Person, die hinzugefügt wird. Andernfalls wird die Person in der Liste mit dieser ID anhand der Werte im Parameter geändert.
- Zeile 60: Die Gültigkeit des [Person]-Parameters wird durch eine private Methode [check] überprüft, die in den Zeilen 129–155 definiert ist. Diese Methode führt grundlegende Prüfungen der Werte der verschiedenen Felder von [Person] durch. Wird eine Anomalie festgestellt, wird eine [DaoException] mit einem spezifischen Fehlercode ausgelöst. Da die Methode [saveOne] diese Ausnahme nicht behandelt, wird sie an die aufrufende Methode weitergeleitet.
- Zeile 62: Wenn der Parameter [Person] eine ID von -1 hat, handelt es sich um einen Hinzufügungsvorgang. Das [Person]-Objekt wird der internen Liste der Personen hinzugefügt (Zeile 66), mit der ersten verfügbaren ID (Zeile 64) und einer Versionsnummer von 1 (Zeile 65).
- Wenn der Parameter [Person] eine [id] ungleich -1 hat, bedeutet dies, dass die Person in der internen Liste mit dieser [id] geändert wird. Zunächst prüfen wir (Zeilen 70–75), ob die zu ändernde Person existiert. Ist dies nicht der Fall, lösen wir eine [DaoException] mit dem Fehlercode 2 aus.
- Wenn die Person existiert, überprüfen wir, ob ihre aktuelle Version mit der des [Person]-Parameters übereinstimmt, der die auf das Original anzuwendenden Änderungen enthält. Ist dies nicht der Fall, bedeutet dies, dass der Benutzer, der versucht, die Person zu ändern, nicht über die neueste Version verfügt. Wir informieren ihn darüber, indem wir eine [DaoException] mit dem Fehlercode 3 auslösen (Zeilen 79–80).
- Wenn alles gut geht, werden die Änderungen am ursprünglichen Personendatensatz vorgenommen (Zeilen 85–90)
Es ist klar, dass diese Methode synchronisiert werden muss. Beispielsweise könnte die Person zwischen dem Zeitpunkt, an dem wir überprüfen, ob die zu ändernde Person tatsächlich vorhanden ist, und dem Zeitpunkt, an dem die Änderung vorgenommen wird, von jemand anderem aus der Liste entfernt worden sein. Die Methode sollte daher als [synchronized] deklariert werden, um sicherzustellen, dass sie jeweils nur von einem Thread ausgeführt wird. Das Gleiche gilt für die anderen Methoden der [IDao]-Schnittstelle. Wir tun dies jedoch nicht, sondern verlagern diese Synchronisation lieber in die [service]-Schicht. Um Synchronisationsprobleme hervorzuheben, werden wir während des Testens der [dao]-Schicht die Ausführung von [saveOne] für 10 ms (Zeile 83) anhalten – zwischen dem Moment, in dem wir wissen, dass wir die Änderung vornehmen können, und dem Moment, in dem wir sie tatsächlich vornehmen. Der Thread, der [saveOne] ausführt, verliert dann die CPU an einen anderen Thread. Dies erhöht die Wahrscheinlichkeit, dass wir Zugriffskonflikte in der Liste der Personen feststellen.
14.5. [DAO]-Schicht-Tests
Für die [dao]-Schicht wird ein JUnit-Test geschrieben:
![]() | ![]() |
[TestDao] ist der JUnit-Test. Um Probleme beim gleichzeitigen Zugriff auf die Personenliste aufzuzeigen, werden Threads vom Typ [ThreadDaoMajEnfants] erstellt. Diese sind dafür zuständig, die Anzahl der Kinder einer bestimmten Person um 1 zu erhöhen.
[TestDao] enthält fünf Tests, [test1] bis [test5]. Wir stellen hier nur zwei davon vor; die Leser sind eingeladen, die anderen im Quellcode zu diesem Artikel zu erkunden.
- Zeile 9: Verweis auf die Implementierung der zu testenden [dao]-Schicht
- Zeilen 12–15: der JUnit-Testkonstruktor. Er erstellt eine Instanz vom Typ [DaoImpl] aus der zu testenden [dao]-Schicht und initialisiert sie.
Die Methode [test1] testet die vier Methoden der Schnittstelle [IDao] wie folgt:
- Zeile 3: Liste der Personen abrufen
- Zeile 6: Wir zeigen sie an
[1,1,Joachim,Major,13/01/1984,true,2]
[2,1,Mélanie,Humbort,12/01/1985,false,1]
[3,1,Charles,Lemarchand,01/01/1986,false,0]
Der Test fügt dann eine Person hinzu, ändert sie und löscht sie. Somit werden die vier Methoden der [IDao]-Schnittstelle verwendet.
- Zeilen 8–10: Eine neue Person wird hinzugefügt (id=-1).
- Zeile 11: Wir rufen die ID der hinzugefügten Person ab, da ihr durch das Hinzufügen eine zugewiesen wurde. Zuvor hatte sie noch keine.
- Zeilen 13–14: Wir fragen die [dao]-Schicht nach einer Kopie der soeben hinzugefügten Person. Beachten Sie, dass die [dao]-Schicht eine Ausnahme auslöst, wenn die angeforderte Person nicht gefunden wird. Dies führt zu einem Absturz in Zeile 13. Wir hätten diesen Fall sauberer handhaben können. In Zeile 14 überprüfen wir den Namen der abgerufenen Person.
- Zeilen 16–17: Wir ändern diesen Namen und bitten die [DAO]-Schicht, die Änderungen zu speichern.
- Zeilen 19–20: Wir fordern von der [DAO]-Schicht eine Kopie der soeben hinzugefügten Person an und überprüfen deren neuen Namen.
- Zeile 22: Löschen Sie die zu Beginn des Tests hinzugefügte Person.
- Zeilen 23–34: Fordern Sie eine Kopie der soeben gelöschten Person von der [DAO]-Schicht an. Sie sollten eine [DaoException] mit dem Code 2 erhalten.
- Zeilen 36–37: Die Liste der Personen wird erneut angefordert. Wir sollten dieselbe Liste erhalten wie zu Beginn des Tests.
Die Methode [test4] soll Probleme beim gleichzeitigen Zugriff auf die Methoden der [dao]-Schicht aufzeigen. Denken Sie daran, dass diese Methoden nicht synchronisiert wurden. Der Testcode lautet wie folgt:
- Zeilen 3–6: Wir fügen eine Person P ohne Kinder zur Liste hinzu. Wir speichern ihre [id] (Zeile 6).
- Zeilen 7–13: Wir starten N Threads. Jeder von ihnen erhöht die Anzahl der Kinder der Person P um 1. Letztendlich sollte die Person P N Kinder haben.
- Zeilen 15–17: Die Methode [test4], die die N Threads gestartet hat, wartet, bis diese ihre Arbeit beendet haben, bevor sie die neue Anzahl der Kinder der Person P überprüft.
- Zeilen 18–21: Wir rufen die Person P ab und überprüfen, ob ihre Anzahl an Kindern N beträgt.
- Zeilen 22–35: Person P wird entfernt, und wir überprüfen, ob sie nicht mehr in der Liste vorhanden ist.
In Zeile 11 sehen wir, dass die Threads vom Typ [ThreadDaoMajEnfants] sind. Der Konstruktor für diesen Typ hat drei Parameter:
- den Namen des Threads, der zur Nachverfolgung über Protokolle dient
- eine Referenz auf die [dao]-Schicht, damit der Thread darauf zugreifen kann
- die ID der Person, an der der Thread arbeiten soll
Der Typ [ThreadDaoMajEnfants] sieht wie folgt aus:
- Zeile 9: [ThreadDaoMajEnfants] ist tatsächlich ein Thread
- Zeilen 18–22: Der Konstruktor, der den Thread mit drei Informationen initialisiert
- dem Namen [name], der dem Thread gegeben wurde
- eine Referenz [dao] auf die [dao]-Schicht. Beachten Sie, dass wir erneut mit dem Schnittstellentyp [IDao] und nicht mit dem Implementierungstyp [DaoImpl] arbeiten.
- die Kennung [id] der Person, an der der Thread arbeiten soll
Wenn [test4] einen Thread [ThreadDaoMajEnfants] startet (Zeile 12 von test4), wird dessen [run]-Methode (Zeile 25) ausgeführt:
- Zeilen 78–81: Die private Methode [suivi] ermöglicht die Protokollierung auf dem Bildschirm. Die Methode [run] nutzt sie, um die Ausführung des Threads zu verfolgen.
- Der Thread versucht, die Anzahl der Kinder der Person P mit der Kennung [id] um 1 zu erhöhen. Diese Aktualisierung erfordert möglicherweise mehrere Versuche. Betrachten wir zwei Threads [TH1] und [TH2]. [TH1] fordert eine Kopie der Person P von der [dao]-Schicht an. Er erhält sie und stellt fest, dass sie die Version V1 hat. [TH1] wird unterbrochen. [TH2], der ihm folgte, tut dasselbe und erhält dieselbe Version V1 der Person P. [TH2] wird unterbrochen. [TH2] übernimmt wieder die Kontrolle, erhöht die Anzahl der Kinder für P und speichert seine Änderungen. Wir wissen, dass diese Änderungen nun gespeichert sind und dass sich die Version von P auf V2 ändern wird. [TH1] hat seine Arbeit beendet. [TH2] übernimmt wieder die Kontrolle und tut dasselbe. Seine Aktualisierung von P wird abgelehnt, da es eine Kopie von P in Version V1 besitzt, während das Original P nun in Version V2 vorliegt. [TH2] muss dann den gesamten Zyklus [lesen -> aktualisieren -> speichern] wiederholen. Aus diesem Grund finden wir die Schleife in den Zeilen 32–72. In dieser Schleife fordert der Thread:
- eine Kopie der Person P zum Bearbeiten anfordert (Zeile 34)
- wartet 10 ms (Zeile 43). Dies ist künstlich und zielt darauf ab, den Thread zwischen dem Lesen der Person P und der tatsächlichen Aktualisierung in der Personenliste zu unterbrechen, um die Wahrscheinlichkeit von Konflikten zu erhöhen.
- erhöht die Anzahl der Kinder von P (Zeile 54) und speichert P (Zeile 56). Wenn der Thread nicht über die richtige Version von P verfügt, wird von der [dao]-Schicht eine Ausnahme ausgelöst. Wir rufen dann den Ausnahmecode ab (Zeile 61), um zu überprüfen, ob es sich tatsächlich um Code 3 handelt (falsche Version von P). Ist dies nicht der Fall, wird die Ausnahme an die aufrufende Methode, letztendlich an die Testmethode [test4], weitergeleitet. Liegt die Ausnahme mit Code 3 vor, starten wir den Zyklus [Lesen -> Aktualisieren -> Speichern] neu. Liegt keine Ausnahme vor, ist die Aktualisierung abgeschlossen und die Arbeit des Threads beendet.
Was zeigen die Tests?
In der ersten getesteten Konfiguration:
- kommentieren wir die wait-Anweisung in der Methode [saveOne] von [DaoImpl] aus (Zeile 83, Abschnitt 14.4).
- Die Methode [test4] erstellt 100 Threads (Zeile 8, Abschnitt 14.5).
Es werden folgende Ergebnisse erzielt:

Alle fünf Tests waren erfolgreich.
In der zweiten getesteten Konfiguration:
- Die „wait“-Anweisung in der Methode [saveOne] von [DaoImpl] ist nicht mehr auskommentiert (Zeile 83, Abschnitt 14.4).
- Die Methode [test4] erstellt 2 Threads (Zeile 8, Abschnitt 14.5).
Es werden folgende Ergebnisse erzielt:
![]() | ![]() |
Der Test [test4] ist fehlgeschlagen. Wir haben zwei Threads erstellt, von denen jeder die Aufgabe hatte, die Anzahl der Kinder einer Person P, die anfangs 0 hatte, um 1 zu erhöhen. Wir erwarteten daher 2 Kinder, nachdem die beiden Threads ausgeführt worden waren, aber wir haben nur eines.
Sehen wir uns die Bildschirmprotokolle von [test4] an, um zu verstehen, was passiert ist:
- Zeile 1: Thread Nr. 0 beginnt seine Arbeit
- Zeile 2: Er hat eine Kopie der Person P abgerufen und stellt fest, dass die Anzahl der Kinder 0 ist
- Zeile 3: Er stößt in seiner [run]-Methode auf [Thread.sleep(10)] und pausiert daher zum Zeitpunkt [1145536368171] (ms)
- Zeile 4: Thread Nr. 1 übernimmt dann den Prozessor und beginnt mit seiner Arbeit
- Zeile 5: Er hat eine Kopie der Person P abgerufen und stellt fest, dass die Anzahl der Kinder 0 ist
- Zeile 6: Er stößt in seiner [run]-Methode auf [Thread.sleep(10)] und hält daher an
- Zeile 7: Thread 0 erhält die CPU zum Zeitpunkt [1145536368187] (ms) zurück, d. h. 16 ms, nachdem er sie verloren hat.
- Zeile 8: dasselbe gilt für Thread Nr. 1
- Zeile 9: Thread Nr. 0 hat sich aktualisiert und die Anzahl der Kinder auf 1 gesetzt
- Zeile 10: Thread Nr. 1 hat dasselbe getan
Die Frage lautet: Warum konnte Thread Nr. 1 seine Aktualisierung durchführen, obwohl er normalerweise nicht mehr über die korrekte Version von Person P verfügte, die gerade von Thread Nr. 0 aktualisiert worden war?
Zunächst lässt sich zwischen den Zeilen 7 und 8 eine Anomalie feststellen: Es scheint, als hätte Thread #0 zwischen diesen beiden Zeilen die CPU an Thread #1 verloren. Was tat er in diesem Moment? Er führte die Methode [saveOne] der [dao]-Schicht aus. Diese Methode hat das folgende Grundgerüst (siehe Abschnitt 14.4):
- Thread #0 führte [saveOne] aus und fuhr mit Zeile 8 fort, wo er gezwungen war, den Prozessor freizugeben. In der Zwischenzeit las er die Version von Person P, die 1 war, da Person P noch nicht aktualisiert worden war.
- Da die CPU nun frei war, übernahm Thread #1 sie. Dieser führte seinerseits [saveOne] aus und erreichte Zeile 8, wo er gezwungen war, die CPU freizugeben. In der Zwischenzeit las er die Version von Person P, die 1 war, da Person P noch nicht aktualisiert worden war.
- Da der Prozessor frei wurde, übernahm Thread #0 ihn. Ab Zeile 9 führte er seine Aktualisierung durch und setzte die Anzahl der Kinder auf 1. Dann beendete die [run]-Methode von Thread #0, und der Thread zeigte das Protokoll an, in dem stand, dass er die Anzahl der Kinder auf 1 gesetzt hatte (Zeile 9).
- Da der Prozessor frei wurde, übernahm Thread #1 ihn. Ab Zeile 9 führte er seine Aktualisierung durch und setzte die Anzahl der Kinder auf 1. Warum 1? Weil er eine Kopie von P enthält, bei der die Anzahl der Kinder auf 0 gesetzt ist. Dies geht aus dem Protokoll hervor (Zeile 5). Dann beendete Thread #1 die [run]-Methode und der Thread zeigte das Protokoll an, in dem stand, dass er die Anzahl der Kinder auf 1 gesetzt hatte (Zeile 10).
Woher kommt das Problem? Es rührt daher, dass Thread #0 keine Zeit hatte, seine Änderung zu übernehmen und somit die Version von Person P zu aktualisieren, bevor Thread #1 versuchte, diese Version zu lesen, um zu prüfen, ob sich Person P geändert hatte. Dieses Szenario ist unwahrscheinlich, aber nicht unmöglich. Wir mussten Thread #0 zwingen, die CPU abzugeben, damit es so aussah, als gäbe es nur zwei Threads. Ohne diesen Workaround war es mit der vorherigen Konfiguration nicht gelungen, dasselbe Szenario mit 100 Threads zu reproduzieren. Der Test [test4] war erfolgreich verlaufen.
Was ist die Lösung? Es gibt zweifellos mehrere. Eine davon, die einfach umzusetzen ist, besteht darin, die Methode [saveOne] zu synchronisieren:
public synchronized void saveOne(Personne personne)
Das Schlüsselwort [synchronized] stellt sicher, dass jeweils nur ein Thread die Methode ausführen kann. Somit darf Thread Nr. 1 [saveOne] erst ausführen, wenn Thread Nr. 0 die Methode beendet hat. Wir können dann sicher sein, dass die Version der Person P geändert wurde, wenn Thread Nr. 1 [saveOne] aufruft. Seine Aktualisierung wird dann abgelehnt, da er nicht über die richtige Version von P verfügt.
Dies sind die vier Methoden der [dao]-Schicht, die synchronisiert werden müssten. Wir entscheiden uns jedoch, diese Schicht wie beschrieben beizubehalten und die Synchronisation in die [service]-Schicht zu verlagern. Dafür gibt es mehrere Gründe:
- Wir gehen davon aus, dass der Zugriff auf die [dao]-Schicht immer über eine [service]-Schicht erfolgt. Dies ist in unserer Webanwendung der Fall.
- Es kann auch aus anderen Gründen als denen, die uns dazu veranlassen würden, die Methoden der [dao]-Schicht zu synchronisieren, notwendig sein, den Zugriff auf die Methoden der [service]-Schicht zu synchronisieren. In diesem Fall besteht keine Notwendigkeit, die Methoden der [dao]-Schicht zu synchronisieren. Wenn wir sicher sind, dass:
- der gesamte Zugriff auf die [DAO]-Schicht über die [Service]-Schicht erfolgt
- jeweils nur ein Thread die [Service]-Schicht nutzt
dann können wir sicher sein, dass die Methoden der [DAO]-Schicht nicht von zwei Threads gleichzeitig ausgeführt werden.
Wir werden nun die [service]-Schicht untersuchen.
14.6. Die [Service]-Schicht
Die [service]-Schicht besteht aus den folgenden Klassen und Schnittstellen:
![]()
- [IService] ist die von der [dao]-Schicht bereitgestellte Schnittstelle
- [ServiceImpl] ist eine Implementierung dieser Schnittstelle
Die [IService]-Schnittstelle sieht wie folgt aus:
Sie ist identisch mit der [IDao]-Schnittstelle.
Die [ServiceImpl]-Implementierung der [IService]-Schnittstelle lautet wie folgt:
- Zeilen 10–19: Das Attribut [IDao dao] ist eine Referenz auf die [dao]-Schicht. Es wird von Spring IoC initialisiert.
- Zeilen 22–24: Implementierung der Methode [getAll] der Schnittstelle [IService]. Die Methode leitet die Anfrage einfach an die [dao]-Schicht weiter.
- Zeilen 27–29: Implementierung der Methode [getOne] der Schnittstelle [IService]. Die Methode leitet die Anfrage einfach an die [dao]-Schicht weiter.
- Zeilen 32–34: Implementierung der Methode [saveOne] der Schnittstelle [IService]. Die Methode leitet die Anfrage einfach an die [dao]-Schicht weiter.
- Zeilen 37–39: Implementierung der Methode [deleteOne] der Schnittstelle [IService]. Die Methode leitet die Anfrage einfach an die [dao]-Schicht weiter.
- Alle Methoden sind synchronisiert (mithilfe des Schlüsselworts `synchronized`), wodurch sichergestellt wird, dass jeweils nur ein Thread die [service]-Schicht und folglich auch die [dao]-Schicht nutzen kann.
14.7. Tests für die [service]-Schicht
Für die [service]-Schicht wird ein JUnit-Test geschrieben:
![]() | ![]() |
[TestService] ist der JUnit-Test. Die durchgeführten Tests sind genau dieselben wie die für die [dao]-Schicht. Das Grundgerüst von [TestService] sieht wie folgt aus:
- Zeile 9: Die zu testende [Service]-Schicht ist vom Typ [ServiceImpl].
- Zeilen 11–15: Der JUnit-Testkonstruktor erstellt eine Instanz der zu testenden [Service]-Schicht (Zeile 12), erstellt eine Instanz der [DAO]-Schicht (Zeile 13) und weist die [Service]-Schicht an, diese [DAO]-Schicht zu verwenden (Zeile 14).
Die Methode [test1] testet die vier Methoden der Schnittstelle [IService] auf dieselbe Weise wie die gleichnamige Testmethode der [dao]-Schicht. Der einzige Unterschied besteht darin, dass sie auf die [service]-Schicht (Zeilen 25, 32, 35) statt auf die [dao]-Schicht zugreift.
Die Methode [test4] soll Probleme beim gleichzeitigen Zugriff auf die Methoden der [service]-Schicht aufzeigen. Sie ist wiederum identisch mit der Testmethode [test4] der [dao]-Schicht. Es gibt jedoch einige Details, die sich unterscheiden:
- wir adressieren die [service]-Schicht anstelle der [dao]-Schicht (Zeile 55)
- wir übergeben eine Referenz auf die [service]-Schicht an die Threads statt an die [dao]-Schicht (Zeile 61)
Der Typ [ThreadServiceMajEnfants] ist ebenfalls nahezu identisch mit dem Typ [ThreadDaoMajEnfants], mit der Ausnahme, dass er mit der [Service]-Schicht statt mit der [DAO]-Schicht arbeitet:
- Zeile 12: Der Thread arbeitet mit der [service]-Ebene
Wir führen die Tests mit der Konfiguration durch, die auf der [dao]-Ebene zu Problemen geführt hat:
- Wir entfernen die Auskommentierung der wait-Anweisung in der [saveOne]-Methode von [DaoImpl] (Zeile 83, Abschnitt 14.4).
- Die Methode [test4] erstellt 100 Threads (Zeile 65, Abschnitt 14.7).
Die erhaltenen Ergebnisse lauten wie folgt:
![]() |
Es war die Synchronisation der Methoden in der [service]-Schicht, die den Erfolg des [test4]-Tests ermöglichte.
14.8. Die [Web]-Schicht
Lassen Sie uns die 3-Schichten-Architektur unserer Anwendung noch einmal betrachten:
![]() |
Die [Web]-Schicht stellt dem Benutzer Bildschirme zur Verfügung, über die er die Personengruppe verwalten kann:
- Liste der Personen in der Gruppe
- eine Person zur Gruppe hinzufügen
- eine Person in der Gruppe bearbeiten
- Person aus der Gruppe entfernen
Dazu stützt es sich auf die [Service]-Schicht, die wiederum die [DAO]-Schicht aufruft. Die von der [Web]-Schicht verwalteten Bildschirme haben wir bereits vorgestellt (Abschnitt 14.1). Um die Web-Schicht zu beschreiben, werden wir nacheinander Folgendes vorstellen:
- ihre Konfiguration
- ihre Ansichten
- ihren Controller
- einige Tests
14.8.1. Konfiguration der Webanwendung
Das Eclipse-Projekt für die Anwendung sieht wie folgt aus:

- Im Paket [istia.st.mvc.personnes.web] finden Sie den [Application]-Controller.
- Die JSP/JSTL-Seiten befinden sich in [WEB-INF/views].
- Der Ordner [lib] enthält die von der Anwendung benötigten Bibliotheken von Drittanbietern. Diese sind im Ordner [Web App Libraries] zu finden.
[web.xml]
Die Datei [web.xml] wird vom Webserver zum Laden der Anwendung verwendet. Ihr Inhalt lautet wie folgt:
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<display-name>mvc-personnes-01</display-name>
<!-- ServletPersonne -->
<servlet>
<servlet-name>personnes</servlet-name>
<servlet-class>
istia.st.mvc.personnes.web.Application
</servlet-class>
<init-param>
<param-name>urlEdit</param-name>
<param-value>/WEB-INF/vues/edit.jsp</param-value>
</init-param>
<init-param>
<param-name>urlErreurs</param-name>
<param-value>/WEB-INF/vues/erreurs.jsp</param-value>
</init-param>
<init-param>
<param-name>urlList</param-name>
<param-value>/WEB-INF/vues/list.jsp</param-value>
</init-param>
</servlet>
<!-- Mapping ServletPersonne-->
<servlet-mapping>
<servlet-name>personnes</servlet-name>
<url-pattern>/do/*</url-pattern>
</servlet-mapping>
<!-- welcome files -->
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<!-- Unexpected error page -->
<error-page>
<exception-type>java.lang.Exception</exception-type>
<location>/WEB-INF/vues/exception.jsp</location>
</error-page>
</web-app>
- Zeilen 27–30: URLs [/do/*] werden vom [people]-Servlet verarbeitet
- Zeilen 9–12: Das [personnes]-Servlet ist eine Instanz der Klasse [Application], einer Klasse, die wir erstellen werden.
- Zeilen 13–24: Definieren Sie drei Parameter [urlList, urlEdit, urlErrors], die die URLs der JSP-Seiten für die Ansichten [list, edit, errors] identifizieren.
- Zeilen 32–34: Die Anwendung verfügt über eine Standard-Startseite [index.jsp], die sich im Stammverzeichnis des Webanwendungsordners befindet.
- Zeilen 36–39: Die Anwendung verfügt über eine Standard-Fehlerseite, die angezeigt wird, wenn der Webserver auf eine Ausnahme stößt, die von der Anwendung nicht behandelt wird.
- Zeile 37: Das <exception-type>-Tag gibt den Typ der Ausnahme an, die von der <error-page>-Direktive behandelt wird; hier handelt es sich um den Typ [java.lang.Exception] und dessen Untertypen, d. h. alle Ausnahmen.
- Zeile 38: Das <location>-Tag gibt die JSP-Seite an, die angezeigt werden soll, wenn eine Ausnahme des durch <exception-type> definierten Typs auftritt. Die aufgetretene Ausnahme ist auf dieser Seite in einem Objekt namens exception verfügbar, sofern die Seite die Anweisung enthält:
<%@ page isErrorPage="true" %>
- (Fortsetzung)
- Wenn <exception-type> einen Typ T1 angibt und eine Ausnahme vom Typ T2 (die nicht von T1 abgeleitet ist) an den Webserver weitergeleitet wird, sendet der Server dem Client eine proprietäre Ausnahmeseite, die in der Regel nicht sehr benutzerfreundlich ist. Daher ist das <error-page>-Tag in der Datei [web.xml] so wichtig.
[index.jsp]
Diese Seite wird angezeigt, wenn ein Benutzer den Anwendungskontext direkt aufruft, ohne eine URL anzugeben, d. h. hier [/personnes-01]. Ihr Inhalt lautet wie folgt:
<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/do/list"/>
[index.jsp] leitet den Client zur URL [/do/list] weiter. Diese URL zeigt die Liste der Personen in der Gruppe an.
14.8.2. Die JSP/JSTL-Seiten der Anwendung
Sie dient zur Anzeige der Personenliste:

Der Code lautet wie folgt:
<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>
<html>
<head>
<title>MVC - Personnes</title>
</head>
<body background="<c:url value="/ressources/standard.jpg"/>">
<h2>Liste des personnes</h2>
<table border="1">
<tr>
<th>Id</th>
<th>Version</th>
<th>Prénom</th>
<th>Nom</th>
<th>Date de naissance</th>
<th>Marié</th>
<th>Nombre d'enfants</th>
<th></th>
</tr>
<c:forEach var="personne" items="${personnes}">
<tr>
<td><c:out value="${personne.id}"/></td>
<td><c:out value="${personne.version}"/></td>
<td><c:out value="${personne.prenom}"/></td>
<td><c:out value="${personne.nom}"/></td>
<td><dt:format pattern="dd/MM/yyyy">${personne.dateNaissance.time}</dt:format></td>
<td><c:out value="${personne.marie}"/></td>
<td><c:out value="${personne.nbEnfants}"/></td>
<td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
<td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
</tr>
</c:forEach>
</table>
<br>
<a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
</body>
</html>
- Diese Ansicht erhält ein Element in ihrer Vorlage:
- das Element [people], das mit einer [ArrayList] von [Person]-Objekten verknüpft ist
- Zeilen 22–34: Wir durchlaufen die Liste ${people}, um eine HTML-Tabelle mit den Personen in der Gruppe anzuzeigen.
- Zeile 31: Die URL, auf die der [Edit]-Link verweist, wird mithilfe des [id]-Feldes der aktuellen Person festgelegt, damit der mit der URL [/do/edit] verknüpfte Controller weiß, welche Person bearbeitet werden soll.
- Zeile 32: Dasselbe geschieht für den [Delete]-Link.
- Zeile 28: Um das Geburtsdatum der Person im Format TT/MM/JJJJ anzuzeigen, verwenden wir das Tag <dt> aus der [DateTime]-Tag-Bibliothek des Apache [Jakarta Taglibs]-Projekts:

Die Beschreibungsdatei für diese Tag-Bibliothek ist in Zeile 3 definiert.
- Zeile 37: Der Link [Hinzufügen] zum Hinzufügen einer neuen Person verweist auf die URL [/do/edit], genau wie der Link [Bearbeiten] in Zeile 31. Der Wert -1 für den Parameter [id] gibt an, dass es sich um ein Hinzufügen und nicht um eine Bearbeitung handelt.
Sie dient zur Anzeige des Formulars zum Hinzufügen einer neuen Person oder zum Ändern einer bestehenden:
![]() |
Der Code für die Ansicht [edit.jsp] lautet wie folgt:
<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>
<html>
<head>
<title>MVC - Personnes</title>
</head>
<body background="../ressources/standard.jpg">
<h2>Ajout/Modification d'une personne</h2>
<c:if test="${erreurEdit != ''}">
<h3>Echec de la mise à jour :</h3>
L'erreur suivante s'est produite : ${erreurEdit}
<hr>
</c:if>
<form method="post" action="<c:url value="/do/validate"/>">
<table border="1">
<tr>
<td>Id</td>
<td>${id}</td>
</tr>
<tr>
<td>Version</td>
<td>${version}</td>
</tr>
<tr>
<td>Prénom</td>
<td>
<input type="text" value="${prenom}" name="prenom" size="20">
</td>
<td>${erreurPrenom}</td>
</tr>
<tr>
<td>Nom</td>
<td>
<input type="text" value="${nom}" name="nom" size="20">
</td>
<td>${erreurNom}</td>
</tr>
<tr>
<td>Date de naissance (JJ/MM/AAAA)</td>
<td>
<input type="text" value="${dateNaissance}" name="dateNaissance">
</td>
<td>${erreurDateNaissance}</td>
</tr>
<tr>
<td>Marié</td>
<td>
<c:choose>
<c:when test="${marie}">
<input type="radio" name="marie" value="true" checked>Oui
<input type="radio" name="marie" value="false">Non
</c:when>
<c:otherwise>
<input type="radio" name="marie" value="true">Oui
<input type="radio" name="marie" value="false" checked>Non
</c:otherwise>
</c:choose>
</td>
</tr>
<tr>
<td>Nombre d'enfants</td>
<td>
<input type="text" value="${nbEnfants}" name="nbEnfants">
</td>
<td>${erreurNbEnfants}</td>
</tr>
</table>
<br>
<input type="hidden" value="${id}" name="id">
<input type="hidden" value="${version}" name="version">
<input type="submit" value="Valider">
<a href="<c:url value="/do/list"/>">Annuler</a>
</form>
</body>
</html>
Diese Ansicht zeigt ein Formular zum Hinzufügen einer neuen Person oder zum Aktualisieren einer bestehenden Person an. Um den Text zu vereinfachen, verwenden wir von nun an den einzigen Begriff [Aktualisieren]. Die Schaltfläche [Submit] (Zeile 73) löst eine POST-Anfrage an die URL [/do/validate] (Zeile 16) aus. Wenn der POST-Vorgang fehlschlägt, wird die Ansicht [edit.jsp] mit den aufgetretenen Fehlern erneut angezeigt; andernfalls wird die Ansicht [list.jsp] angezeigt.
- Die Ansicht [edit.jsp], die sowohl bei einer GET-Anfrage als auch bei einer fehlgeschlagenen POST-Anfrage angezeigt wird, erhält die folgenden Elemente in ihrem Modell:
Attribut | GET | POST |
ID der Person, die aktualisiert wird | gleich | |
ihre Version | gleich | |
Vorname | Vorname eingegeben | |
sein/ihr Nachname | Eingegabter Nachname | |
sein/ihr Geburtsdatum | eingegebenes Geburtsdatum | |
Familienstand | Familienstand eingegeben | |
Anzahl der Kinder | Anzahl der Kinder eingegeben | |
leer | Eine Fehlermeldung, die darauf hinweist, dass das Hinzufügen oder Ändern während des durch die Schaltfläche [Absenden] ausgelösten POST-Vorgangs fehlgeschlagen ist. Leer, wenn kein Fehler vorliegt. | |
leer | Weist auf einen falschen Vornamen hin – andernfalls leer | |
leer | meldet einen falschen Namen – andernfalls leer | |
leer | weist auf ein falsches Geburtsdatum hin – andernfalls leer | |
leer | bedeutet eine falsche Anzahl von Kindern – andernfalls leer |
- Zeilen 11–15: Wenn der POST-Versuch des Formulars fehlschlägt, wird [errorEdit!=''] zurückgegeben und eine Fehlermeldung angezeigt.
- Zeile 16: Das Formular wird an die URL [/do/validate] gesendet
- Zeile 20: Das Element [id] der Vorlage wird angezeigt
- Zeile 24: Das Element [version] der Vorlage wird angezeigt
- Zeilen 26–32: Eingabe des Vornamens der Person:
- Wenn das Formular zunächst angezeigt wird (GET), zeigt ${firstName} den aktuellen Wert des Feldes [firstName] des aktualisierten Objekts [Person] an, und ${firstNameError} ist leer.
- Im Falle eines Fehlers nach dem POST wird der eingegebene Wert ${firstName} erneut angezeigt, zusammen mit einer etwaigen Fehlermeldung ${firstNameError}
- Zeilen 33–39: Eingabe des Nachnamens der Person
- Zeilen 40–46: Eingabe des Geburtsdatums der Person
- Zeilen 47–61: Eingabe des Familienstands der Person über ein Optionsfeld. Wir verwenden den Wert des Feldes [married] des Objekts [Person], um zu bestimmen, welches der beiden Optionsfelder ausgewählt werden soll.
- Zeilen 62–68: Eingabe der Anzahl der Kinder der Person
- Zeile 71: Ein verstecktes HTML-Feld namens [id] mit einem Wert, der dem Feld [id] der zu aktualisierenden Person entspricht, -1 für einen Neuzugang oder einem anderen Wert für eine Änderung.
- Zeile 72: Ein verstecktes HTML-Feld namens [version] mit einem Wert, der dem Feld [id] der zu aktualisierenden Person entspricht.
- Zeile 73: Die Schaltfläche [Absenden] des Formulars
- Zeile 74: Ein Link, um zur Liste der Personen zurückzukehren. Er ist mit [Abbrechen] beschriftet, da er es dem Benutzer ermöglicht, das Formular zu verlassen, ohne es abzuschicken.
Sie wird verwendet, um eine Seite anzuzeigen, die darauf hinweist, dass eine von der Anwendung nicht behandelte Ausnahme aufgetreten ist und an den Webserver weitergeleitet wurde.
Löschen wir zum Beispiel eine Person, die in der Gruppe nicht existiert:
![]() |
Der Code für die Ansicht [exception.jsp] lautet wie folgt:
<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ page isErrorPage="true" %>
<%
response.setStatus(200);
%>
<html>
<head>
<title>MVC - Personnes</title>
</head>
<body background="<c:url value="/ressources/standard.jpg"/>">
<h2>MVC - personnes</h2>
L'exception suivante s'est produite :
<%= exception.getMessage()%>
<br><br>
<a href="<c:url value="/do/list"/>">Retour à la liste</a>
</body>
</html>
- Diese Ansicht erhält in ihrer Vorlage ein Schlüsselelement, das [exception]-Element, bei dem es sich um die vom Webserver abgefangene Ausnahme handelt. Damit dieses Element vom Webserver in die JSP-Seitenvorlage eingefügt wird, muss in der Seite in Zeile 3 das Tag definiert sein.
- Zeile 6: Wir setzen den HTTP-Statuscode der Antwort auf 200. Dies ist der erste HTTP-Header der Antwort. Der Statuscode 200 signalisiert dem Client, dass seine Anfrage erfolgreich war. In der Regel ist ein HTML-Dokument in die Antwort des Servers eingebunden. Dies ist hier der Fall. Wenn der HTTP-Statuscode der Antwort nicht auf 200 gesetzt ist, hat er den Wert 500, was bedeutet, dass ein Fehler aufgetreten ist. Wenn der Webserver eine unbehandelte Ausnahme abfängt, betrachtet er diese Situation als abnormal und signalisiert dies mit einem 500-Code. Die Reaktion auf einen HTTP-500-Code variiert je nach Browser: Firefox zeigt das HTML-Dokument an, das dieser Antwort möglicherweise beigefügt ist, während der IE dieses Dokument ignoriert und eine eigene Seite anzeigt. Aus diesem Grund haben wir den 500-Code durch den 200-Code ersetzt.
- Zeile 16: Der Ausnahmetext wird angezeigt
- Zeile 18: Dem Benutzer wird ein Link angeboten, um zur Liste der Personen zurückzukehren
Diese Seite dient zur Anzeige von Fehlern bei der Initialisierung der Anwendung, d. h. von Fehlern, die während der Ausführung der Methode [init] des Controller-Servlets festgestellt wurden. Dies kann beispielsweise das Fehlen eines Parameters in der Datei [web.xml] sein, wie im folgenden Beispiel gezeigt:

Der Code für die Seite [errors.jsp] lautet wie folgt:
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<html>
<head>
<title>MVC - Personnes</title>
</head>
<body>
<h2>Les erreurs suivantes se sont produites</h2>
<ul>
<c:forEach var="erreur" items="${erreurs}">
<li>${erreur}</li>
</c:forEach>
</ul>
</body>
</html>
Die Seite erhält in ihrer Vorlage ein [errors]-Element, bei dem es sich um eine [ArrayList] von [String]-Objekten handelt; dies sind Fehlermeldungen. Sie werden durch die Schleife in den Zeilen 13–15 angezeigt.
14.8.3. Der Anwendungscontroller
Der [Application]-Controller ist im Paket [istia.st.mvc.personnes.web] definiert:
![]()
Struktur und Initialisierung des Controllers
Das Grundgerüst des [Application]-Controllers sieht wie folgt aus:
- Zeilen 20–36: Rufen Sie die in der Datei [web.xml] angegebenen Parameter ab.
- Zeilen 39–41: Der Parameter [urlErrors] muss vorhanden sein, da er die URL der Ansicht [errors] angibt, die etwaige Initialisierungsfehler anzeigt. Ist er nicht vorhanden, wird die Anwendung durch Auslösen einer [ServletException] beendet (Zeile 40). Diese Ausnahme wird an den Webserver weitergeleitet und durch das <error-page>-Tag in der Datei [web.xml] behandelt. Daher wird die Ansicht [exception.jsp] angezeigt:

Der Link [Zurück zur Liste] oben ist inaktiv. Ein Klick darauf liefert dieselbe Antwort, solange die Anwendung nicht geändert und neu geladen wurde. Wie wir bereits gesehen haben, ist dies für andere Arten von Ausnahmen nützlich.
- Zeile 43: Erstellt eine [DaoImpl]-Instanz, die die [dao]-Schicht implementiert
- Zeile 44: initialisiert diese Instanz (erstellt eine anfängliche Liste mit drei Personen)
- Zeile 46: Erstellt eine Instanz von [ServiceImpl], die die [service]-Schicht implementiert
- Zeile 47: Initialisiert die [service]-Schicht, indem ihr eine Referenz auf die [dao]-Schicht bereitgestellt wird
Nachdem der Controller initialisiert wurde, verfügen seine Methoden über eine [service]-Referenz auf die [service]-Schicht (Zeile 15), die sie zur Ausführung der vom Benutzer angeforderten Aktionen verwenden. Diese werden von der [doGet]-Methode abgefangen, die sie von einer bestimmten Methode des Controllers verarbeiten lässt:
Url | HTTP-Methode | Controller-Methode |
GET | doListPeople | |
GET | doEditPerson | |
POST | doValidatePerson | |
GET | doDeletePerson |
Die [doGet]-Methode
Der Zweck dieser Methode besteht darin, die Verarbeitung von vom Benutzer angeforderten Aktionen an die richtige Methode weiterzuleiten. Der Code lautet wie folgt:
- Zeilen 7–13: Wir prüfen, ob die Liste der Initialisierungsfehler leer ist. Ist dies nicht der Fall, zeigen wir die Ansicht [errors(errors)] an, die die Fehler meldet.
- Zeile 15: Wir rufen die [get]- oder [post]-Methode ab, die der Client für die Anfrage verwendet hat.
- Zeile 17: Wir rufen den Wert des Parameters [action] aus der Anfrage ab.
- Zeilen 23–27: Verarbeiten der [GET /do/list]-Anfrage, die die Liste der Personen anfordert.
- Zeilen 28–32: Wir verarbeiten die [GET /do/delete]-Anfrage, die das Löschen einer Person anfordert.
- Zeilen 33–37: Verarbeitung der Anfrage [GET /do/edit], die das Formular zur Aktualisierung einer Person anfordert.
- Zeilen 38–42: Verarbeitung der Anfrage [POST /do/validate], die die Validierung der aktualisierten Person anfordert.
- Zeile 44: Wenn die angeforderte Aktion nicht zu den vorherigen fünf gehört, behandeln wir sie so, als wäre sie [GET /do/list].
Die Methode [doListPersonnes]
Diese Methode verarbeitet die Anfrage [GET /do/list], die die Liste der Personen anfordert:

Der Code lautet wie folgt:
- Zeile 5: Wir fordern die Liste der Personen in der Gruppe von der [Service]-Schicht an und speichern sie im Modell unter dem Schlüssel „people“.
- Zeile 7: Die in Abschnitt 14.8.2 beschriebene Ansicht [list.jsp] wird angezeigt.
Die Methode [doDeletePerson]
Diese Methode verarbeitet die Anfrage [GET /do/delete?id=XX], die das Löschen der Person mit der ID id=XX anfordert. Die URL [/do/delete?id=XX] ist die der [Delete]-Links in der Ansicht [list.jsp]:

deren Code wie folgt lautet:
Zeile 12 zeigt die URL [/do/delete?id=XX] für den Link [Löschen] an. Die Methode [doDeletePerson], die diese URL verarbeitet, muss die Person mit der ID XX löschen und anschließend die aktualisierte Liste der Personen in der Gruppe anzeigen. Der Code lautet wie folgt:
- Zeile 5: Die zu verarbeitende URL hat das Format [/do/delete?id=XX]. Wir entnehmen den Wert [XX] aus dem Parameter [id].
- Zeile 7: Wir weisen die [service]-Schicht an, die Person mit der erhaltenen ID zu löschen. Wir führen keine Validierung durch. Wenn die Person, die wir löschen wollen, nicht existiert, löst die [dao]-Schicht eine Ausnahme aus, die bis zur [service]-Schicht weitergeleitet wird. Wir behandeln sie auch hier im Controller nicht. Sie wird daher bis zum Webserver weitergeleitet, der gemäß der Konfiguration die Seite [exception.jsp] anzeigt, die in Abschnitt 14.8.2 beschrieben ist:

- Zeile 9: Wenn das Löschen erfolgreich war (keine Ausnahme), wird der Client zur entsprechenden URL [list] weitergeleitet. Da die gerade verarbeitete URL [/do/delete] war, lautet die Weiterleitungs-URL [/do/list]. Der Browser führt daher eine [GET /do/list]-Anfrage durch, wodurch die Liste der Personen angezeigt wird.
Die Methode [doEditPerson]
Diese Methode verarbeitet die Anfrage [GET /do/edit?id=XX], die das Formular zum Aktualisieren der Person mit der ID id=XX anfordert. Die URL [/do/edit?id=XX] ist diejenige, die für die Links [Bearbeiten] und [Hinzufügen] in der Ansicht [list.jsp] verwendet wird:

deren Code wie folgt lautet:
In Zeile 11 sehen wir die URL [/do/edit?id=XX] für den Link [Bearbeiten] und in Zeile 17 die URL [/do/edit?id=-1] für den Link [Hinzufügen]. Die Methode [doEditPersonne] muss das Bearbeitungsformular für die Person mit der ID XX anzeigen oder, falls es sich um einen Neuzugang handelt, ein leeres Formular anzeigen.
![]() | ![]() |
Der Code für die Methode [doEditPerson] lautet wie folgt:
- Die GET-Anfrage zielt auf eine URL der Form [/do/edit?id=XX] ab. In Zeile 5 rufen wir den Wert von [id] ab. Dann gibt es zwei Fälle:
- Wenn id nicht gleich -1 ist, handelt es sich um eine Aktualisierung, und wir müssen ein Formular anzeigen, das bereits mit den Informationen der zu bearbeitenden Person vorbelegt ist. In Zeile 10 wird diese Person von der [service]-Schicht angefordert.
- Ist id gleich -1, handelt es sich um einen Neuzugang, und es muss ein leeres Formular angezeigt werden. Dazu wird in den Zeilen 13–14 eine leere Person angelegt.
- Das [Person]-Objekt wird in die in Abschnitt 14.8.2 beschriebene Seitenvorlage [edit.jsp] eingefügt. Diese Vorlage enthält die folgenden Elemente: [errorEdit, id, version, firstName, errorFirstName, lastName, errorLastName, dateOfBirth, errorDateOfBirth, spouse, numberOfChildren, errorNumberOfChildren]. Diese Elemente werden in den Zeilen 17–30 initialisiert, mit Ausnahme derjenigen, deren Wert eine leere Zeichenkette ist [firstNameError, lastNameError, birthDateError, childrenCountError]. Wir wissen, dass die JSTL-Bibliothek eine leere Zeichenfolge als Wert anzeigt, wenn diese Elemente in der Vorlage fehlen. Obwohl das Element [errorEdit] ebenfalls eine leere Zeichenfolge als Wert hat, wird es dennoch initialisiert, da auf der Seite [edit.jsp] eine Überprüfung seines Werts durchgeführt wird.
- Sobald das Modell bereit ist, wird die Steuerung an die Seite [edit.jsp] (Zeilen 32–33) übergeben, die die Ansicht [edit] generiert.
Die Methode [doValidatePersonne]
Diese Methode verarbeitet die [POST /do/validate]-Anfrage, die das Aktualisierungsformular validiert. Dieser POST-Aufruf wird durch die Schaltfläche [Validate] ausgelöst:

Sehen wir uns die Eingabeelemente des HTML-Formulars in der obigen Ansicht an:
Die POST-Anfrage enthält die Parameter [firstName, lastName, dateOfBirth, spouse, numberOfChildren, id, version] und wird an die URL [/do/validate] gesendet (Zeile 1). Sie wird von der folgenden Methode [doValidatePerson] verarbeitet:
- Zeilen 8–14: Der Parameter [firstName] aus der POST-Anfrage wird abgerufen und auf Gültigkeit geprüft. Ist er fehlerhaft, wird das Element [firstNameError] mit einer Fehlermeldung initialisiert und in die Anfrageattribute aufgenommen.
- Zeilen 16–22: Der gleiche Vorgang wird für den Parameter [lastName] durchgeführt
- Zeilen 24–32: Der gleiche Vorgang wird auf den Parameter [dateOfBirth] angewendet
- Zeile 34: Der Parameter [spouse] wird abgerufen. Wir prüfen seine Gültigkeit nicht, da er grundsätzlich aus dem Wert eines Optionsfelds stammt. Allerdings hindert nichts ein Programm daran, eine [POST /people-01/do/validate]-Anfrage mit einem fiktiven [spouse]-Parameter zu senden. Wir sollten daher die Gültigkeit dieses Parameters prüfen. Hier verlassen wir uns auf unsere Ausnahmebehandlung, die bewirkt, dass die Seite [exception.jsp] angezeigt wird, wenn der Controller die Ausnahme nicht selbst behandelt. Wenn also die Konvertierung des Parameters [marie] in einen booleschen Wert in Zeile 34 fehlschlägt, wird eine Ausnahme ausgelöst, was dazu führt, dass die Seite [exception.jsp] an den Client gesendet wird. Dieses Verhalten ist für uns in Ordnung.
- Zeilen 34–54: Wir rufen den Parameter [nbEnfants] ab und prüfen seinen Wert.
- Zeile 56: Wir rufen den Parameter [id] ab, ohne dessen Wert zu prüfen
- Zeile 58: Wir verfahren ebenso mit dem Parameter [version]
- Zeilen 60–65: Wenn das Formular ungültig ist, wird es mit den zuvor generierten Fehlermeldungen erneut angezeigt
- Zeilen 67–69: Ist es gültig, erstellen wir anhand der Formularfelder ein neues [Person]-Objekt
- Zeilen 70–78: Die Person wird gespeichert. Der Speichervorgang kann fehlschlagen. In einer Mehrbenutzerumgebung kann die zu ändernde Person möglicherweise gelöscht oder bereits von jemand anderem geändert worden sein. In diesem Fall löst die [dao]-Schicht eine Ausnahme aus, die wir hier behandeln.
- Zeile 80: Wenn keine Ausnahme aufgetreten ist, wird der Client zur URL [/do/list] weitergeleitet, um den neuen Status der Gruppe anzuzeigen.
- Zeile 75: Wenn beim Speichern eine Ausnahme aufgetreten ist, fordern wir die erneute Anzeige des ursprünglichen Formulars an und übergeben dabei die Fehlermeldung der Ausnahme (3. Parameter).
Die Methode [showFormulaire] (Zeilen 84–101) erstellt anhand der eingegebenen Werte (request.getParameter(" ... ") die für die Seite [edit.jsp] erforderliche Vorlage. Zur Erinnerung: Die Fehlermeldungen wurden bereits durch die Methode [doValidatePersonne] in die Vorlage eingefügt. Die Seite [edit.jsp] wird in den Zeilen 99–100 angezeigt.
14.9. Testen der Webanwendung
In Abschnitt 14.1 wurden eine Reihe von Tests vorgestellt. Wir laden den Leser ein, diese erneut auszuführen. Hier zeigen wir zusätzliche Screenshots, die Fälle von Datenzugriffskonflikten in einer Mehrbenutzerumgebung veranschaulichen:
[Firefox] ist der Browser des Benutzers U1. Benutzer U1 ruft die URL [http://localhost:8080/personnes-01] auf:

[IE] ist der Browser von Benutzer U2. Benutzer U2 fordert dieselbe URL an:

Benutzer U1 beginnt mit der Bearbeitung des Eintrags für [Lemarchand]:

Benutzer U2 tut dasselbe:

Benutzer U1 nimmt Änderungen vor und speichert:
![]() |
Benutzer U2 macht dasselbe:
![]() |
Der Benutzer U2 kehrt über den Link [Abbrechen] im Formular zur Liste der Benutzer zurück:

Er findet die Person [Lemarchand] in der von U1 geänderten Form. Nun löscht U2 [Lemarchand]:
![]() |
U1 hat immer noch seine eigene Liste und möchte [Lemarchand] erneut bearbeiten:
![]() |
U1 nutzt den Link [Zurück zur Liste], um zu sehen, was los ist:

Er stellt fest, dass [Lemarchand] tatsächlich nicht mehr auf der Liste steht...
14.10. Fazit
Wir haben die MVC-Architektur innerhalb einer 3-Schichten-Architektur [Web, Geschäftslogik, DAO] anhand eines einfachen Beispiels zur Verwaltung einer Personenliste implementiert. Dies ermöglichte es uns, die in den vorangegangenen Abschnitten vorgestellten Konzepte anzuwenden. In der von uns untersuchten Version wurde die Personenliste im Arbeitsspeicher gehalten. Wir werden uns bald mit Versionen befassen, bei denen diese Liste in einer Datenbanktabelle gespeichert wird.
Zunächst stellen wir jedoch ein Tool namens Spring IoC vor, das die Integration der verschiedenen Schichten einer mehrschichtigen Anwendung erleichtert.

















