14. Aplikacja internetowa MVC w architekturze trójwarstwowej – przykład 1
14.1. Présentation
Do tej pory ograniczaliśmy się do przykładów o charakterze dydaktycznym. Z tego powodu musiały one być proste. Teraz przedstawiamy podstawową aplikację, która jest jednak bardziej rozbudowana niż wszystkie dotychczasowe. Jej cechą charakterystyczną będzie wykorzystanie trzech warstw architektury trójwarstwowej:

Zachęcamy czytelnika do ponownego zapoznania się z zasadami działania aplikacji internetowej MVC w architekturze trójwarstwowej, jeśli je zapomniał, w akapicie 4.
Aplikacja internetowa, którą zamierzamy stworzyć, umożliwi zarządzanie grupą osób za pomocą czterech operacji:
- wyświetlenie listy osób w grupie
- dodanie osoby do grupy
- zmiana danych osoby z grupy
- usunięcie osoby z grupy
Te cztery podstawowe operacje odpowiadają operacjom na tabeli bazy danych. Napiszemy dwie wersje tej aplikacji:
- w wersji 1 warstwa [dao] nie będzie korzystać z bazy danych. Osoby z grupy będą przechowywane w prostym obiekcie [ArrayList], zarządzanym wewnętrznie przez warstwę [dao]. Umożliwi to czytelnikowi przetestowanie aplikacji bez ograniczeń związanych z bazą danych.
- W wersji 2 umieścimy grupę osób w tabeli bazy danych. Pokażemy, że nastąpi to bez wpływu na warstwę internetową wersji 1, która pozostanie niezmieniona.
Poniższe zrzuty ekranu przedstawiają strony, które aplikacja wyświetla użytkownikowi.



![]() |
![]() |
14.2. Projekt Eclipse
Projekt aplikacji nosi nazwę [personnes-01]:

Projekt ten obejmuje trzy warstwy trójwarstwowej architektury aplikacji:
![]() |
- warstwa [dao] znajduje się w pakiecie [istia.st.mvc.personnes.dao]
- warstwa [metier] lub [service] znajduje się w pakiecie [istia.st.mvc.personnes.service]
- warstwa [web] lub [ui] znajduje się w pakiecie [istia.st.mvc.personnes.web]
- pakiet [istia.st.mvc.personnes.entites] zawiera obiekty współdzielone między różnymi warstwami
- pakiet [istia.st.mvc.personnes.tests] zawiera testy JUnit dla warstw [dao] i [service]
Omówimy kolejno trzy warstwy: [dao], [service] i [web]. Ponieważ opisanie tego wszystkiego zajęłoby zbyt dużo czasu i mogłoby być zbyt nużące dla czytelnika, czasami będziemy nieco skracać wyjaśnienia, z wyjątkiem sytuacji, gdy omawiane zagadnienie jest nowe.
14.3. Reprezentacja osoby
Aplikacja zarządza grupą osób. Zrzuty ekranu z paragrafu 14.1 pokazały niektóre cechy osoby. Formalnie są one reprezentowane przez klasę [Personne]:
![]()
Klasa [Personne] ma następujący wygląd:
- osoba jest identyfikowana na podstawie następujących informacji:
- id: numer jednoznacznie identyfikujący daną osobę
- nazwisko: nazwisko osoby
- imię: jej imię
- dateNaissance: data urodzenia
- stan cywilny: informacja o tym, czy osoba jest w związku małżeńskim, czy nie
- nbEnfants: liczba jej dzieci
- atrybut [version] jest atrybutem dodanym sztucznie na potrzeby aplikacji. Z punktu widzenia obiektowego z pewnością lepiej byłoby dodać ten atrybut w klasie pochodnej od [Personne]. Jego potrzeba ujawnia się podczas analizy przypadków użycia aplikacji internetowej. Jeden z nich wygląda następująco:
W momencie T1 użytkownik U1 przechodzi do edycji danych osoby P. W tym momencie liczba dzieci wynosi 0. Zmienia tę liczbę na 1, ale zanim zatwierdzi swoją zmianę, użytkownik U2 rozpoczyna edycję tej samej osoby P. Ponieważ U1 nie zatwierdził jeszcze swojej zmiany, U2 widzi liczbę dzieci równą 0. U2 zamienia imię osoby P na wielkie litery. Następnie U1 i U2 zatwierdzają swoje zmiany w tej kolejności. Zmiana wprowadzona przez U2 będzie miała pierwszeństwo: imię zostanie zapisane wielkimi literami, a liczba dzieci pozostanie równa zero, mimo że U1 uważa, że zmienił ją na 1.
Pojęcie wersji osoby pomaga nam rozwiązać ten problem. Przyjrzyjmy się ponownie temu samemu przypadkowi użycia:
W momencie T1 użytkownik U1 rozpoczyna edycję danych osoby P. W tym momencie liczba dzieci wynosi 0, a wersja to V1. Zmienia liczbę dzieci na 1, ale zanim zatwierdzi swoją zmianę, użytkownik U2 rozpoczyna edycję tej samej osoby P. Ponieważ U1 nie zatwierdził jeszcze swojej zmiany, U2 widzi liczbę dzieci równą 0, a wersję V1. U2 zmienia imię osoby P na wielkie litery. Następnie U1 i U2 zatwierdzają swoje zmiany w tej kolejności. Przed zatwierdzeniem zmiany sprawdza się, czy osoba wprowadzająca zmiany w danym rekordzie osoby P posiada tę samą wersję, co aktualnie zarejestrowany rekord osoby P. Tak będzie w przypadku użytkownika U1. Jego zmiana zostaje zatem zaakceptowana, a wersja edytowanej osoby zostaje zmieniona z V1 na V2, aby odnotować fakt, że osoba ta została zmieniona. Podczas zatwierdzania zmiany wprowadzonej przez użytkownika U2 okaże się, że posiada on wersję osoby P o numerze V1, podczas gdy aktualna wersja tej osoby to V2. Będzie można wówczas poinformować użytkownika U2, że ktoś go wyprzedził i że musi zacząć od nowej wersji osoby P. Użytkownik ten wykona tę czynność, pobierze osobę P o wersji V2, która ma teraz dziecko, zamieni imię na wielkie litery i zatwierdzi zmiany. Jego zmiana zostanie zaakceptowana, jeśli zarejestrowana osoba P nadal ma wersję V2. Ostatecznie zmiany wprowadzone przez U1 i U2 zostaną uwzględnione, podczas gdy w przypadku scenariusza bez wersji jedna ze zmian zostałaby utracona.
- wiersze 32–40: konstruktor umożliwiający zainicjowanie pól osoby. Pomija się pole [version].
- wiersze 43–51: konstruktor, który tworzy kopię osoby przekazanej mu jako parametr. W ten sposób otrzymujemy dwa obiekty o identycznej zawartości, ale odwołujące się do nich dwa różne wskaźniki.
- wiersz 55: metoda [toString] została przedefiniowana, aby zwracała ciąg znaków reprezentujący stan osoby
14.4. Warstwa [dao]
Warstwa [dao] składa się z następujących klas i interfejsów:
![]()
- [IDao] to interfejs prezentowany przez warstwę [dao]
- [DaoImpl] jest jej implementacją, w której grupa osób jest enkapsulowana w obiekcie [ArrayList]
- [DaoException] to typ niekontrolowanych (unchecked) wyjątków, wywoływanych przez warstwę [dao]
Interfejs [IDao] ma następujący wygląd:
- Interfejs posiada cztery metody odpowiadające czterem operacjom, które chcemy wykonać na grupie osób:
- getAll: w celu uzyskania zbioru osób
- getOne: w celu uzyskania osoby o określonym id
- saveOne: w celu dodania osoby (id=-1) lub modyfikacji istniejącej osoby (id <> -1)
- deleteOne: w celu usunięcia osoby o określonym identyfikatorze id
Warstwa [dao] może generować wyjątki. Będą to wyjątki typu [DaoException] :
- linia 3: klasa [DaoException] wywodząca się z [RuntimeException] jest typem wyjątku niekontrolowanego: kompilator nie wymaga od nas:
- obsługi tego typu wyjątków za pomocą try/catch podczas wywoływania metody, która może go wygenerować
- umieszczać znacznika „throws DaoException” w sygnaturze metody, która może wywołać wyjątek
Technika ta pozwala nam uniknąć konieczności podpisywania metod interfejsu [IDao] za pomocą wyjątków określonego typu. Każda implementacja generująca niekontrolowane wyjątki będzie wówczas akceptowalna, co zapewni elastyczność architektury.
- wiersz 6: kod błędu. Warstwa [dao] będzie zgłaszać różne wyjątki, które będą identyfikowane za pomocą różnych kodów błędów. Pozwoli to warstwie odpowiedzialnej za obsługę wyjątku poznać dokładne źródło błędu i podjąć odpowiednie działania. Istnieją inne sposoby osiągnięcia tego samego rezultatu. Jednym z nich jest utworzenie typu wyjątku dla każdego możliwego rodzaju błędu, na przykład NomManquantException, PrenomManquantException, AgeIncorrectException, ...
- wiersze 13–16: konstruktor, który pozwoli utworzyć wyjątek identyfikowany przez kod błędu oraz komunikat o błędzie.
- wiersze 8–10: metoda, która umożliwi kodowi obsługi wyjątku pobranie kodu błędu.
Klasa [DaoImpl] implementuje interfejs [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 | |
Przedstawimy jedynie ogólny zarys tego kodu. Poświęcimy jednak nieco czasu na omówienie najbardziej skomplikowanych fragmentów.
- wiersz 13: obiekt [ArrayList], który będzie zawierał grupę osób
- wiersz 16: identyfikator ostatniej dodanej osoby. Przy każdym nowym dodaniu identyfikator ten będzie zwiększany o 1.
Klasa [DaoImpl] zostanie zainicjowana w postaci pojedynczego egzemplarza. Nazywa się to singletonem. Aplikacja internetowa obsługuje użytkowników jednocześnie. W danym momencie na serwerze internetowym wykonywanych jest kilka wątków. Wątki te współdzielą singletony:
- singleton warstwy [dao]
- singleton warstwy [service]
- te z różnych kontrolerów, walidatorów danych itp. warstwy internetowej
Jeśli singleton posiada pola prywatne, należy od razu zadać sobie pytanie, dlaczego je ma. Czy są one uzasadnione? W rzeczywistości będą one współdzielone między różnymi wątkami. Jeśli są one tylko do odczytu, nie stanowi to problemu, o ile można je zainicjować w momencie, w którym mamy pewność, że aktywny jest tylko jeden wątek. Zazwyczaj potrafimy znaleźć ten moment. Jest to moment uruchomienia aplikacji internetowej, kiedy jeszcze nie zaczęła ona obsługiwać klientów. Jeśli pola te są dostępne zarówno do odczytu, jak i do zapisu, należy wdrożyć synchronizację dostępu do pól, w przeciwnym razie grozi to katastrofą. Zilustrujemy ten problem podczas testowania warstwy [dao].
- Klasa [DaoImpl] nie posiada konstruktora. Zostanie zatem użyty jej domyślny konstruktor.
- wiersze 19–38: metoda [init] zostanie wywołana podczas instancjonowania singletonu warstwy [dao]. Tworzy ona listę trzech osób.
- wiersze 41–43: implementują metodę [getAll] z interfejsu [IDao]. Zwracają odwołanie do listy osób.
- wiersze 46–55: implementują metodę [getOne] z interfejsu [IDao]. Jej parametrem jest identyfikator poszukiwanej osoby.
Aby ją pobrać, wywołuje się prywatną metodę [getPosition] z wierszy 113–126. Metoda ta zwraca pozycję poszukiwanej osoby na liście lub -1, jeśli osoba ta nie została znaleziona.
Jeśli dana osoba została znaleziona, metoda [getOne] zwraca odwołanie (wiersz 51) do kopii tej osoby, a nie do samej osoby. W rzeczywistości, gdy użytkownik zechce zmodyfikować dane osoby, informacje o niej zostaną pobrane z warstwy [dao] i przekazane do warstwy [web] w celu modyfikacji, w postaci odwołania do obiektu [Personne]. Odwołanie to będzie służyć jako kontener danych wprowadzanych w formularzu modyfikacyjnym. Gdy w warstwie internetowej użytkownik prześle swoje zmiany, zawartość kontenera danych zostanie zmodyfikowana. Jeśli kontener jest odwołaniem do rzeczywistej osoby z warstwy [ArrayList] w warstwie [dao], to zostanie ona zmodyfikowana, mimo że zmiany nie zostały jeszcze przekazane do warstw [service] i [dao]. Tylko ta ostatnia warstwa jest uprawniona do zarządzania listą osób. Dlatego warstwa internetowa musi pracować na kopii osoby, która ma zostać zmodyfikowana. W tym przypadku warstwa [dao] dostarcza tę kopię.
Jeśli nie znaleziono poszukiwanej osoby, generowany jest wyjątek typu [DaoException] z kodem błędu 2 (wiersz 53).
- wiersze 94–104: implementują metodę [deleteOne] z interfejsu [IDao]. Jej parametrem jest identyfikator osoby, którą należy usunąć. Jeśli osoba, która ma zostać usunięta, nie istnieje, generowany jest wyjątek typu [DaoException] z kodem błędu 2.
- wiersze 58–91: implementują metodę [saveOne] z interfejsu [IDao]. Jej parametrem jest obiekt typu [Personne]. Jeśli obiekt ten ma id=-1, oznacza to dodanie osoby. W przeciwnym razie oznacza to modyfikację osoby z listy o tym id przy użyciu wartości podanych w parametrze.
- wiersz 60: poprawność parametru [Personne] jest sprawdzana przez metodę prywatną [check] zdefiniowaną w wierszach 129–155. Metoda ta przeprowadza podstawowe weryfikacje wartości poszczególnych pól obiektu [Personne]. Za każdym razem, gdy wykryta zostanie nieprawidłowość, uruchamiana jest metoda [DaoException] z określonym kodem błędu. Ponieważ metoda [saveOne] nie obsługuje tego wyjątku, zgłosi go do metody wywołującej.
- Wiersz 62: jeśli parametr [Personne] ma identyfikator równy -1, oznacza to dodanie. Obiekt [Personne] jest dodawany do wewnętrznej listy osób (wiersz 66) z pierwszym dostępnym identyfikatorem (wiersz 64) oraz numerem wersji równym 1 (wiersz 65).
- Jeśli parametr [Personne] ma wartość [id] inną niż -1, oznacza to modyfikację osoby z listy wewnętrznej posiadającej ten identyfikator [id]. Najpierw sprawdzamy (wiersze 70–75), czy osoba, którą należy zmodyfikować, istnieje. Jeśli tak nie jest, generujemy wyjątek typu [DaoException] z kodem błędu 2.
- Jeśli osoba rzeczywiście istnieje, sprawdzamy, czy jej aktualna wersja jest taka sama jak ta zawarta w parametrze [Personne], który zawiera zmiany do wprowadzenia w oryginale. Jeśli tak nie jest, oznacza to, że osoba chcąca wprowadzić zmiany nie posiada najnowszej wersji tej osoby. Informuje się o tym użytkownika, generując wyjątek typu [DaoException] z kodem błędu 3 (wiersze 79–80).
- Jeśli wszystko przebiega pomyślnie, zmiany są wprowadzane w oryginalnym rekordzie osoby (wiersze 85–90)
Wyraźnie widać, że ta metoda wymaga synchronizacji. Na przykład w okresie między sprawdzeniem, czy osoba, którą chcemy zmodyfikować, rzeczywiście istnieje, a momentem wprowadzenia zmiany, osoba ta mogła zostać usunięta z listy przez kogoś innego. Metoda powinna zatem zostać zadeklarowana jako [synchronized], aby zapewnić, że w danym momencie wykonuje ją tylko jeden wątek. To samo dotyczy pozostałych metod interfejsu [IDao]. Nie stosujemy jednak tej synchronizacji, preferując przeniesienie jej do warstwy [service]. Aby uwidocznić problemy z synchronizacją, podczas testów warstwy [dao] zatrzymamy wykonywanie [saveOne] na 10 ms (wiersz 83) między momentem, w którym wiemy, że możemy dokonać modyfikacji, a momentem, w którym faktycznie ją wykonujemy. Wątek wykonujący [saveOne] straci wówczas dostęp do procesora na rzecz innego wątku. W ten sposób zwiększamy prawdopodobieństwo wystąpienia konfliktów dostępu do listy osób.
14.5. Testy warstwy [dao]
Dla warstwy [dao] napisano test JUnit:
![]() | ![]() |
[TestDao] jest testem JUnit. Aby uwidocznić problemy związane z równoczesnym dostępem do listy osób, tworzone są wątki typu [ThreadDaoMajEnfants]. Ich zadaniem jest zwiększenie o 1 liczby dzieci danej osoby.
[TestDao] zawiera pięć testów o nazwach od [test1] do [test5]. Przedstawiamy tylko dwa z nich, a czytelnika zachęcamy do zapoznania się z pozostałymi w kodzie źródłowym dołączonym do tego artykułu.
- wiersz 9: odniesienie do implementacji warstwy [dao] poddanej testowaniu
- wiersze 12–15: konstruktor testu JUnit. Tworzy on instancję typu [DaoImpl] warstwy [dao], która ma zostać przetestowana, i inicjuje ją.
Metoda [test1] testuje cztery metody interfejsu [IDao] w następujący sposób:
- wiersz 3: żądana jest lista osób
- wiersz 6: wyświetla się ta lista
[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]
Następnie test dodaje osobę, modyfikuje ją i usuwa. W ten sposób wykorzystywane są wszystkie cztery metody interfejsu [IDao].
- wiersze 8–10: dodaje się nową osobę (id=-1).
- wiersz 11: pobierany jest identyfikator dodanej osoby, ponieważ w wyniku dodania otrzymała ona identyfikator. Wcześniej go nie miała.
- wiersze 13–14: zwracamy się do warstwy [dao] o kopię osoby, która została właśnie dodana. Należy pamiętać, że jeśli żądana osoba nie zostanie znaleziona, warstwa [dao] zgłasza wyjątek. Wówczas nastąpi awaria w wierszu 13. Można było obsłużyć ten przypadek w bardziej elegancki sposób. W wierszu 14 sprawdzamy nazwisko znalezionej osoby.
- wiersze 16–17: modyfikujemy to imię i nazwisko oraz prosimy warstwę [dao] o zapisanie zmian.
- wiersze 19–20: żądamy od warstwy [dao] kopii osoby, która została właśnie dodana, i sprawdzamy jej nowe imię.
- wiersz 22: usuwa się osobę dodaną na początku testu.
- wiersze 23–34: żądamy od warstwy [dao] kopii osoby, która została właśnie usunięta. Powinniśmy otrzymać warstwę [DaoException] o kodzie 2.
- wiersze 36–37: ponownie żąda się listy osób. Powinna pojawić się ta sama lista, co na początku testu.
Metoda [test4] ma na celu uwidocznienie problemów związanych z równoczesnym dostępem do metod warstwy [dao]. Należy pamiętać, że metody te nie zostały zsynchronizowane. Kod testu jest następujący:
- wiersze 3–6: dodajemy do listy osobę P, która nie ma dzieci. Zapisujemy jej [id] (wiersz 6).
- wiersze 7–13: uruchamia się N wątków. Każdy z nich zwiększy liczbę dzieci osoby P o 1. Ostatecznie osoba P powinna mieć N dzieci.
- wiersze 15–17: metoda [test4], która uruchomiła N wątków, czeka, aż zakończą one swoją pracę, a dopiero potem sprawdza nową liczbę dzieci osoby P.
- wiersze 18–21: pobieramy osobę P i sprawdzamy, czy liczba jej dzieci wynosi N.
- wiersze 22–35: osoba P zostaje usunięta, a następnie sprawdzamy, czy nie ma jej już na liście.
W wierszu 11 widać, że wątki są typu [ThreadDaoMajEnfants]. Konstruktor tego typu ma trzy parametry:
- nazwa nadana wątku, służąca do śledzenia go za pomocą logów
- numer referencyjny w warstwie [dao], aby wątek miał do niej dostęp
- identyfikator osoby, nad którą wątek ma pracować
Typ [ThreadDaoMajEnfants] ma następujący wygląd:
- wiersz 9: [ThreadDaoMajEnfants] jest rzeczywiście wątkiem
- wiersze 18–22: konstruktor, który inicjuje wątek przy użyciu trzech informacji
- nazwa [name] nadana wątku
- odwołanie [dao] do warstwy [dao]. Należy zauważyć, że po raz kolejny mamy do czynienia z typem interfejsu [IDao], a nie z typem implementacji [DaoImpl].
- identyfikator [id] osoby, nad którą wątek ma pracować
Gdy [test4] uruchamia wątek [ThreadDaoMajEnfants] (wiersz 12 pliku test4), wykonywana jest metoda [run] (wiersz 25) tego wątku:
- wiersze 78–81: prywatna metoda [suivi] umożliwia generowanie logów ekranowych. Metoda [run] korzysta z niej, aby umożliwić śledzenie przebiegu wątku podczas jego wykonywania.
- wątek będzie próbował zwiększyć o 1 liczbę potomków osoby P o identyfikatorze [id]. Ta aktualizacja może wymagać kilku prób. Weźmy dwa wątki: [TH1] i [TH2]. [TH1] żąda kopii osoby P od warstwy [dao]. Otrzymuje ją i stwierdza, że ma ona wersję V1. [TH1] zostaje przerwany. [TH2], który był następny w kolejności, wykonuje tę samą czynność i otrzymuje tę samą wersję V1 osoby P. [TH2] zostaje przerwany. [TH2] przejmuje kontrolę, zwiększa liczbę potomków P i zapisuje swoje zmiany. Wiemy, że w tym momencie zmiany te są zapisane, a wersja P przejdzie do V2. [TH1] zakończył swoją pracę. [TH2] przejmuje kontrolę i postępuje analogicznie. Jego aktualizacja P zostanie odrzucona, ponieważ posiada kopię P o wersji V1, podczas gdy oryginalny P ma obecnie wersję V2. [TH2] musi zatem powtórzyć cały cykl [lecture -> mise à jour -> sauvegarde]. Dlatego właśnie w wierszach 32–72 występuje pętla. W jej ramach wątek:
- żąda kopii osoby P do modyfikacji (wiersz 34)
- czeka 10 ms (wiersz 43). Jest to sztuczny zabieg mający na celu przerwanie działania wątku pomiędzy odczytaniem osoby P a jej faktyczną aktualizacją na liście osób, aby zwiększyć prawdopodobieństwo wystąpienia konfliktów.
- zwiększa liczbę dzieci osoby P (wiersz 54) i zapisuje osobę P (wiersz 56). Jeśli wątek nie posiada prawidłowej wersji osoby P, warstwa [dao] wygeneruje wyjątek. Następnie pobierany jest kod wyjątku (wiersz 61) w celu sprawdzenia, czy jest to rzeczywiście kod 3 (nieprawidłowa wersja P). Jeśli tak nie jest, wyjątek jest ponownie zgłaszany do metody wywołującej, czyli ostatecznie do metody testowej [test4]. Jeśli wystąpi wyjątek o kodzie 3, wówczas cykl rozpoczyna się od nowa od metody [lecture -> mise à jour -> sauvegarde]. Jeśli nie wystąpi wyjątek, oznacza to, że aktualizacja została przeprowadzona, a praca wątku została zakończona.
Jakie są wyniki testów?
W pierwszej testowanej konfiguracji:
- w metodzie [saveOne] z [DaoImpl] (wiersz 83, paragraf 14.4) wykomentowano instrukcję oczekiwania.
- metoda [test4] tworzy 100 wątków (wiersz 8, punkt 14.5).
Otrzymujemy następujące wyniki:

Wszystkie pięć testów zakończyło się powodzeniem.
W drugiej testowanej konfiguracji:
- usuwamy komentarz z instrukcji oczekiwania w metodzie [saveOne] z pliku [DaoImpl] (wiersz 83, paragraf 14.4).
- metoda [test4] tworzy 2 wątki (wiersz 8, punkt 14.5).
Otrzymujemy następujące wyniki:
![]() | ![]() |
Test [test4] zakończył się niepowodzeniem. Utworzono dwa wątki, z których każdy miał za zadanie zwiększyć o 1 liczbę dzieci osoby P, która początkowo miała ich 0. Oczekiwano zatem, że po wykonaniu obu wątków liczba dzieci wyniesie 2, jednak jest tylko jedno.
Przeanalizujmy logi ekranowe [test4], aby zrozumieć, co się stało:
- wiersz 1: wątek nr 0 rozpoczyna pracę
- wiersz 2: pobrał kopię osoby P i stwierdził, że liczba jej dzieci wynosi 0
- wiersz 3: napotyka [Thread.sleep(10)] swojej metody [run] i zatrzymuje się zatem w momencie [1145536368171] (ms)
- wiersz 4: wątek nr 1 przejmuje następnie procesor i rozpoczyna swoją pracę
- wiersz 5: pobrał kopię osoby P i stwierdza, że liczba jej dzieci wynosi 0
- wiersz 6: napotyka punkt [Thread.sleep(10)] swojej metody [run] i w związku z tym zatrzymuje się
- wiersz 7: wątek nr 0 przejmuje procesor w momencie [1145536368187] (ms), c.a.d. 16 ms po utracie kontroli nad procesorem.
- wiersz 8: to samo dotyczy wątku nr 1
- wiersz 9: wątek nr 0 dokonał aktualizacji i zmienił liczbę potomków na 1
- wiersz 10: wątek nr 1 postąpił tak samo
Pytanie brzmi: dlaczego wątek nr 1 mógł dokonać aktualizacji, skoro normalnie nie posiadał już prawidłowej wersji osoby P, która została właśnie zaktualizowana przez wątek nr 0?
Po pierwsze, można zauważyć nieprawidłowość między wierszami 7 i 8: wydaje się, że wątek nr 0 stracił dostęp do procesora między tymi dwoma wierszami na rzecz wątku nr 1. Co robił w tym momencie? Wykonuje metodę [saveOne] z warstwy [dao]. Ma ona następujący szkielet (patrz paragraf 14.4):
- wątek nr 0 wykonał metodę [saveOne] i dotarł do linii 8, gdzie musiał zwolnić procesor. W międzyczasie odczytał wersję osoby P, która wynosiła 1, ponieważ osoba P nie została jeszcze zaktualizowana.
- Gdy procesor stał się wolny, przejął go wątek nr 1. Ten z kolei uruchomił program [saveOne] i dotarł do linii 8, gdzie musiał zwolnić procesor. W międzyczasie odczytał wersję osoby P i wynosiła ona 1, ponieważ osoba P nadal nie została zaktualizowana.
- Ponieważ procesor stał się wolny, przejął go wątek nr 0. Począwszy od linii 9, dokonał aktualizacji i zmienił liczbę potomków na 1. Następnie metoda [run] wątku nr 0 zakończyła się, a wątek wyświetlił wpis w dzienniku informujący, że zmienił liczbę potomków na 1 (linia 9).
- Ponieważ procesor stał się wolny, przejął go wątek nr 1. Począwszy od linii 9, dokonał on aktualizacji i zmienił liczbę potomków na 1. Dlaczego 1? Ponieważ posiada kopię P z liczbą potomków równą 0. Tak wynika z logu (linia 5). Następnie metoda [run] wątku nr 1 zakończyła się, a wątek wyświetlił wpis w dzienniku informujący, że zmienił liczbę dzieci na 1 (wiersz 10).
Skąd bierze się problem? Wynika on z faktu, że wątek nr 0 nie zdążył zatwierdzić swojej modyfikacji, a tym samym zmienić wersji osoby P, zanim wątek nr 1 próbował odczytać tę wersję, aby sprawdzić, czy osoba P uległa zmianie. Taki scenariusz jest mało prawdopodobny, ale nie niemożliwy. Konieczne było wymuszenie utraty dostępu do procesora przez wątek nr 0, aby wywołać ten błąd przy zaledwie dwóch wątkach. Bez tego zabiegu poprzednia konfiguracja nie zdołała wywołać tego samego przypadku przy 100 wątkach. Test [test4] zakończył się powodzeniem.
Jakie jest rozwiązanie? Z pewnością istnieje kilka. Jednym z nich, łatwym do wdrożenia, jest zsynchronizowanie metody [saveOne]:
public synchronized void saveOne(Personne personne)
Słowo kluczowe [synchronized] gwarantuje, że w danym momencie tylko jeden wątek może wykonywać tę metodę. W ten sposób wątek nr 1 będzie mógł wykonać metodę [saveOne] dopiero wtedy, gdy wątek nr 0 zakończy jej wykonywanie. Mamy zatem pewność, że wersja osoby P zostanie zmieniona, gdy wątek nr 1 wejdzie do metody [saveOne]. Jego aktualizacja zostanie wówczas odrzucona, ponieważ nie będzie posiadał prawidłowej wersji P.
To właśnie te cztery metody warstwy [dao] należałoby zsynchronizować. Postanawiamy jednak pozostawić tę warstwę w opisanej postaci i przenieść synchronizację na warstwę [service]. Wynika to z kilku powodów:
- zakładamy, że dostęp do warstwy [dao] odbywa się zawsze poprzez warstwę [service]. Tak jest w przypadku naszej aplikacji internetowej.
- może zaistnieć konieczność zsynchronizowania również dostępu do metod warstwy [service] z powodów innych niż te, które skłoniłyby nas do zsynchronizowania metod warstwy [dao]. W takim przypadku nie ma potrzeby synchronizowania metod warstwy [dao]. Jeśli mamy pewność, że:
- każdy dostęp do warstwy [dao] odbywa się poprzez warstwę [service]
- i że warstwę [service] wykorzystuje w danym momencie tylko jeden wątek
to mamy pewność, że metody warstwy [dao] nie będą wykonywane przez dwa wątki jednocześnie.
Teraz przechodzimy do warstwy [service].
14.6. Warstwa [service]
Warstwa [service] składa się z następujących klas i interfejsów:
![]()
- [IService] to interfejs udostępniany przez warstwę [dao]
- [ServiceImpl] jest jej implementacją
Interfejs [IService] ma następującą postać:
Jest on identyczny z interfejsem [IDao].
Implementacja [ServiceImpl] interfejsu [IService] wygląda następująco:
- wiersze 10–19: atrybut [IDao dao] jest odwołaniem do warstwy [dao]. Zostanie on zainicjowany przez Spring IoC.
- wiersze 22–24: implementacja metody [getAll] z interfejsu [IService]. Metoda ta ogranicza się do przekazania żądania do warstwy [dao].
- wiersze 27–29: implementacja metody [getOne] z interfejsu [IService]. Metoda ta ogranicza się do przekazania żądania do warstwy [dao].
- wiersze 32–34: implementacja metody [saveOne] z interfejsu [IService]. Metoda ta ogranicza się do przekazania żądania do warstwy [dao].
- wiersze 37–39: implementacja metody [deleteOne] z interfejsu [IService]. Metoda ta ogranicza się do przekazania żądania do warstwy [dao].
- Wszystkie metody są zsynchronizowane (słowo kluczowe `synchronized`), co gwarantuje, że tylko jeden wątek na raz będzie mógł korzystać z warstwy [service], a tym samym z warstwy [dao].
14.7. Testy warstwy [service]
Dla warstwy [service] napisano test JUnit:
![]() | ![]() |
[TestService] to test JUnit. Przeprowadzone testy są dokładnie takie same jak te wykonane dla warstwy [dao]. Schemat testu [TestService] wygląda następująco:
- wiersz 9: testowana warstwa [service] typu [ServiceImpl].
- wiersze 11–15: konstruktor testu JUnit tworzy instancję warstwy [service], która ma zostać przetestowana (wiersz 12), tworzy instancję warstwy [dao] (wiersz 13) oraz nakazuje warstwie [service], aby korzystała z tej warstwy [dao] (wiersz 14).
Metoda [test1] testuje cztery metody interfejsu [IService] w taki sam sposób, jak metoda testowa warstwy [dao] o tej samej nazwie. Różnica polega jedynie na tym, że zamiast warstwy [dao] wywoływana jest warstwa [service] (wiersze 25, 32, 35).
Metoda [test4] ma na celu wykrycie problemów związanych z równoczesnym dostępem do metod warstwy [service]. Jest ona, podobnie jak w poprzednim przypadku, identyczna z metodą testową [test4] warstwy [dao]. Istnieje jednak kilka różnic:
- odwołujemy się do warstwy [service] zamiast do warstwy [dao] (wiersz 55)
- do wątków przekazywane jest odwołanie do warstwy [service] zamiast do warstwy [dao] (wiersz 61)
Typ [ThreadServiceMajEnfants] jest również niemal identyczny z typem [ThreadDaoMajEnfants], z tą różnicą, że współpracuje z warstwą [service], a nie z warstwą [dao]:
- wiersz 12: wątek współpracuje z warstwą [service]
Przeprowadzamy testy z konfiguracją, która spowodowała problem w warstwie [dao]:
- usuwamy komentarz z instrukcji oczekiwania w metodzie [saveOne] klasy [DaoImpl] (wiersz 83, paragraf 14.4).
- metoda [test4] tworzy 100 wątków (wiersz 65, punkt 14.7).
Uzyskane wyniki są następujące:
![]() |
To właśnie synchronizacja metod warstwy [service] umożliwiła pomyślne przeprowadzenie testu [test4].
14.8. Warstwa [web]
Przypomnijmy sobie trójwarstwową architekturę naszej aplikacji:
![]() |
Warstwa [web] udostępni użytkownikowi ekrany umożliwiające zarządzanie grupą osób:
- lista osób w grupie
- dodawanie osoby do grupy
- zmiana danych osoby w grupie
- usunięcie osoby z grupy
W tym celu będzie ona korzystać z warstwy [service], która z kolei będzie korzystać z warstwy [dao]. Ekrany obsługiwane przez warstwę [web] zostały już omówione (punkt 14.1). Aby opisać warstwę internetową, przedstawimy kolejno:
- jej konfigurację
- jej widoki
- jej kontroler
- kilka testów
14.8.1. Konfiguracja aplikacji internetowej
Projekt Eclipse aplikacji wygląda następująco:

- w pakiecie [istia.st.mvc.personnes.web] znajduje się kontroler [Application].
- Strony JSP / JSTL znajdują się w [WEB-INF/vues].
- Folder [lib] zawiera archiwa stron trzecich niezbędne dla działania aplikacji. Są one widoczne w folderze [Web App Libraries].
[web.xml]
Plik [web.xml] jest plikiem wykorzystywanym przez serwer WWW do ładowania aplikacji. Jego zawartość jest następująca:
<?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>
<!-- Mapowanie ServletPersonne-->
<servlet-mapping>
<servlet-name>personnes</servlet-name>
<url-pattern>/do/*</url-pattern>
</servlet-mapping>
<!-- pliki strony głównej -->
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<!-- Strona nieoczekiwanego błędu -->
<error-page>
<exception-type>java.lang.Exception</exception-type>
<location>/WEB-INF/vues/exception.jsp</location>
</error-page>
</web-app>
- wiersze 27–30: adresy URL [/do/*] będą obsługiwane przez serwlet [personnes]
- wiersze 9–12: serwlet [personnes] jest instancją klasy [Application], którą zamierzamy stworzyć.
- wiersze 13–24: definiują trzy parametry [urlList, urlEdit, urlErreurs] identyfikujące adresy URL stron JSP widoków [list, edit, erreurs].
- wiersze 32–34: aplikacja posiada domyślną stronę startową [index.jsp], która znajduje się w katalogu głównym aplikacji internetowej.
- wiersze 36–39: aplikacja posiada domyślną stronę błędów, która jest wyświetlana, gdy serwer WWW napotka wyjątek nieobsługiwany przez aplikację.
- wiersz 37: tag <exception-type> określa typ wyjątku obsługiwany przez dyrektywę <error-page>; w tym przypadku jest to typ [java.lang.Exception] i jego pochodne, a więc wszystkie wyjątki.
- wiersz 38: tag <location> wskazuje stronę JSP, która ma zostać wyświetlona w przypadku wystąpienia wyjątku typu zdefiniowanego przez <exception-type>. Wystąpiony wyjątek jest dostępny na tej stronie w obiekcie o nazwie exception, jeśli strona zawiera dyrektywę:
<%@ page isErrorPage="true" %>
- (ciąg dalszy)
- jeśli <exception-type> określa typ T1, a wyjątek typu T2, który nie jest pochodną typu T1, zostanie zgłoszony do serwera WWW, serwer ten wysyła do klienta własną stronę wyjątku, która zazwyczaj jest mało przyjazna dla użytkownika. Stąd znaczenie tagu <error-page> w pliku [web.xml].
[index.jsp]
Strona ta jest wyświetlana, gdy użytkownik bezpośrednio żąda kontekstu aplikacji bez podania adresu URL, c.a.d. Tutaj [/personnes-01]. Jej zawartość jest następująca:
<%@ 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] przekierowuje klienta na adres URL [/do/list]. Ten adres URL wyświetla listę osób z grupy.
14.8.2. Strony JSP / JSTL w aplikacji
Widok [list.jsp]
Służy do wyświetlania listy osób:

Jej kod wygląda następująco:
<%@ 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>
- Ten widok otrzymuje w swoim modelu element:
- element [personnes] powiązany z obiektem typu [ArrayList], zawierającym obiekty typu [Personne]
- wiersze 22–34: przeglądamy listę ${personnes}, aby wyświetlić tabelę HTML zawierającą osoby z grupy.
- wiersz 31: adres URL, do którego prowadzi link [Modifier], jest ustalany na podstawie pola [id] bieżącej osoby, aby kontroler powiązany z adresem URL [/do/edit] wiedział, którą osobę należy zmodyfikować.
- wiersz 32: to samo dotyczy linku [Supprimer].
- wiersz 28: aby wyświetlić datę urodzenia osoby w formacie JJ/MM/AAAA, używa się tagu <dt> z biblioteki tagów [DateTime] projektu Apache [Jakarta Taglibs]:

Plik opisu tej biblioteki tagów jest zdefiniowany w wierszu 3.
- Wiersz 37: link [Ajout] służący do dodania nowej osoby ma jako cel adres URL [/do/edit], podobnie jak link [Modifier] z wiersza 31. To właśnie wartość -1 parametru [id] wskazuje, że mamy do czynienia z dodaniem, a nie z modyfikacją.
Widok [edit.jsp]
Służy do wyświetlenia formularza dodawania nowej osoby lub modyfikacji istniejącej osoby:
![]() |
Kod widoku [edit.jsp] jest następujący:
<%@ 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>
Ten widok zawiera formularz służący do dodania nowej osoby lub aktualizacji istniejącej osoby. W dalszej części, w celu uproszczenia zapisu, będziemy używać wyłącznie terminu [mise à jour]. Przycisk [Valider] (wiersz 73) powoduje wywołanie widoku POST formularza pod adresem URL [/do/validate] (wiersz 16). Jeśli operacja POST zakończy się niepowodzeniem, ponownie wyświetlany jest widok [edit.jsp] wraz z wystąpionym błędem lub błędami; w przeciwnym razie wyświetlany jest widok [list.jsp].
- Widok [edit.jsp], wyświetlany zarówno w przypadku nieudanego widoku GET, jak i POST, otrzymuje w swoim szablonie następujące elementy:
atrybut | GET | POST |
identyfikator zaktualizowanej osoby | to samo | |
jej wersja | tak samo | |
imię | wpisane imię | |
jego nazwisko | wpisane nazwisko | |
data urodzenia | wprowadzona data urodzenia | |
stan cywilny | wprowadzony stan cywilny | |
liczba dzieci | wprowadzona liczba dzieci | |
puste | komunikat o błędzie sygnalizujący niepowodzenie dodania lub modyfikacji w momencie wykonania POST, spowodowane naciśnięciem przycisku [Envoyer]. Puste, jeśli nie wystąpił błąd. | |
puste | sygnalizuje błędne imię – w przeciwnym razie puste | |
puste | oznacza błędne nazwisko – w przeciwnym razie puste | |
puste | oznacza błędną datę urodzenia – w przeciwnym razie puste | |
puste | oznacza błędną liczbę dzieci – w przeciwnym razie pozostaje puste |
- wiersze 11–15: jeśli kod POST z formularza nie zostanie poprawnie przetworzony, pojawi się kod [erreurEdit!=''] i zostanie wyświetlony komunikat o błędzie.
- wiersz 16: formularz zostanie wysłany pod adres URL [/do/validate]
- wiersz 20: wyświetlany jest element szablonu o nazwie [id]
- wiersz 24: wyświetlany jest element szablonu o nazwie [version]
- wiersze 26–32: wprowadzenie imienia osoby:
- podczas pierwszego wyświetlenia formularza (GET), ${imię} wyświetla aktualną wartość pola [prenom] z zaktualizowanego obiektu [Personne], a ${erreurPrenom} jest puste.
- w przypadku błędu po POST ponownie wyświetlana jest wprowadzona wartość ${prenom} oraz ewentualny komunikat o błędzie ${erreurPrenom}
- wiersze 33–39: wprowadzenie nazwiska osoby
- wiersze 40–46: wprowadzanie daty urodzenia osoby
- wiersze 47–61: wprowadzanie informacji o stanie cywilnym osoby za pomocą przycisku opcji. Wykorzystuje się wartość pola [marie] obiektu [Personne], aby określić, który z dwóch przycisków opcji należy zaznaczyć.
- wiersze 62–68: wprowadzenie liczby dzieci danej osoby
- wiersz 71: ukryte pole HTML o nazwie [id], którego wartością jest pole [id] aktualizowanej osoby; -1 oznacza dodanie, inna wartość oznacza modyfikację.
- wiersz 72: ukryte pole HTML o nazwie [version], którego wartość odpowiada wartości pola [id] aktualizowanej osoby.
- wiersz 73: przycisk [Valider] typu [Submit] w formularzu
- wiersz 74: link umożliwiający powrót do listy osób. Został on nazwany [Annuler], ponieważ pozwala opuścić formularz bez jego zatwierdzania.
Widok [exception.jsp]
Służy do wyświetlenia strony informującej, że wystąpił wyjątek nieobsługiwany przez aplikację, który został zgłoszony do serwera WWW.
Na przykład usuńmy osobę, która nie istnieje w grupie:
![]() |
Kod widoku [exception.jsp] jest następujący:
<%@ 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>
- widok ten otrzymuje w swoim szablonie element o kluczu [exception], który stanowi wyjątek przechwycony przez serwer WWW. Aby serwer WWW uwzględnił ten element w szablonie strony JSP, strona musi zawierać tag z linii 3.
- wiersz 6: ustawia się kod stanu odpowiedzi HTTP na wartość 200. Jest to pierwszy nagłówek odpowiedzi HTTP. Kod 200 oznacza dla klienta, że jego żądanie zostało zrealizowane. Zazwyczaj do odpowiedzi serwera dołączony jest dokument o kodzie HTML. Tak jest również w tym przypadku. Jeśli kod stanu odpowiedzi HTTP nie zostanie ustawiony na 200, przyjmie on w tym przypadku wartość 500, co oznacza, że wystąpił błąd. Serwer WWW, który przechwycił nieobsługiwany wyjątek, uznaje tę sytuację za nieprawidłową i sygnalizuje ją kodem 500. Reakcja na kod HTTP 500 różni się w zależności od przeglądarki: Firefox wyświetla dokument HTML, który może towarzyszyć tej odpowiedzi, podczas gdy IE ignoruje ten dokument i wyświetla własną stronę. Z tego powodu zastąpiliśmy kod 500 kodem 200.
- wiersz 16: wyświetlany jest tekst wyjątku
- wiersz 18: użytkownikowi proponowany jest link umożliwiający powrót do listy osób
Widok [erreurs.jsp]
Służy do wyświetlania strony informującej o błędach inicjalizacji aplikacji, c.a.d, oraz o błędach wykrytych podczas wykonywania metody [init] serwletu kontrolera. Może to być na przykład brak parametru w pliku [web.xml], jak pokazano w poniższym przykładzie:

Kod strony [erreurs.jsp] wygląda następująco:
<%@ 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>
Strona otrzymuje w swoim szablonie element [erreurs], który jest obiektem typu [ArrayList] zawierającym obiekty [String], przy czym te ostatnie są komunikatami o błędach. Są one wyświetlane w pętli w wierszach 13–15.
14.8.3. Kontroler aplikacji
Kontroler [Application] jest zdefiniowany w pakiecie [istia.st.mvc.personnes.web]:
![]()
Struc tura i inicjalizacja kontrolera
Szkielet kontrolera [Application] wygląda następująco:
- wiersze 20–36: pobierane są oczekiwane parametry z pliku [web.xml].
- wiersze 39–41: parametr [urlErreurs] musi być obowiązkowo obecny, ponieważ określa adres URL widoku [erreurs], który może wyświetlać ewentualne błędy inicjalizacji. Jeśli nie istnieje, aplikacja zostaje przerwana poprzez uruchomienie widoku [ServletException] (wiersz 40). Wyjątek ten zostanie przekazany do serwera WWW i obsłużony przez tag <error-page> w pliku [web.xml]. Wyświetlany jest zatem widok [exception.jsp]:

Powyższy link [Retour à la liste] nie działa. Jego użycie zwraca tę samą odpowiedź, dopóki aplikacja nie zostanie zmodyfikowana i ponownie załadowana. Jest on przydatny w przypadku innych rodzajów wyjątków, jak już widzieliśmy.
- wiersz 43: tworzy instancję [DaoImpl] implementującą warstwę [dao]
- wiersz 44: inicjuje tę instancję (tworzenie początkowej listy trzech osób)
- wiersz 46: tworzy instancję [ServiceImpl] implementującą warstwę [service]
- wiersz 47: inicjuje warstwę [service], nadając jej odwołanie do warstwy [dao]
Po zainicjowaniu kontrolera jego metody dysponują odwołaniem [service] do warstwy [service] (wiersz 15), którego będą używać do wykonywania akcji żądanych przez użytkownika. Działania te zostaną przechwycone przez metodę [doGet], która przekaże je do przetworzenia przez konkretną metodę kontrolera:
Url | Metoda HTTP | metoda kontrolera |
GET | doListPersonnes | |
GET | doEditPersonne | |
POST | doValidatePersonne | |
GET | doDeletePersonne |
Metoda [doGet]
Celem tej metody jest skierowanie przetwarzania akcji żądanych przez użytkownika do właściwej metody. Jej kod jest następujący:
- wiersze 7–13: sprawdzamy, czy lista błędów inicjalizacji jest pusta. Jeśli tak nie jest, wyświetlamy widok [erreurs(erreurs)], który zgłosi błąd lub błędy.
- wiersz 15: pobieramy metodę [get] lub [post], której klient użył do wysłania żądania.
- wiersz 17: pobieramy wartość parametru [action] z zapytania.
- wiersze 23–27: przetwarzanie żądania [GET /do/list], które dotyczy listy osób.
- wiersze 28–32: przetwarzanie żądania [GET /do/delete], które dotyczy usunięcia osoby.
- wiersze 33–37: przetwarzanie zapytania [GET /do/edit], które dotyczy formularza aktualizacji danych osoby.
- wiersze 38–42: przetwarzanie żądania [POST /do/validate], które dotyczy zatwierdzenia zaktualizowanych danych osoby.
- wiersz 44: jeśli żądana akcja nie jest jedną z pięciu poprzednich, to traktuje się ją tak, jakby była to akcja [GET /do/list].
Metoda [doListPersonnes]
Metoda ta przetwarza żądanie [GET /do/list], które dotyczy listy osób:

Jej kod wygląda następująco:
- wiersz 5: żądamy od warstwy [service] listy osób z grupy i umieszczamy ją w modelu pod kluczem „osoby”.
- wiersz 7: wyświetla się widok [list.jsp] opisany w paragrafie 14.8.2.
Metoda [doDeletePersonne]
Metoda ta przetwarza zapytanie [GET /do/delete?id=XX], które żąda usunięcia osoby o identyfikatorze id=XX. Adres URL [/do/delete?id=XX] jest adresem linków [Supprimer] w widoku [list.jsp]:

którego kod wygląda następująco:
W wierszu 12 widoczny jest adres URL [/do/delete?id=XX] linku [Supprimer]. Metoda [doDeletePersonne], która ma przetwarzać ten adres URL, musi usunąć osobę o identyfikatorze id=XX, a następnie wyświetlić nową listę osób z grupy. Jej kod jest następujący:
- wiersz 5: przetwarzany adres URL ma postać [/do/delete?id=XX]. Pobieramy wartość [XX] z parametru [id].
- wiersz 7: wysyłamy do warstwy [service] żądanie usunięcia osoby o uzyskanym identyfikatorze. Nie przeprowadzamy żadnej weryfikacji. Jeśli osoba, którą próbujemy usunąć, nie istnieje, warstwa [dao] zgłasza wyjątek, który jest przekazywany dalej przez warstwę [service]. Nie obsługujemy tego również tutaj, w kontrolerze. Wyjątek zostanie zatem przekazany dalej do serwera WWW, który zgodnie z konfiguracją wyświetli stronę [exception.jsp], opisaną w paragrafie 14.8.2:

- wiersz 9: jeśli usunięcie doszło do skutku (nie wystąpił wyjątek), klient jest przekierowywany do względnego adresu URL [list]. Ponieważ właśnie przetworzono stronę [/do/delete], adres URL przekierowania będzie brzmiał [/do/list]. Przeglądarka zostanie zatem przekierowana do strony [GET /do/list], co spowoduje wyświetlenie listy osób.
Metoda [doEditPersonne]
Ta metoda obsługuje żądanie [GET /do/edit?id=XX], które dotyczy formularza aktualizacji osoby o identyfikatorze id=XX. Adres URL [/do/edit?id=XX] jest adresem linków [Modifier] oraz linku [Ajout] w widoku [list.jsp]:

którego kod wygląda następująco:
W wierszu 11 widoczny jest adres URL [/do/edit?id=XX] linku [Modifier], a w wierszu 17 adres URL [/do/edit?id=-1] linku [Ajout]. Metoda [doEditPersonne] powinna wyświetlić formularz edycji osoby o identyfikatorze id=XX lub, w przypadku dodawania nowego wpisu, wyświetlić pusty formularz.
![]() | ![]() |
Kod metody [doEditPersonne] jest następujący:
- GET ma jako cel adres URL typu [/do/edit?id=XX]. W wierszu 5 pobieramy wartość z [id]. Następnie występują dwa przypadki:
- id jest różne od -1. W takim przypadku mamy do czynienia z modyfikacją i należy wyświetlić formularz wstępnie wypełniony danymi osoby, której dane mają zostać zmodyfikowane. W wierszu 10 dana osoba jest pobierana z warstwy [service].
- id jest równe -1. W takim przypadku mamy do czynienia z dodaniem i należy wyświetlić pusty formularz. W tym celu w wierszach 13–14 tworzona jest pusta osoba.
- Uzyskany obiekt [Personne] jest umieszczany w szablonie strony [edit.jsp] opisanym w paragrafie 14.8.2. Szablon ten zawiera następujące elementy: [erreurEdit, id, version, prenom, erreurPrenom, nom, erreurNom, dateNaissance, erreurDateNaissance, marie, nbEnfants, erreurNbEnfants]. Elementy te są inicjowane w wierszach 17–30, z wyjątkiem tych, których wartością jest pusty ciąg znaków [erreurPrenom, erreurNom, erreurDateNaissance, erreurNbEnfants]. Wiadomo, że w przypadku ich braku w szablonie biblioteka JSTL wyświetli pusty ciąg znaków jako ich wartość. Chociaż element [erreurEdit] również ma za wartość pusty ciąg znaków, jest on jednak inicjowany, ponieważ na stronie [edit.jsp] przeprowadzany jest test jego wartości.
- Gdy szablon jest gotowy, kontrola przechodzi do strony [edit.jsp], wiersze 32–33, która wygeneruje widok [edit].
Metoda [doValidatePersonne]
Ta metoda obsługuje żądanie [POST /do/validate], które waliduje formularz aktualizacji. Żądanie POST jest wywoływane przez przycisk [Valider]:

Przypomnijmy elementy wprowadzania danych w formularzu HTML z powyższego widoku:
Zapytanie POST zawiera parametry [prenom, nom, dateNaissance, marie, nbEnfants, id, version] i jest wysyłane na adres URL [/do/validate] (wiersz 1). Jest ono przetwarzane przez następującą metodę [doValidatePersonne]:
- wiersze 8–14: pobierany jest parametr [prenom] z żądania POST i sprawdzana jest jego poprawność. Jeśli okaże się nieprawidłowy, element [erreurPrenom] jest inicjowany komunikatem o błędzie i umieszczany w atrybutach zapytania.
- wiersze 16–22: podobną procedurę stosuje się w przypadku parametru [nom]
- wiersze 24–32: podobną procedurę stosuje się w przypadku parametru [dateNaissance]
- wiersz 34: pobierany jest parametr [marie]. Nie sprawdzamy jego poprawności, ponieważ a priori pochodzi on z wartości przycisku opcji. Nie oznacza to jednak, że program nie może wygenerować parametru [POST /personnes-01/do/validate] wraz z fikcyjnym parametrem [marie]. Powinniśmy zatem sprawdzić poprawność tego parametru. W tym przypadku polegamy na naszej obsłudze wyjątków, która powoduje wyświetlenie strony o wartości [exception.jsp], jeśli sam kontroler nie obsługuje tych wyjątków. Jeśli więc konwersja parametru [marie] na wartość logiczną nie powiedzie się w wierszu 34, zostanie zgłoszony wyjątek, który spowoduje wysłanie strony [exception.jsp] do klienta. Takie działanie nam odpowiada.
- wiersze 34–54: pobieramy parametr [nbEnfants] i sprawdzamy jego wartość.
- wiersz 56: pobieramy parametr [id] bez sprawdzania jego wartości
- wiersz 58: postępujemy analogicznie w przypadku parametru [version]
- wiersze 60–65: jeśli formularz zawiera błędy, jest on ponownie wyświetlany wraz z wcześniej wygenerowanymi komunikatami o błędach
- wiersze 67–69: jeśli formularz jest poprawny, tworzy się nowy obiekt [Personne] z elementami formularza
- wiersze 70–78: osoba zostaje zapisana. Zapisanie może się nie powieść. W środowisku wieloużytkownikowym osoba, którą należy zmodyfikować, mogła zostać usunięta lub już zmodyfikowana przez kogoś innego. W takim przypadku warstwa [dao] zgłosi wyjątek, który jest tutaj obsługiwany.
- wiersz 80: jeśli nie wystąpił wyjątek, przekierowujemy klienta na adres URL [/do/list], aby wyświetlić mu nowy stan grupy.
- wiersz 75: jeśli podczas zapisywania wystąpił wyjątek, ponownie wyświetlamy początkowy formularz, przekazując mu komunikat o błędzie wyjątku (trzeci parametr).
Metoda [showFormulaire] (wiersze 84–101) tworzy szablon niezbędny dla strony [edit.jsp] na podstawie wprowadzonych wartości (request.getParameter(" ... ")). Należy pamiętać, że komunikaty o błędach zostały już umieszczone w szablonie przez metodę [doValidatePersonne]. Strona [edit.jsp] jest wyświetlana w wierszach 99–100.
14.9. Testy aplikacji internetowej
W rozdziale 14.1 przedstawiono szereg testów. Zachęcamy czytelnika do ich powtórzenia. Poniżej przedstawiamy kolejne zrzuty ekranu ilustrujące przypadki konfliktów dostępu do danych w środowisku wieloużytkownikowym:
[Firefox] będzie przeglądarką użytkownika U1. Użytkownik ten wysyła żądanie do adresu URL [http://localhost:8080/personnes-01]:

[IE] będzie przeglądarką użytkownika U2. Użytkownik ten wysyła żądanie na ten sam adres URL:

Użytkownik U1 przechodzi do edycji danych osoby [Lemarchand]:

Użytkownik U2 postępuje analogicznie:

Użytkownik U1 wprowadza zmiany i zatwierdza je:
![]() |
Użytkownik U2 postępuje tak samo:
![]() |
Użytkownik U2 wraca do listy osób, korzystając z linku [Annuler] w formularzu:

Znajduje osobę [Lemarchand] w wersji zmodyfikowanej przez U1. Teraz U2 usuwa [Lemarchand]:
![]() |
U1 nadal posiada własną listę i chce ponownie zmodyfikować [Lemarchand]:
![]() |
U1 korzysta z linku [Retour à la liste], aby sprawdzić, o co chodzi:

Odkrywa, że rzeczywiście [Lemarchand] nie znajduje się już na liście...
14.10. Conclusion
Wdrożyliśmy architekturę MVC w ramach trójwarstwowej architektury [web, metier, dao] na przykładzie prostego zarządzania listą osób. Pozwoliło nam to wykorzystać koncepcje przedstawione w poprzednich sekcjach. W analizowanej wersji lista osób była przechowywana w pamięci. Wkrótce zajmiemy się wersjami, w których lista ta będzie przechowywana w tabeli bazy danych.
Zanim jednak to zrobimy, przedstawimy narzędzie o nazwie Spring IoC, które ułatwia integrację różnych warstw aplikacji ntier.

















