Skip to content

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:

Image

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.

Image

Image

Image

 

14.2. Projekt Eclipse

Projekt aplikacji nosi nazwę [personnes-01]:

Image

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]:

Image

Klasa [Personne] ma następujący wygląd:

package istia.st.springmvc.personnes.entites;

import java.text.SimpleDateFormat;
import java.util.Date;

public class Personne {

     // unikalny identyfikator osoby
    private int id;
     // aktualna wersja
    private long version;
     // nazwisko
    private String nom;
     // imię
    private String prenom;
     // data urodzenia
    private Date dateNaissance;
     // stan cywilny
    private boolean marie = false;
     // liczba dzieci
    private int nbEnfants;

     // metody pobierające i ustawiające
...

     // konstruktor domyślny
    public Personne() {

    }

     // konstruktor z inicjalizacją pól osoby
    public Personne(int id, String prenom, String nom, Date dateNaissance,
            boolean marie, int nbEnfants) {
        setId(id);
        setNom(nom);
        setPrenom(prenom);
        setDateNaissance(dateNaissance);
        setMarie(marie);
        setNbEnfants(nbEnfants);
    }

     // konstruktor osoby poprzez skopiowanie innej osoby
    public Personne(Personne p) {
        setId(p.getId());
        setVersion(p.getVersion());
        setNom(p.getNom());
        setPrenom(p.getPrenom());
        setDateNaissance(p.getDateNaissance());
        setMarie(p.getMarie());
        setNbEnfants(p.getNbEnfants());
    }


     // toString
    public String toString() {
        return "[" + id + "," + version + "," + prenom + "," + nom + ","
                + new SimpleDateFormat("dd/MM/yyyy").format(dateNaissance)
                + "," + marie + "," + nbEnfants + "]";
    }
}
  • 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:

Image

  • [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:

package istia.st.springmvc.personnes.dao;

import istia.st.springmvc.personnes.entites.Personne;

import java.util.Collection;

public interface IDao {
     // lista wszystkich osób
    Collection getAll();
     // pobranie konkretnej osoby
    Personne getOne(int id);
     // dodaj/edytuj osobę
    void saveOne(Personne personne);
     // usunięcie osoby
    void deleteOne(int id);
}
  • 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] :

package istia.st.springmvc.personnes.dao;

public class DaoException extends RuntimeException {

     // kod błędu
    private int code;

    public int getCode() {
        return code;
    }

// twórca
    public DaoException(String message,int code) {
        super(message);
        this.code=code;
    }
}
  • 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]:

package istia.st.springmvc.personnes.dao;

import istia.st.springmvc.personnes.entites.Personne;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;

public class DaoImpl implements IDao {

     // lista osób
    private ArrayList personnes = new ArrayList();

     // numer kolejnej osoby
    private int id = 0;

     // inicjalizacje
    public void init() {
        try {
            Personne p1 = new Personne(-1, "Joachim", "Major",
                    new SimpleDateFormat("dd/MM/yyyy").parse("13/11/1984"),
                    true, 2);
            saveOne(p1);
            Personne p2 = new Personne(-1, "Mélanie", "Humbort",
                    new SimpleDateFormat("dd/MM/yyyy").parse("12/02/1985"),
                    false, 1);
            saveOne(p2);
            Personne p3 = new Personne(-1, "Charles", "Lemarchand",
                    new SimpleDateFormat("dd/MM/yyyy").parse("01/03/1986"),
                    false, 0);
            saveOne(p3);
        } catch (ParseException ex) {
            throw new DaoException(
                    "Erreur d'initialisation de la couche [dao] : "
                            + ex.toString(), 1);
        }
    }

     // lista osób
    public Collection getAll() {
        return personnes;
    }

     // pobranie konkretnej osoby
    public Personne getOne(int id) {
         // szukamy osoby
        int i = getPosition(id);
         // czy udało się znaleźć?
        if (i != -1) {
            return new Personne(((Personne) personnes.get(i)));
        } else {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        }
    }

     // dodaj lub zmień osobę
    public void saveOne(Personne personne) {
         // czy parametr „osoba” jest prawidłowy?
        check(personne);
         // dodanie czy zmiana?
        if (personne.getId() == -1) {
             // dodanie
            personne.setId(getNextId());
            personne.setVersion(1);
            personnes.add(personne);
            return;
        }
         // zmiana – szukamy osoby
        int i = getPosition(personne.getId());
         // czy znaleziono?
        if (i == -1) {
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] qu'on veut modifier n'existe pas", 2);
        }
         // czy mamy właściwą wersję oryginału?
        Personne original = (Personne) personnes.get(i);
        if (original.getVersion() != personne.getVersion()) {
            throw new DaoException("L'original de la personne [" + personne
                    + "] a changé depuis sa lecture initiale", 3);
        }
         // czekamy 10 ms
         //wait(10);
         // w porządku – wprowadzamy zmianę
        original.setVersion(original.getVersion()+1);
        original.setNom(personne.getNom());
        original.setPrenom(personne.getPrenom());
        original.setDateNaissance((personne.getDateNaissance()));
        original.setMarie(personne.getMarie());
        original.setNbEnfants(personne.getNbEnfants());
    }

     // usunięcie osoby
    public void deleteOne(int id) {
         // szukamy tej osoby
        int i = getPosition(id);
         // czy znaleźliśmy?
        if (i == -1) {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        } else {
             // usuwamy osobę
            personnes.remove(i);
        }
    }

     // generator identyfikatora
    private int getNextId() {
        id++;
        return id;
    }

     // wyszukiwanie osoby
    private int getPosition(int id) {
        int i = 0;
        boolean trouvé = false;
         // przeglądamy listę osób
        while (i < personnes.size() && !trouvé) {
            if (id == ((Personne) personnes.get(i)).getId()) {
                trouvé = true;
            } else {
                i++;
            }
        }
         // wynik?
        return trouvé ? i : -1;
    }

     // weryfikacja osoby
    private void check(Personne p) {
         // osoba p
        if (p == null) {
            throw new DaoException("Personne null", 10);
        }
         // identyfikator
        if (p.getId() != -1 && p.getId() < 0) {
            throw new DaoException("Id [" + p.getId() + "] invalide", 11);
        }
         // data urodzenia
        if (p.getDateNaissance() == null) {
            throw new DaoException("Date de naissance manquante", 12);
        }
         // liczba dzieci
        if (p.getNbEnfants() < 0) {
            throw new DaoException("Nombre d'enfants [" + p.getNbEnfants()
                    + "] invalide", 13);
        }
         // nazwisko
        if (p.getNom() == null || p.getNom().trim().length() == 0) {
            throw new DaoException("Nom manquant", 14);
        }
         // imię
        if (p.getPrenom() == null || p.getPrenom().trim().length() == 0) {
            throw new DaoException("Prénom manquant", 15);
        }
    }

     // oczekiwanie
    private void wait(int N) {
         // oczekuje się N ms
        try {
            Thread.sleep(N);
        } catch (InterruptedException e) {
             // wyświetla się ślad wyjątku
            e.printStackTrace();
            return;
        }
    }
}

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.

package istia.st.springmvc.personnes.tests;

import java.text.ParseException;
...

public class TestDao extends TestCase {

     // warstwa [dao]
    private DaoImpl dao;

     // konstruktor
    public TestDao() {
        dao = new DaoImpl();
        dao.init();
    }

     // lista osób
    private void doListe(Collection personnes) {
        Iterator iter = personnes.iterator();
        while (iter.hasNext()) {
            System.out.println(iter.next());
        }
    }

     // test1
    public void test1() throws ParseException {
...
    }

     // modyfikacja lub usunięcie nieistniejącego elementu
    public void test2() throws ParseException {
...
    }

     // zarządzanie wersjami osób
    public void test3() throws ParseException, InterruptedException {
...
    }

     // blokowanie optymistyczne – dostęp wielowątkowy
    public void test4() throws Exception {
...
    }

     // testy poprawności saveOne
    public void test5() throws ParseException {
    ...
}
  • 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:

    public void test1() throws ParseException {
         // aktualna lista
        Collection personnes = dao.getAll();
        int nbPersonnes = personnes.size();
         // wyświetlanie
        doListe(personnes);
         // dodanie osoby
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 1);
        dao.saveOne(p1);
        int id1 = p1.getId();
         // weryfikacja – nastąpi awaria, jeśli osoba nie zostanie znaleziona
        p1 = dao.getOne(id1);
        assertEquals("X", p1.getNom());
         // modyfikacja
        p1.setNom("Y");
        dao.saveOne(p1);
         // weryfikacja – nastąpi awaria, jeśli osoba nie zostanie znaleziona
        p1 = dao.getOne(id1);
        assertEquals("Y", p1.getNom());
         // usunięcie
        dao.deleteOne(id1);
         // weryfikacja
        int codeErreur = 0;
        boolean erreur = false;
        try {
            p1 = dao.getOne(id1);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         // powinien pojawić się błąd o kodzie 2
        assertTrue(erreur);
        assertEquals(2, codeErreur);
         // lista osób
        personnes = dao.getAll();
        assertEquals(nbPersonnes, personnes.size());
    }
  • 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:

    public void test4() throws Exception {
         // dodanie osoby
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        dao.saveOne(p1);
        int id1 = p1.getId();
         // utworzenie N wątków aktualizujących liczbę dzieci
        final int N = 10;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadDaoMajEnfants("thread n° " + i, dao, id1);
            taches[i].start();
        }
         // czekamy na zakończenie wątków
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
         // pobieranie osoby
        p1 = dao.getOne(id1);
         // osoba ta musi mieć N dzieci
        assertEquals(N, p1.getNbEnfants());
         // usunięcie osoby p1
        dao.deleteOne(p1.getId());
         // weryfikacja
        boolean erreur = false;
        int codeErreur = 0;
        try {
            p1 = dao.getOne(p1.getId());
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         // powinien wystąpić błąd o kodzie 2
        assertTrue(erreur);
        assertEquals(2, codeErreur);
    }
  • 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:

  1. nazwa nadana wątku, służąca do śledzenia go za pomocą logów
  2. numer referencyjny w warstwie [dao], aby wątek miał do niej dostęp
  3. identyfikator osoby, nad którą wątek ma pracować

Typ [ThreadDaoMajEnfants] ma następujący wygląd:

package istia.st.mvc.personnes.tests;

import java.util.Date;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.personnes.dao.IDao;
import istia.st.mvc.personnes.entites.Personne;

public class ThreadDaoMajEnfants extends Thread {
     // nazwa wątku
    private String name;
     // odwołanie do warstwy [dao]
    private IDao dao;
     // identyfikator osoby, nad którą będziemy pracować
    private int idPersonne;

     // twórca
    public ThreadDaoMajEnfants(String name, IDao dao, int idPersonne) {
        this.name = name;
        this.dao = dao;
        this.idPersonne = idPersonne;
    }

     // rdzeń wątku
    public void run() {
         // śledzenie
        suivi("lancé");
         // pętla trwa, dopóki nie uda się zwiększyć wartości o 1
         // liczba dzieci osoby idPersonne
        boolean fini = false;
        int nbEnfants = 0;
        while (!fini) {
             // pobieramy kopię osoby z idPersonne
            Personne personne = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
             // kontynuacja
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version "+personne.getVersion());
             // oczekiwanie 10 ms przed zwolnieniem procesora
            try {
                 // kontynuacja
                suivi("début attente");
                 // przerywamy działanie, aby zwolnić procesor
                Thread.sleep(10);
                 // kontynuacja
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
             // oczekiwanie zakończone – próba zatwierdzenia kopii
             // w międzyczasie inne wątki mogły zmodyfikować oryginał
            int codeErreur = 0;
            try {
                 // zwiększa o 1 liczbę potomków tej kopii
                personne.setNbEnfants(nbEnfants + 1);
                 // próbujemy zmodyfikować oryginał
                dao.saveOne(personne);
                 // operacja się powiodła – oryginał został zmodyfikowany
                fini = true;
            } catch (DaoException ex) {
                 // odzyskuje się kod błędu
                codeErreur = ex.getCode();
                 // musi to być błąd wersji 3 – w przeciwnym razie należy ponowić próbę
                 // wyjątek
                if (codeErreur != 3) {
                    throw ex;
                } else {
                     // monitorowanie
                    suivi(ex.getMessage());
                }
                 // oryginał uległ zmianie – zaczynamy wszystko od nowa
            }
        }
         // kontynuacja
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }

     // kontynuacja
    private void suivi(String message) {
        System.out
                .println(name + " [" + new Date().getTime()+ "] : " + message);
    }
}
  • wiersz 9: [ThreadDaoMajEnfants] jest rzeczywiście wątkiem
  • wiersze 18–22: konstruktor, który inicjuje wątek przy użyciu trzech informacji
    1. nazwa [name] nadana wątku
    2. 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].
    3. 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.
         // czekamy 10 ms
         //wait(10);
  • metoda [test4] tworzy 100 wątków (wiersz 8, punkt 14.5).
         // utworzenie N wątków aktualizujących liczbę potomków
        final int N = 100;

Otrzymujemy następujące wyniki:

Image

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).
         // oczekiwanie 10 ms
        wait(10);
  • metoda [test4] tworzy 2 wątki (wiersz 8, punkt 14.5).
         // utworzenie N wątków aktualizujących liczbę potomków
        final int N = 2;

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:

thread n° 0 [1145536368171] : lancé
thread n° 0 [1145536368171] : 0 -> 1 pour la version 1
thread n° 0 [1145536368171] : début attente
thread n° 1 [1145536368171] : lancé
thread n° 1 [1145536368171] : 0 -> 1 pour la version 1
thread n° 1 [1145536368171] : début attente
thread n° 0 [1145536368187] : fin attente
thread n° 1 [1145536368187] : fin attente
thread n° 0 [1145536368187] : a terminé et passé le nombre d'enfants à 1
thread n° 1 [1145536368187] : a terminé et passé le nombre d'enfants à 1
  • 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):

    public void saveOne(Personne personne) {
...
         // modyfikacja – szukanie osoby
....
         // czy mamy właściwą wersję oryginału?
...
         // czekamy 10 ms
        wait(10);
         // w porządku – wprowadzamy zmianę
    ...
}
  • 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:

Image

  • [IService] to interfejs udostępniany przez warstwę [dao]
  • [ServiceImpl] jest jej implementacją

Interfejs [IService] ma następującą postać:

package istia.st.springmvc.personnes.service;

import istia.st.springmvc.personnes.entites.Personne;

import java.util.Collection;

public interface IService {
     // lista wszystkich osób
    Collection getAll();
     // pobierz konkretną osobę
    Personne getOne(int id);
     // dodaj/edytuj osobę
    void saveOne(Personne personne);
     // usunięcie osoby
    void deleteOne(int id);
}

Jest on identyczny z interfejsem [IDao].

Implementacja [ServiceImpl] interfejsu [IService] wygląda następująco:

package istia.st.springmvc.personnes.service;

import istia.st.springmvc.personnes.dao.IDao;
import istia.st.springmvc.personnes.entites.Personne;

import java.util.Collection;

public class ServiceImpl implements IService {

     // warstwa [dao]
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

    public void setDao(IDao dao) {
        this.dao = dao;
    }

     // lista osób
    public synchronized Collection getAll() {
        return dao.getAll();
    }

     // wyświetlenie konkretnej osoby
    public synchronized Personne getOne(int id) {
        return dao.getOne(id);
    }

     // dodaj lub edytuj osobę
    public synchronized void saveOne(Personne personne) {
        dao.saveOne(personne);
    }

     // usunięcie osoby
    public synchronized void deleteOne(int id) {
        dao.deleteOne(id);
    }
}
  • 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:

package istia.st.springmvc.personnes.tests;

...

public class TestService extends TestCase {

     // warstwa [service]
    private ServiceImpl service;

     // producent
    public TestService() {
        service = new ServiceImpl();
        DaoImpl dao=new DaoImpl();
        service.setDao(dao);
    }

     // lista osób
    private void doListe(Collection personnes) {
...
    }

     // test1
    public void test1() throws ParseException {
         // aktualna lista
        Collection personnes = service.getAll();
        int nbPersonnes = personnes.size();
         // wyświetlanie
        doListe(personnes);
         // dodanie osoby
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 1);
        service.saveOne(p1);
        int id1 = p1.getId();
         // weryfikacja – nastąpi awaria, jeśli osoba nie zostanie znaleziona
        p1 = service.getOne(id1);
        assertEquals("X", p1.getNom());
...
    }

     // modyfikacja lub usunięcie nieistniejącego elementu
    public void test2() throws ParseException {
...
    }

     // zarządzanie wersjami osoby
    public void test3() throws ParseException, InterruptedException {
...
    }

     // blokowanie optymistyczne – dostęp wielowątkowy
    public void test4() throws Exception {
         // dodanie osoby
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        service.saveOne(p1);
        int id1 = p1.getId();
         // utworzenie N wątków aktualizujących liczbę dzieci
        final int N = 100;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadServiceMajEnfants("thread n° " + i, service,
                    id1);
            taches[i].start();
        }
...
    }

     // testy poprawności saveOne
    public void test5() throws ParseException {
    ...
    }
}
  • 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]:

package istia.st.springmvc.personnes.tests;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.personnes.entites.Personne;
import istia.st.mvc.personnes.service.IService;

public class ThreadServiceMajEnfants extends Thread {

     // nazwa wątku
    private String name;
     // odwołanie do warstwy [service]
    private IService service;
     // identyfikator osoby, nad którą będziemy pracować
    private int idPersonne;

    public ThreadServiceMajEnfants(String name, IService service, int idPersonne) {
        this.name = name;
        this.service = service;
        this.idPersonne = idPersonne;
    }

    public void run() {
...
    }

     // śledzenie
    private void suivi(String message) {
        System.out.println(name + " : " + message);
    }

}
  • 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).
         // oczekujemy 10 ms
        wait(10);
  • metoda [test4] tworzy 100 wątków (wiersz 65, punkt 14.7).
         // utworzenie N wątków aktualizujących liczbę dzieci
        final int N = 100;

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:

Image

  • 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:

Image

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&eacute;nom</th>
                <th>Nom</th>
                <th>Date de naissance</th>
                <th>Mari&eacute;</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]:

Image

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&eacute;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&eacute;</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
id
identyfikator zaktualizowanej osoby
to samo
version
jej wersja
tak samo
prenom
imię
wpisane imię
nom
jego nazwisko
wpisane nazwisko
dateNaissance
data urodzenia
wprowadzona data urodzenia
marie
stan cywilny
wprowadzony stan cywilny
nbEnfants
liczba dzieci
wprowadzona liczba dzieci
erreurEdit
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.
erreurPrenom
puste
sygnalizuje błędne imię – w przeciwnym razie puste
erreurNom
puste
oznacza błędne nazwisko – w przeciwnym razie puste
erreurDateNaissance
puste
oznacza błędną datę urodzenia – w przeciwnym razie puste
erreurNbEnfants
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 &agrave; 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:

Image

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]:

Image


Struc tura i inicjalizacja kontrolera


Szkielet kontrolera [Application] wygląda następująco:

package istia.st.mvc.personnes.web;

import istia.st.mvc.personnes.dao.DaoException;
...

@SuppressWarnings("serial")
public class Application extends HttpServlet {
     // parametry instancji
    private String urlErreurs = null;
    private ArrayList erreursInitialisation = new ArrayList<String>();
    private String[] paramètres = { "urlList", "urlEdit", "urlErreurs" };
    private Map params = new HashMap<String, String>();

     // usługa
    ServiceImpl service=null;

     // inicjalizacja
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
         // pobieranie parametrów inicjalizacyjnych serwletu
        ServletConfig config = getServletConfig();
         // przetwarzanie pozostałych parametrów inicjalizacyjnych
        String valeur = null;
        for (int i = 0; i < paramètres.length; i++) {
             // wartość parametru
            valeur = config.getInitParameter(paramètres[i]);
             // czy parametr występuje?
            if (valeur == null) {
                 // odnotowuje się błąd
                erreursInitialisation.add("Le paramètre [" + paramètres[i]
                        + "] n'a pas été initialisé");
            } else {
                 // zapisywanie wartości parametru
                params.put(paramètres[i], valeur);
            }
        }
         // adres URL widoku [erreurs] podlega specjalnej obróbce
        urlErreurs = config.getInitParameter("urlErreurs");
        if (urlErreurs == null)
            throw new ServletException(
                    "Le paramètre [urlErreurs] n'a pas été initialisé");
         // instancjonowanie warstwy [dao]
        DaoImpl dao = new DaoImpl();
        dao.init();
         // utworzenie instancji warstwy [service]
        service = new ServiceImpl();
        service.setDao(dao);
    }

     // GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
....
    }

     // wyświetlanie listy osób
    private void doListPersonnes(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

     // edycja / dodanie osoby
    private void doEditPersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

     // potwierdzenie zmiany / dodania osoby
    private void doDeletePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

     // potwierdzenie zmiany / dodania osoby
    public void doValidatePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

     // wyświetlenie wstępnie wypełnionego formularza
    private void showFormulaire(HttpServletRequest request,
            HttpServletResponse response, String erreurEdit) throws ServletException, IOException{
...
    }

     // wysłanie
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
         // przekazanie kontroli do GET
        doGet(request, response);    }
}
  • 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]:

Image

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
/do/list
GET
doListPersonnes
/do/edit
GET
doEditPersonne
/do/validate
POST
doValidatePersonne
/do/delete
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:

// GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {

         // sprawdzamy, jak przebiegła inicjalizacja serwletu
        if (erreursInitialisation.size() != 0) {
             // przekazujemy kontrolę do strony błędów
            request.setAttribute("erreurs", erreursInitialisation);
            getServletContext().getRequestDispatcher(urlErreurs).forward(request, response);
             // koniec
            return;
        }
         // pobieramy metodę wysyłania żądania
        String méthode = request.getMethod().toLowerCase();
         // pobieramy akcję do wykonania
        String action = request.getPathInfo();
         // akcja?
        if (action == null) {
            action = "/list";
        }
         // wykonanie akcji
        if (méthode.equals("get") && action.equals("/list")) {
             // lista osób
            doListPersonnes(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/delete")) {
             // usunięcie osoby
            doDeletePersonne(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/edit")) {
             // wyświetlenie formularza dodania / zmiany osoby
            doEditPersonne(request, response);
            return;
        }
        if (méthode.equals("post") && action.equals("/validate")) {
             // zatwierdzenie formularza dodania / zmiany osoby
            doValidatePersonne(request, response);
            return;
        }
         // inne przypadki
        doListPersonnes(request, response);
    }
  • 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:

Image

Jej kod wygląda następująco:

1
2
3
4
5
6
7
8
9
     // wyświetlanie listy osób
    private void doListPersonnes(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
         // szablon widoku [list]
        request.setAttribute("personnes", service.getAll());
         // wyświetlanie widoku [list]
        getServletContext()
                .getRequestDispatcher((String) params.get("urlList")).forward(request, response);
    }
  • 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]:

Image

którego kod wygląda następująco:

...
<html>
    <head>
        <title>MVC - personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
...
            <c:forEach var="personne" items="${personnes}">
                <tr>
...
                    <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>

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:

     // zatwierdzanie zmiany / dodania osoby
    private void doDeletePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
         // pobieranie identyfikatora osoby
        int id = Integer.parseInt(request.getParameter("id"));
         // usuwanie osoby
        service.deleteOne(id);
         // przekierowanie do listy osób
        response.sendRedirect("list");
    }
  • 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:

Image

  • 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]:

Image

którego kod wygląda następująco:

...
<html>
    <head>
        <title>MVC - personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
...
            <c:forEach var="personne" items="${personnes}">
                <tr>
...
                    <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>

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:

     // zmiana / dodanie osoby
    private void doEditPersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
         // pobieranie identyfikatora osoby
        int id = Integer.parseInt(request.getParameter("id"));
         // dodanie czy zmiana?
        Personne personne = null;
        if (id != -1) {
             // modyfikacja – pobieranie osoby do modyfikacji
            personne = service.getOne(id);
        } else {
             // dodanie – tworzy się pusty rekord osoby
            personne = new Personne();
            personne.setId(-1);
        }
         // umieszczamy obiekt [Personne] w szablonie widoku [edit]
        request.setAttribute("erreurEdit", "");
        request.setAttribute("id", personne.getId());
        request.setAttribute("version", personne.getVersion());
        request.setAttribute("prenom", personne.getPrenom());
        request.setAttribute("nom", personne.getNom());
        Date dateNaissance = personne.getDateNaissance();
        if (dateNaissance != null) {
            request.setAttribute("dateNaissance", new SimpleDateFormat(
                    "dd/MM/yyyy").format(dateNaissance));
        } else {
            request.setAttribute("dateNaissance", "");
        }
        request.setAttribute("marie", personne.getMarie());
        request.setAttribute("nbEnfants", personne.getNbEnfants());
         // wyświetlenie widoku [edit]
        getServletContext()
                .getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • GET ma jako cel adres URL typu [/do/edit?id=XX]. W wierszu 5 pobieramy wartość z [id]. Następnie występują dwa przypadki:
  1. 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].
  2. 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]:

Image

Przypomnijmy elementy wprowadzania danych w formularzu HTML z powyższego widoku:

<form method="post" action="<c:url value="/do/validate"/>">
....
        <input type="text" value="${prenom}" name="prenom" size="20">
....
        <input type="text" value="${nom}" name="nom" size="20">
....
        <input type="text" value="${dateNaissance}" name="dateNaissance">
...
        <input type="radio" name="marie" value="true" checked>Oui
....
        <input type="text" value="${nbEnfants}" name="nbEnfants">
....
            <input type="hidden" value="${id}" name="id">
     <input type="hidden" value="${version}" name="version">
            <input type="submit" value="Valider">
</form>

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]:

// zatwierdzenie zmiany / dodania osoby
    public void doValidatePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
         // pobieranie przesłanych elementów
        boolean formulaireErroné = false;
        boolean erreur;
         // imię
        String prenom = request.getParameter("prenom").trim();
         // czy imię jest prawidłowe?
        if (prenom.length() == 0) {
             // odnotowujemy błąd
            request.setAttribute("erreurPrenom", "Le prénom est obligatoire");
            formulaireErroné = true;
        }
         // nazwisko
        String nom = request.getParameter("nom").trim();
         // czy imię jest prawidłowe?
        if (nom.length() == 0) {
             // odnotowano błąd
            request.setAttribute("erreurNom", "Le nom est obligatoire");
            formulaireErroné = true;
        }
         // data urodzenia
        Date dateNaissance = null;
        try {
            dateNaissance = new SimpleDateFormat("dd/MM/yyyy").parse(request
                    .getParameter("dateNaissance").trim());
        } catch (ParseException e) {
             // odnotowano błąd
            request.setAttribute("erreurDateNaissance", "Date incorrecte");
            formulaireErroné = true;
        }
         // stan cywilny
        boolean marie = Boolean.parseBoolean(request.getParameter("marie"));
         // liczba dzieci
        int nbEnfants = 0;
        erreur = false;
        try {
            nbEnfants = Integer.parseInt(request.getParameter("nbEnfants")
                    .trim());
            if (nbEnfants < 0) {
                erreur = true;
            }
        } catch (NumberFormatException ex) {
             // odnotowano błąd
            erreur = true;
        }
         // błędna liczba dzieci?
        if (erreur) {
             // zgłoszono błąd
            request.setAttribute("erreurNbEnfants",
                    "Nombre d'enfants incorrect");
            formulaireErroné = true;
        }
         // identyfikator osoby
        int id = Integer.parseInt(request.getParameter("id"));
         // wersja
        long version = Long.parseLong(request.getParameter("version"));
         // czy formularz zawiera błędy?
        if (formulaireErroné) {
             // ponowne wyświetlenie formularza z komunikatami o błędach
            showFormulaire(request, response, "");
             // zakończono
            return;
        }
         // formularz jest poprawny – zapisujemy dane osoby
        Personne personne = new Personne(id, prenom, nom, dateNaissance, marie,
                nbEnfants);
        personne.setVersion(version);
        try {
             // zapis
            service.saveOne(personne);
        } catch (DaoException ex) {
             // ponownie wyświetla się formularz z komunikatem o wystąpieniu błędu
            showFormulaire(request, response, ex.getMessage());
             // zakończono
            return;
        }
         // przekierowanie do listy osób
        response.sendRedirect("list");
    }

     // wyświetlenie wstępnie wypełnionego formularza
    private void showFormulaire(HttpServletRequest request,
            HttpServletResponse response, String erreurEdit)
            throws ServletException, IOException {
         // przygotowuje się szablon widoku [edit]
        request.setAttribute("erreurEdit", erreurEdit);
        request.setAttribute("id", request.getParameter("id"));
        request.setAttribute("version", request.getParameter("version"));
        request.setAttribute("prenom", request.getParameter("prenom").trim());
        request.setAttribute("nom", request.getParameter("nom").trim());
        request.setAttribute("dateNaissance", request.getParameter(
                "dateNaissance").trim());
        request.setAttribute("marie", request.getParameter("marie"));
        request.setAttribute("nbEnfants", request.getParameter("nbEnfants")
                .trim());
         // wyświetlenie widoku [edit]
        getServletContext()
                .getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • 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]:

Image

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

Image

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

Image

Użytkownik U2 postępuje analogicznie:

Image

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:

Image

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:

Image

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.