14. Веб-додаток MVC у трирівневій архітектурі — Приклад 1
14.1. Présentation
Дотепер ми обмежувалися прикладами, що мали навчальний характер. Тому вони мали бути простими. Тепер ми представляємо базовий додаток, який, однак, є більш функціональним, ніж усі представлені досі. Його особливістю є використання трьох рівнів трирівневої архітектури:

Читачеві пропонується ще раз ознайомитися з принципами роботи веб-додатку MVC у трирівневій архітектурі, якщо він їх забув, у параграфі 4.
Веб-додаток, який ми будемо писати, дасть змогу керувати групою осіб за допомогою чотирьох операцій:
- перелік осіб у групі
- додавання особи до групи
- редагування особи в групі
- видалення особи з групи
Ці чотири базові операції відповідають операціям з таблицею бази даних. Ми напишемо дві версії цього додатка:
- у версії 1 рівень [dao] не використовуватиме базу даних. Особи з групи будуть зберігатися у простому об’єкті [ArrayList], який внутрішньо керується шаром [dao]. Це дозволить читачеві протестувати додаток без обмежень, пов’язаних із базою даних.
- У версії 2 ми розмістимо групу осіб у таблиці бази даних. Ми покажемо, що це відбудеться без впливу на веб-шар версії 1, який залишиться незмінним.
Наступні скріншоти демонструють сторінки, які додаток відображає користувачеві.



![]() |
![]() |
14.2. Проєкт Eclipse
Проєкт додатка має назву [personnes-01]:

Цей проєкт охоплює три рівні трирівневої архітектури додатка:
![]() |
- шар [dao] міститься в пакеті [istia.st.mvc.personnes.dao]
- шар [metier] або [service] міститься в пакеті [istia.st.mvc.personnes.service]
- шар [web] або [ui] міститься в пакеті [istia.st.mvc.personnes.web]
- пакет [istia.st.mvc.personnes.entites] містить об’єкти, спільні для різних шарів
- пакет [istia.st.mvc.personnes.tests] містить тести Junit для шарів [dao] та [service]
Ми по черзі розглянемо три шари: [dao], [service] та [web]. Оскільки описувати все це було б занадто довго, а читати — можливо, занадто нудно, ми іноді будемо трохи поспішати з поясненнями, за винятком тих випадків, коли йдеться про щось нове.
14.3. Представлення особи
Додаток керує групою осіб. На скріншотах у розділі 14.1 показано деякі характеристики особи. Формально вони представлені класом [Personne]:
![]()
Клас [Personne] має такий вигляд:
- особа ідентифікується за допомогою такої інформації:
- id: номер, що однозначно ідентифікує особу
- прізвище: прізвище особи
- ім’я: її ім’я
- dateNaissance: її дата народження
- статус шлюбу: її сімейний стан (заміжня чи незаміжня)
- nbEnfants: кількість дітей
- атрибут [version] — це атрибут, штучно доданий для потреб додатка. З точки зору об’єктно-орієнтованого програмування, безсумнівно, було б краще додати цей атрибут у клас, похідний від [Personne]. Необхідність у ньому виникає під час аналізу сценаріїв використання веб-додатку. Один із них такий:
У момент часу T1 користувач U1 переходить до редагування особи P. На цей момент кількість дітей дорівнює 0. Він змінює цю кількість на 1, але перш ніж він підтвердить свою зміну, користувач U2 переходить до редагування тієї самої особи P. Оскільки U1 ще не підтвердив свою зміну, U2 бачить кількість дітей рівною 0. U2 переводить ім’я особи P у великі літери. Потім U1 і U2 підтверджують свої зміни в такому порядку. Переможе зміна, внесена U2: ім’я буде переведено у великі літери, а кількість дітей залишиться рівною нулю, хоча U1 вважає, що змінив її на 1.
Поняття «версії особи» допомагає нам вирішити цю проблему. Розглянемо той самий приклад використання:
У момент часу T1 користувач U1 переходить до редагування особи P. У цей момент кількість дітей дорівнює 0, а версія — V1. Він змінює кількість дітей на 1, але перш ніж він підтвердить свої зміни, користувач U2 переходить до редагування тієї самої особи P. Оскільки U1 ще не підтвердив свої зміни, U2 бачить, що кількість дітей дорівнює 0, а версія — V1. U2 змінює ім’я особи P на великі літери. Потім U1 та U2 затверджують свої зміни в цьому порядку. Перед затвердженням зміни перевіряється, чи користувач, який вносить зміни до особи P, має ту саму версію, що й особа P, яка наразі зареєстрована. Це стосується користувача U1. Отже, його зміна приймається, і версія зміненої особи змінюється з V1 на V2, щоб відзначити, що особа зазнала змін. Під час перевірки зміни, внесеної користувачем U2, ми побачимо, що він має версію V1 особи P, тоді як на даний момент її версія — V2. Тоді ми зможемо повідомити користувачу U2, що хтось встиг зробити це раніше за нього і що йому слід почати з нової версії особи P. Він це зробить, отримає особу P з версією V2, яка тепер має дитину, переведе ім’я у великі літери та підтвердить зміни. Його зміна буде прийнята, якщо зареєстрована особа P все ще має версію V2. У підсумку зміни, внесені U1 та U2, будуть враховані, тоді як у випадку використання без версій одна зі змін була б втрачена.
- рядки 32–40: конструктор, здатний ініціалізувати поля особи. Поле [version] опущено.
- рядки 43–51: конструктор, який створює копію особи, що передається йому як параметр. У результаті маємо два об’єкти з однаковим вмістом, але на які вказують два різні покажчики.
- рядок 55: метод [toString] перевизначено, щоб повертати рядок, що відображає стан особи
14.4. Рівень [dao]
Рівень [dao] складається з таких класів та інтерфейсів:
![]()
- [IDao] — це інтерфейс, представлений рівнем [dao]
- [DaoImpl] — це реалізація цього інтерфейсу, в якій група осіб інкапсульована в об’єкт [ArrayList]
- [DaoException] — це тип неперевірених (unchecked) винятків, що генеруються рівнем [dao]
Інтерфейс [IDao] має такий вигляд:
- Інтерфейс має чотири методи для чотирьох операцій, які потрібно виконати над групою осіб:
- getAll: для отримання набору осіб
- getOne: для отримання особи з певним id
- saveOne: для додавання особи (id=-1) або редагування існуючої особи (id <> -1)
- deleteOne: для видалення особи з певним id
У рівні [dao] можуть виникати винятки. Вони будуть типу [DaoException] :
- рядок 3: клас [DaoException], що походить від [RuntimeException], є типом неконтрольованого винятку: компілятор не вимагає від нас:
- обробляти цей тип винятків за допомогою блоку try/catch під час виклику методу, який може його викликати
- включати маркер «throws DaoException» у сигнатуру методу, який може викликати це виключення
Ця техніка дозволяє нам уникнути необхідності підписувати методи інтерфейсу [IDao] винятками певного типу. Будь-яка реалізація, що генерує неконтрольовані винятки, буде прийнятною, що забезпечить гнучкість архітектури.
- рядок 6: код помилки. Рівень [dao] генеруватиме різні винятки, які ідентифікуватимуться за допомогою різних кодів помилок. Це дозволить рівню, який вирішить обробляти виняток, дізнатися точне джерело помилки та вжити відповідних заходів. Існують й інші способи досягнення того самого результату. Один із них — створити тип винятку для кожного можливого типу помилки, наприклад NomManquantException, PrenomManquantException, AgeIncorrectException, ...
- рядки 13–16: конструктор, який дозволить створити виняток, ідентифікований за кодом помилки та повідомленням про помилку.
- рядки 8–10: метод, який дозволить коду обробки винятку отримати код помилки.
Клас [DaoImpl] реалізує інтерфейс [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 | |
Ми наведемо лише основні риси цього коду. Однак трохи зупинимося на найскладніших частинах.
- рядок 13: об’єкт [ArrayList], який міститиме групу осіб
- рядок 16: ідентифікатор останньої доданої особи. З кожним новим додаванням цей ідентифікатор збільшуватиметься на 1.
Клас [DaoImpl] буде інстанційовано в єдиному екземплярі. Це називається синглтоном. Веб-додаток обслуговує своїх користувачів одночасно. У певний момент часу веб-сервер виконує кілька потоків. Вони спільно використовують синглтони:
- синглтон рівня [dao]
- з рівня [service]
- окремі екземпляри різних контролерів, валідаторів даних тощо з веб-шару
Якщо синглтон має приватні поля, слід одразу ж запитати себе, навіщо вони йому потрібні. Чи є вони виправданими? Адже вони будуть спільними для різних потоків. Якщо вони призначені лише для читання, це не становить проблеми, якщо їх можна ініціалізувати в момент, коли ми впевнені, що активний лише один потік. Зазвичай такий момент можна визначити. Це момент запуску веб-додатку, коли він ще не почав обслуговувати клієнтів. Якщо ж ці поля доступні як для читання, так і для запису, то необхідно забезпечити синхронізацію доступу до полів, інакше це призведе до катастрофи. Ми проілюструємо цю проблему під час тестування шару [dao].
- Клас [DaoImpl] не має конструктора. Тому буде використано його конструктор за замовчуванням.
- рядки 19–38: метод [init] буде викликано під час інстанціювання синглтона шару [dao]. Він створює список із трьох осіб.
- рядки 41–43: реалізують метод [getAll] інтерфейсу [IDao]. Вони повертають посилання на список осіб.
- рядки 46–55: реалізують метод [getOne] інтерфейсу [IDao]. Його параметром є ідентифікатор особи, яку шукають.
Щоб отримати її, викликається приватний метод [getPosition] у рядках 113–126. Цей метод повертає позицію шуканої особи у списку або -1, якщо особу не знайдено.
Якщо особу було знайдено, метод [getOne] повертає посилання (рядок 51) на копію цієї особи, а не на саму особу. Дійсно, коли користувач захоче змінити дані особи, інформація про неї буде запитана на рівні [dao] і передана до рівня [web] для редагування у вигляді посилання на об’єкт [Personne]. Ця посилання слугуватиме контейнером для введення даних у формі редагування. Коли у веб-шарі користувач надішле свої зміни, вміст контейнера для введення даних буде змінено. Якщо контейнер є посиланням на реальну особу з об’єкта [ArrayList] у шарі [dao], то вона змінюється, навіть якщо зміни не були передані до шарів [service] та [dao]. Останній є єдиним, уповноваженим керувати списком осіб. Тому веб-шар повинен працювати з копією особи, яку потрібно змінити. У цьому випадку шар [dao] надає цю копію.
Якщо шукану особу не знайдено, генерується виняток типу [DaoException] з кодом помилки 2 (рядок 53).
- рядки 94–104: реалізують метод [deleteOne] інтерфейсу [IDao]. Його параметром є ідентифікатор особи, яку потрібно видалити. Якщо особа, яку потрібно видалити, не існує, генерується виняток типу [DaoException] з кодом помилки 2.
- рядки 58–91: реалізують метод [saveOne] інтерфейсу [IDao]. Його параметром є об’єкт [Personne]. Якщо цей об’єкт має id=-1, то це додавання особи. В іншому випадку — це зміна даних особи зі списку, що має цей id, на значення, вказані в параметрі.
- рядок 60: правильність параметра [Personne] перевіряється за допомогою приватного методу [check], визначеного в рядках 129–155. Цей метод виконує базові перевірки значень різних полів [Personne]. Щоразу, коли виявляється відхилення, запускається [DaoException] із конкретним кодом помилки. Оскільки метод [saveOne] не обробляє це виключення, воно передається до методу, що його викликав.
- рядок 62: якщо ідентифікатор параметра [Personne] дорівнює -1, то це додавання. Об’єкт [Personne] додається до внутрішнього списку осіб (рядок 66) із першим доступним ідентифікатором (рядок 64) та номером версії, рівним 1 (рядок 65).
- якщо параметр [Personne] має значення [id], відмінне від -1, то це означає, що потрібно змінити особу у внутрішньому списку, яка має цей [id]. Спочатку перевіряється (рядки 70–75), чи існує особа, яку потрібно змінити. Якщо ні, генерується виняток типу [DaoException] з кодом помилки 2.
- Якщо особа дійсно існує, перевіряється, чи її поточна версія збігається з версією параметра [Personne], який містить зміни, що мають бути внесені до оригіналу. Якщо це не так, це означає, що особа, яка хоче внести зміни до даних особи, не має її останньої версії. Про це повідомляють, генеруючи виняток типу [DaoException] з кодом помилки 3 (рядки 79–80).
- Якщо все гаразд, зміни вносяться до оригінального запису особи (рядки 85–90)
Очевидно, що цей метод має бути синхронізований. Наприклад, у проміжку часу між перевіркою наявності особи, яку потрібно змінити, та моментом внесення зміни ця особа могла бути видалена зі списку кимось іншим. Тому метод слід оголосити як [synchronized], щоб гарантувати, що його виконує лише один потік одночасно. Те саме стосується й інших методів інтерфейсу [IDao]. Ми цього не робимо, вважаючи за краще перенести цю синхронізацію на рівень [service]. Щоб висвітлити проблеми синхронізації, під час тестування шару [dao] ми зупинимо виконання [saveOne] на 10 мс (рядок 83) між моментом, коли ми знаємо, що можна внести зміну, і моментом, коли ми її фактично вносимо. Потік, що виконує [saveOne], втратить доступ до процесора на користь іншого. Таким чином ми збільшуємо ймовірність виникнення конфліктів доступу до списку осіб.
14.5. Тестування рівня [dao]
Для рівня [dao] написано тест JUnit:
![]() | ![]() |
[TestDao] — це тест JUnit. Щоб виявити проблеми з паралельним доступом до списку осіб, створюються потоки типу [ThreadDaoMajEnfants]. Їх завданням є збільшення на 1 кількості дітей певної особи.
[TestDao] містить п’ять тестів — від [test1] до [test5]. Ми наводимо лише два з них, а решту читач може ознайомитися у вихідному коді, що додається до цієї статті.
- рядок 9: посилання на реалізацію тестованого шару [dao]
- рядки 12–15: конструктор тесту JUnit. Він створює екземпляр типу [DaoImpl] з тестованого шару [dao] та ініціалізує його.
Метод [test1] тестує чотири методи інтерфейсу [IDao] наступним чином:
- рядок 3: запитується список осіб
- рядок 6: виводиться цей список
[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]
Далі тест додає особу, змінює її та видаляє. Таким чином використовуються всі чотири методи інтерфейсу [IDao].
- рядки 8–10: додається нова особа (id=-1).
- рядок 11: отримуємо ідентифікатор доданої особи, оскільки під час додавання їй було присвоєно ідентифікатор. До цього вона його не мала.
- рядки 13–14: запитується у шару [dao] копія особи, яку щойно додано. Слід пам’ятати, що якщо запитувану особу не знайдено, шар [dao] генерує виняток. У такому разі відбудеться збій у роботі програми на рядку 13. Цей випадок можна було б обробити більш коректно. У рядку 14 перевіряється ім’я знайденої особи.
- рядки 16–17: змінюємо це ім’я та звертаємося до шару [dao] із запитом на збереження змін.
- рядки 19–20: запитуємо у шару [dao] копію особи, яку щойно додали, і перевіряємо її нове ім’я.
- рядок 22: видаляємо особу, додану на початку тесту.
- рядки 23–34: запитуємо у шару [dao] копію особи, яку щойно видалили. Повинно бути отримано [DaoException] з кодом 2.
- рядки 36–37: повторно запитується список осіб. Повинно бути отримано той самий список, що й на початку тесту.
Метод [test4] має на меті виявити проблеми одночасного доступу до методів шару [dao]. Нагадаємо, що ці методи не були синхронізовані. Код тесту такий:
- рядки 3–6: до списку додається особа P, яка не має дітей. Записується її [id] (рядок 6).
- рядки 7–13: запускаємо N потоків. Кожен із них збільшить кількість дітей особи P на 1 одиницю. У підсумку особа P повинна мати N дітей.
- рядки 15–17: метод [test4], який запустив N потоків, чекає, поки вони завершать свою роботу, перш ніж перевірити нову кількість дітей особи P.
- рядки 18–21: отримуємо особу P і перевіряємо, чи кількість її дітей дорівнює N.
- рядки 22–35: особу P видаляють, а потім перевіряють, чи вона більше не існує у списку.
У рядку 11 бачимо, що потоки мають тип [ThreadDaoMajEnfants]. Конструктор цього типу має три параметри:
- назва, присвоєна потоку, для його відстеження за допомогою логів
- посилання на рівень [dao], щоб потік мав до нього доступ
- ідентифікатор особи, з якою має працювати потік
Тип [ThreadDaoMajEnfants] має такий вигляд:
- рядок 9: [ThreadDaoMajEnfants] дійсно є потоком
- рядки 18–22: конструктор, який ініціалізує потік, використовуючи три параметри
- ім’я [name], присвоєне потоку
- посилання [dao] на рівень [dao]. Зазначимо, що знову ми працюємо з типом інтерфейсу [IDao], а не з типом реалізації [DaoImpl].
- ідентифікатор [id] особи, з якою повинен працювати потік
Коли [test4] запускає потік [ThreadDaoMajEnfants] (рядок 12 файлу test4), виконується його метод [run] (рядок 25):
- рядки 78–81: приватний метод [suivi] дозволяє виводити повідомлення на екран. Метод [run] використовує його для відстеження виконання потоку.
- потік намагатиметься збільшити на 1 кількість дітей особи P з ідентифікатором [id]. Це оновлення може вимагати декількох спроб. Розглянемо два потоки [TH1] та [TH2]. [TH1] запитує копію особи P у шару [dao]. Він отримує її та констатує, що вона має версію V1. [TH1] переривається. [TH2], який йшов за ним, робить те саме і отримує ту саму версію V1 особи P. [TH2] переривається. [TH2] повертає собі контроль, збільшує кількість дочірніх елементів P та зберігає свої зміни. Ми знаємо, що на цей момент ці зміни збережені, а версія P перейде до V2. [TH1] завершив свою роботу. [TH2] повертається до роботи і робить те саме. Його оновлення P буде відхилено, оскільки він має копію P версії V1, тоді як оригінальний P тепер має версію V2. Тож [TH2] повинен повторити весь цикл [lecture -> mise à jour -> sauvegarde]. Саме тому ми бачимо цикл у рядках 32–72. У ньому потік:
- запитує копію особи P для редагування (рядок 34)
- очікує 10 мс (рядок 43). Це штучно введена затримка, яка має на меті перервати роботу потоку між зчитуванням особи P та її фактичним оновленням у списку осіб, щоб збільшити ймовірність конфліктів.
- збільшує кількість дітей P (рядок 54) та зберігає P (рядок 56). Якщо потік не має правильної версії P, рівень [dao] викличе виняток. Потім отримується код винятку (рядок 61) для перевірки, чи це дійсно код 3 (неправильна версія P). Якщо це не так, виняток перенаправляється до методу, що викликає, а саме до тестового методу [test4]. Якщо виникло виключення з кодом 3, то цикл [lecture -> mise à jour -> sauvegarde] повторюється. Якщо виключення не виникло, то оновлення виконано, і робота потоку завершена.
Які результати дають тести?
У першій протестованій конфігурації:
- закоментували інструкцію очікування в методі [saveOne] з [DaoImpl] (рядок 83, параграф 14.4).
- метод [test4] створює 100 потоків (рядок 8, параграф 14.5).
Отримуємо такі результати:

Усі п’ять тестів пройшли успішно.
У другій перевіреній конфігурації:
- видаляємо коментар з команди очікування в методі [saveOne] з [DaoImpl] (рядок 83, параграф 14.4).
- метод [test4] створює 2 потоки (рядок 8, параграф 14.5).
Отримуємо такі результати:
![]() | ![]() |
Тест [test4] завершився невдало. Було створено два потоки, кожен з яких мав збільшити на 1 кількість дітей особи P, яка спочатку не мала жодної дитини. Отже, після виконання обох потоків очікувалося 2 дітей, проте є лише одна.
Проаналізуємо екранні логи [test4], щоб зрозуміти, що сталося:
- рядок 1: потік № 0 починає роботу
- рядок 2: він отримав копію особи P і виявив, що кількість її дітей дорівнює 0
- рядок 3: він натрапляє на [Thread.sleep(10)] свого методу [run] і, отже, зупиняється на часі [1145536368171] (мс)
- рядок 4: потім потік № 1 отримує доступ до процесора і починає свою роботу
- рядок 5: він отримав копію особи P і встановлює, що кількість її дітей дорівнює 0
- рядок 6: він натрапляє на [Thread.sleep(10)] свого методу [run] і, отже, зупиняється
- рядок 7: потік № 0 отримує доступ до процесора в момент часу [1145536368187] (мс), c.a.d — через 16 мс після втрати доступу.
- рядок 8: те саме для потоку № 1
- рядок 9: потік № 0 виконав оновлення та змінив кількість дочірніх процесів на 1
- рядок 10: потік № 1 зробив те саме
Питання полягає в тому, чому потік № 1 зміг виконати оновлення, хоча зазвичай він уже не мав правильної версії особи P, яку щойно оновив потік № 0.
По-перше, можна помітити аномалію між рядками 7 і 8: схоже, що між цими двома рядками потік № 0 втратив контроль над процесором на користь потоку № 1. Що він робив у цей момент? Він виконував метод [saveOne] з шару [dao]. Цей метод має такий каркас (див. параграф 14.4):
- потік № 0 виконав [saveOne] і дійшов до рядка 8, де був змушений звільнити процесор. Тим часом він зчитав версію особи P, і вона дорівнювала 1, оскільки особа P ще не була оновлена.
- Оскільки процесор звільнився, його перейняв потік № 1. Він, у свою чергу, виконав [saveOne] і дійшов до рядка 8, де був змушений звільнити процесор. Тим часом він зчитав версію користувача P, і вона дорівнювала 1, оскільки дані користувача P ще не були оновлені.
- Оскільки процесор став вільним, його перейняв потік № 0. Починаючи з 9-го рядка, він виконав оновлення та змінив кількість дітей на 1. Потім метод [run] потоку № 0 завершився, і потік вивів запис у журнал, який повідомляв, що він змінив кількість дітей на 1 (9-й рядок).
- Оскільки процесор звільнився, його перейняв потік № 1. Починаючи з 9-го рядка, він виконав оновлення та змінив кількість дочірніх процесів на 1. Чому 1? Тому що він має копію P із кількістю дочірніх процесів, що дорівнює 0. Про це свідчить запис у журналі (5-й рядок). Потім метод [run] потоку № 1 завершився, і потік вивів у лог повідомлення про те, що він змінив кількість дітей на 1 (рядок 10).
У чому полягає проблема? Вона полягає в тому, що потік № 0 не встиг підтвердити свою зміну і, отже, змінити версію особи P до того, як потік № 1 спробував прочитати цю версію, щоб дізнатися, чи змінилася особа P. Такий сценарій малоймовірний, але не неможливий. Довелося примусово відібрати ресурси процесора у потоку № 0, щоб відтворити цю ситуацію лише з двома потоками. Без цього прийому попередня конфігурація не змогла відтворити цей самий випадок із 100 потоками. Тест [test4] пройшов успішно.
Яке рішення? Безсумнівно, їх є кілька. Одне з них, просте у реалізації, полягає в синхронізації методу [saveOne]:
public synchronized void saveOne(Personne personne)
Ключове слово [synchronized] гарантує, що метод може виконувати лише один потік одночасно. Таким чином, потік № 1 отримає дозвіл на виконання [saveOne] лише тоді, коли потік № 0 вийде з нього. Тоді ми можемо бути впевнені, що версія особи P буде змінена, коли потік № 1 увійде в [saveOne]. Його оновлення буде відхилено, оскільки він не матиме правильної версії P.
Саме ці чотири методи шару [dao] слід було б синхронізувати. Однак ми вирішили залишити цей шар таким, як він описаний, і перенести синхронізацію на шар [service]. Для цього є кілька причин:
- ми виходимо з припущення, що доступ до шару [dao] завжди здійснюється через шар [service]. Саме так відбувається в нашому веб-додатку.
- може виникнути необхідність синхронізувати також доступ до методів шару [service] з інших причин, ніж ті, що змушують нас синхронізувати методи шару [dao]. У цьому випадку немає потреби синхронізувати методи шару [dao]. Якщо ми впевнені, що:
- усі звернення до шару [dao] проходять через шар [service]
- що одночасно лише один потік використовує шар [service]
то можна бути впевненим, що методи шару [dao] не будуть виконуватися двома потоками одночасно.
Тепер розглянемо рівень [service].
14.6. Рівень [service]
Рівень [service] складається з таких класів та інтерфейсів:
![]()
- [IService] — це інтерфейс, представлений рівнем [dao]
- [ServiceImpl] є її реалізацією
Інтерфейс [IService] має такий вигляд:
Він ідентичний інтерфейсу [IDao].
Реалізація [ServiceImpl] інтерфейсу [IService] має такий вигляд:
- рядки 10–19: атрибут [IDao dao] є посиланням на шар [dao]. Він буде ініціалізований Spring IoC.
- рядки 22–24: реалізація методу [getAll] інтерфейсу [IService]. Метод просто делегує запит на рівень [dao].
- рядки 27–29: реалізація методу [getOne] інтерфейсу [IService]. Цей метод просто делегує запит на рівень [dao].
- рядки 32–34: реалізація методу [saveOne] інтерфейсу [IService]. Цей метод просто делегує запит на рівень [dao].
- рядки 37–39: реалізація методу [deleteOne] інтерфейсу [IService]. Цей метод просто делегує запит на рівень [dao].
- Усі методи синхронізовані (ключове слово `synchronized`), що гарантує, що одночасно лише один потік зможе використовувати рівень [service], а отже, і рівень [dao].
14.7. Тестування шару [service]
Для шару [service] написано тест JUnit:
![]() | ![]() |
[TestService] — це тест JUnit. Виконані тести є повністю ідентичними тим, що були виконані для шару [dao]. Структура [TestService] така:
- рядок 9: тестований шар [service] типу [ServiceImpl].
- рядки 11–15: конструктор тесту JUnit створює екземпляр шару [service], що підлягає тестуванню (рядок 12), створює екземпляр шару [dao] (рядок 13) та вказує шару [service], що він повинен використовувати цей шар [dao] (рядок 14).
Метод [test1] тестує чотири методи інтерфейсу [IService] так само, як і однойменний метод тестування шару [dao]. Просто замість шару [dao] здійснюється звернення до шару [service] (рядки 25, 32, 35).
Метод [test4] призначений для виявлення проблем одночасного доступу до методів шару [service]. Він, знову ж таки, ідентичний методу тестування [test4] рівня [dao]. Однак є кілька відмінностей:
- звертаємося до шару [service], а не до шару [dao] (рядок 55)
- потокам передається посилання на шар [service] замість шару [dao] (рядок 61)
Тип [ThreadServiceMajEnfants] також майже ідентичний типу [ThreadDaoMajEnfants], за винятком того, що він працює з шаром [service], а не з шаром [dao]:
- рядок 12: потік працює з шаром [service]
Ми проводимо тестування з конфігурацією, яка спричинила проблему на рівні [dao]:
- видаляємо коментар з інструкції очікування в методі [saveOne] класу [DaoImpl] (рядок 83, параграф 14.4).
- метод [test4] створює 100 потоків (рядок 65, параграф 14.7).
Отримано такі результати:
![]() |
Саме синхронізація методів рівня [service] забезпечила успішне виконання тесту [test4].
14.8. Шар [web]
Нагадаємо про трирівневу архітектуру нашого додатка:
![]() |
Рівень [web] надаватиме користувачеві екрани, що дозволять йому керувати групою осіб:
- список осіб у групі
- додавання особи до групи
- редагування особи в групі
- видалення особи з групи
Для цього вона буде спиратися на рівень [service], який, у свою чергу, буде звертатися до рівня [dao]. Ми вже розглядали екрани, що обслуговуються шаром [web] (розділ 14.1). Для опису веб-шару ми послідовно розглянемо:
- її конфігурацію
- її подання
- її контролер
- деякі тести
14.8.1. Конфігурація веб-додатку
Проект Eclipse для додатка виглядає наступним чином:

- у пакеті [istia.st.mvc.personnes.web] міститься контролер [Application].
- Сторінки JSP / JSTL знаходяться в [WEB-INF/vues].
- Папка [lib] містить сторонні архіви, необхідні для роботи програми. Їх можна переглянути в папці [Web App Libraries].
[web.xml]
Файл [web.xml] — це файл, який використовується веб-сервером для завантаження програми. Його вміст такий:
<?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>
<!-- Мапування ServletPersonne-->
<servlet-mapping>
<servlet-name>personnes</servlet-name>
<url-pattern>/do/*</url-pattern>
</servlet-mapping>
<!-- файли головних сторінок -->
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<!-- Сторінка несподіваної помилки -->
<error-page>
<exception-type>java.lang.Exception</exception-type>
<location>/WEB-INF/vues/exception.jsp</location>
</error-page>
</web-app>
- рядки 27–30: URL-адреси [/do/*] оброблятиме сервлет [personnes]
- рядки 9–12: сервлет [personnes] є екземпляром класу [Application], який ми будемо створювати.
- рядки 13–24: визначають три параметри [urlList, urlEdit, urlErreurs], що ідентифікують URL-адреси сторінок JSP для переглядів [list, edit, erreurs].
- рядки 32–34: додаток має сторінку входу за замовчуванням [index.jsp], яка знаходиться в кореневій папці веб-додатка.
- рядки 36–39: у додатка є сторінка помилок за замовчуванням, яка відображається, коли веб-сервер отримує виняток, що не обробляється додатком.
- рядок 37: тег <exception-type> вказує тип винятку, що обробляється директивою <error-page>; у даному випадку це тип [java.lang.Exception] та його похідні, тобто всі винятки.
- рядок 38: тег <location> вказує на сторінку JSP, яку слід відобразити, коли виникає виняток типу, визначеного тегом <exception-type>. Інформація про виняток, що виник, доступна на цій сторінці в об’єкті з іменем exception, якщо на сторінці є директива:
<%@ page isErrorPage="true" %>
- (продовження)
- якщо <exception-type> вказує тип T1, і виняток типу T2, що не є похідним від T1, передається до веб-сервера, останній надсилає клієнту власну сторінку винятку, яка зазвичай є малозручною. Звідси й корисність тегу <error-page> у файлі [web.xml].
[index.jsp]
Ця сторінка відображається, якщо користувач безпосередньо запитує контекст додатка, не вказавши URL-адресу, c.a.d. тут [/personnes-01]. Її вміст такий:
<%@ 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] перенаправляє клієнта на URL-адресу [/do/list]. Ця URL-адреса відображає список осіб у групі.
14.8.2. Сторінки JSP / JSTL додатка
Вигляд [list.jsp]
Він слугує для відображення списку осіб:

Його код такий:
<%@ 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>
- Цей вигляд отримує елемент у своїй моделі:
- елемент [personnes], пов’язаний з об’єктом типу [ArrayList], що містить об’єкти типу [Personne]
- рядки 22–34: виконується перебір списку ${personnes} для відображення таблиці HTML, що містить осіб із групи.
- рядок 31: URL-адреса, на яку вказує посилання [Modifier], задається полем [id] поточної особи, щоб контролер, пов’язаний з URL-адресою [/do/edit], знав, яку саме особу потрібно редагувати.
- рядок 32: те саме робиться для посилання [Supprimer].
- рядок 28: щоб відобразити дату народження особи у форматі JJ/MM/AAAA, використовується тег <dt> із бібліотеки тегів [DateTime] проекту Apache [Jakarta Taglibs]:

Файл опису цієї бібліотеки тегів визначено у рядку 3.
- рядок 37: посилання [Ajout] для додавання нової особи має цільовим URL-адресу [/do/edit], так само як і посилання [Modifier] у рядку 31. Саме значення -1 параметра [id] вказує на те, що мова йде про додавання, а не про редагування.
Вигляд [edit.jsp]
Він слугує для відображення форми додавання нової особи або редагування існуючої особи:
![]() |
Код подання [edit.jsp] такий:
<%@ 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>
Цей вигляд містить форму для додавання нової особи або оновлення існуючої. Надалі, для спрощення викладу, ми будемо використовувати єдиний термін [mise à jour]. Кнопка [Valider] (рядок 73) викликає POST з URL-адреси [/do/validate] (рядок 16). Якщо POST завершується з помилкою, знову відображається сторінка [edit.jsp] із зазначенням помилки або помилок, що виникли; в іншому випадку відображається сторінка [list.jsp].
- Вигляд [edit.jsp], що відображається як на GET, так і на POST, які завершуються з помилкою, отримує у свою модель такі елементи:
атрибут | GET | POST |
ідентифікатор оновленої особи | те саме | |
його версія | те саме | |
його ім'я | введене ім'я | |
його прізвище | введене прізвище | |
дата народження | введена дата народження | |
його сімейний стан | введений сімейний стан | |
кількість дітей | введена кількість дітей | |
поле порожнє | повідомлення про помилку, що вказує на невдале додавання або зміну під час виконання POST, викликаного кнопкою [Envoyer]. Поле залишається порожнім, якщо помилки немає. | |
порожнє | вказує на неправильне ім’я — у іншому випадку порожнє | |
порожнє | вказує на помилкове прізвище – у разі відсутності значення | |
порожнє | позначає неправильну дату народження – у протилежному випадку порожнє | |
порожнє | вказує на неправильну кількість дітей – у разі відсутності таких даних залишається порожнім |
- рядки 11–15: якщо виконання POST у формі завершиться з помилкою, буде отримано [erreurEdit!=''] і відобразиться повідомлення про помилку.
- рядок 16: форма буде відправлена за URL-адресою [/do/validate]
- рядок 20: відображається елемент шаблону [id]
- рядок 24: відображається елемент [version] шаблону
- рядки 26–32: введення імені особи:
- під час початкового відображення форми (GET), ${ім'я} відображає поточне значення поля [prenom] оновленого об'єкта [Personne], а ${erreurPrenom} є порожнім.
- у разі помилки після POST знову відображається введене значення ${prenom}, а також можливе повідомлення про помилку ${erreurPrenom}
- рядки 33–39: введення прізвища особи
- рядки 40–46: введення дати народження особи
- рядки 47–61: введення сімейного стану особи за допомогою перемикача. Для визначення, який із двох перемикачів слід позначити, використовується значення поля [marie] об’єкта [Personne].
- рядки 62–68: введення кількості дітей особи
- рядок 71: приховане поле HTML з іменем [id], значенням якого є поле [id] особи, дані якої оновлюються; -1 — для додавання, інше значення — для редагування.
- рядок 72: приховане поле HTML з іменем [version], значення якого дорівнює значенню поля [id] особи, що оновлюється.
- рядок 73: кнопка [Valider] типу [Submit] у формі
- рядок 74: посилання, що дозволяє повернутися до списку осіб. Воно отримало назву [Annuler], оскільки дозволяє покинути форму без її підтвердження.
Вигляд [exception.jsp]
Вона служить для відображення сторінки, що повідомляє про виникнення винятку, який не обробляється додатком і який був переданий на веб-сервер.
Наприклад, видалимо особу, якої немає в групі:
![]() |
Код подання [exception.jsp] такий:
<%@ 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>
- Цей вигляд отримує у своїй моделі елемент [exception], який є винятком, перехопленим веб-сервером. Щоб цей елемент був включений веб-сервером у модель сторінки JSP, на сторінці має бути визначений тег у рядку 3.
- рядок 6: код стану відповіді HTTP встановлюється на 200. Це перший заголовок відповіді HTTP. Код 200 означає для клієнта, що його запит було виконано. Зазвичай у відповідь сервера вбудовується документ HTML. Так і є в даному випадку. Якщо не встановити код стану відповіді HTTP на 200, він матиме тут значення 500, що означає, що сталася помилка. Дійсно, веб-сервер, перехопивши некероване виключення, розцінює цю ситуацію як аномальну та сигналізує про це кодом 500. Реакція на код 500 у відповіді HTTP відрізняється залежно від браузера: Firefox відображає документ HTML, який може супроводжувати цю відповідь, тоді як IE ігнорує цей документ і відображає власну сторінку. Саме з цієї причини ми замінили код 500 на код 200.
- рядок 16: відображається текст винятку
- рядок 18: користувачеві пропонується посилання для повернення до списку осіб
Вигляд [erreurs.jsp]
Вона призначена для відображення сторінки, що повідомляє про помилки ініціалізації додатка, c.a.d, та про помилки, виявлені під час виконання методу [init] сервлета контролера. Це може бути, наприклад, відсутність параметра у файлі [web.xml], як показано у прикладі нижче:

Код сторінки [erreurs.jsp] такий:
<%@ 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>
Сторінка отримує у свій шаблон елемент [erreurs], який є об’єктом типу [ArrayList], що містить об’єкти [String], які, у свою чергу, є повідомленнями про помилки. Вони відображаються за допомогою циклу у рядках 13–15.
14.8.3. Контролер додатка
Контролер [Application] визначено в пакеті [istia.st.mvc.personnes.web]:
![]()
Структу та ініціалізація контролера
Структура контролера [Application] виглядає наступним чином:
- рядки 20–36: отримуємо очікувані параметри з файлу [web.xml].
- рядки 39–41: параметр [urlErreurs] має бути обов’язково присутнім, оскільки він вказує URL-адресу подання [erreurs], здатного відображати можливі помилки ініціалізації. Якщо його немає, додаток переривається шляхом запуску [ServletException] (рядок 40). Цей виняток буде передано на веб-сервер і оброблено тегом <error-page> у файлі [web.xml]. Отже, відображається сторінка [exception.jsp]:

Наведене вище посилання [Retour à la liste] не працює. Його використання повертає той самий результат, доки додаток не буде змінено та перезавантажено. Воно корисне для інших типів винятків, як ми вже бачили.
- рядок 43: створює екземпляр [DaoImpl], що реалізує шар [dao]
- рядок 44: ініціалізує цей екземпляр (створення початкового списку з трьох осіб)
- рядок 46: створює екземпляр [ServiceImpl], що реалізує шар [service]
- рядок 47: ініціалізує шар [service], надаючи йому посилання на шар [dao]
Після ініціалізації контролера його методи отримують посилання [service] на шар [service] (рядок 15), яке вони використовуватимуть для виконання дій, запитуваних користувачем. Ці дії будуть перехоплені методом [doGet], який передасть їх на обробку спеціальному методу контролера:
Url | Метод HTTP | метод контролера |
GET | doListPersonnes | |
GET | doEditPersonne | |
POST | doValidatePersonne | |
GET | doDeletePersonne |
Метод [doGet]
Цей метод призначений для спрямування обробки дій, запитуваних користувачем, до відповідного методу. Його код такий:
- рядки 7–13: перевіряється, чи список помилок ініціалізації порожній. Якщо це не так, відображається вікно [erreurs(erreurs)], яке повідомить про помилку або помилки.
- рядок 15: отримується метод [get] або [post], який клієнт використав для формування запиту.
- рядок 17: отримуємо значення параметра [action] із запиту.
- рядки 23–27: обробка запиту [GET /do/list], який запитує список осіб.
- рядки 28–32: обробка запиту [GET /do/delete], який вимагає видалення особи.
- рядки 33–37: обробка запиту [GET /do/edit], який вимагає форми для оновлення даних особи.
- рядки 38–42: обробка запиту [POST /do/validate], який вимагає підтвердження оновлених даних особи.
- рядок 44: якщо запитувана дія не є однією з п’яти попередніх, то виконується обробка, як для запиту [GET /do/list].
Метод [doListPersonnes]
Цей метод обробляє запит [GET /do/list], який вимагає переліку осіб:

Його код такий:
- рядок 5: у шарі [service] запитується список осіб групи, який поміщається в модель під ключем «особи».
- рядок 7: відображається вигляд [list.jsp], описаний у параграфі 14.8.2.
Метод [doDeletePersonne]
Цей метод обробляє запит [GET /do/delete?id=XX], який вимагає видалення особи з id=XX. URL-адреса [/do/delete?id=XX] відповідає посиланням [Supprimer] у поданні [list.jsp]:

код якого такий:
У рядку 12 ми бачимо URL-адресу [/do/delete?id=XX] посилання [Supprimer]. Метод [doDeletePersonne], який повинен обробити цей URL-адресу, повинен видалити особу з id=XX, а потім відобразити новий список осіб у групі. Його код такий:
- рядок 5: оброблюваний URL має вигляд [/do/delete?id=XX]. З параметра [id] отримуємо значення [XX].
- рядок 7: до шару [service] надсилається запит на видалення особи з отриманим ідентифікатором. Ми не проводимо жодної перевірки. Якщо особа, яку ми намагаємося видалити, не існує, шар [dao] генерує виняток, який передається вгору шаром [service]. Ми також не обробляємо це тут, у контролері. Отже, виняток передаватиметься аж до веб-сервера, який, згідно з налаштуваннями, відобразить сторінку [exception.jsp], описану в параграфі 14.8.2:

- рядок 9: якщо видалення відбулося (винятку немає), клієнту пропонується перейти за відносним URL-адресою [list]. Оскільки щойно оброблено сторінку [/do/delete], URL-адреса перенаправлення буде [/do/list]. Отже, браузер перейде на сторінку [GET /do/list], що призведе до відображення списку осіб.
Метод [doEditPersonne]
Цей метод обробляє запит [GET /do/edit?id=XX], який запитує форму оновлення особи з id=XX. URL-адреса [/do/edit?id=XX] є адресою посилань [Modifier] та [Ajout] у поданні [list.jsp]:

код якого такий:
У рядку 11 ми бачимо URL-адресу [/do/edit?id=XX] для посилання [Modifier], а в рядку 17 — URL-адресу [/do/edit?id=-1] для посилання [Ajout]. Метод [doEditPersonne] повинен відобразити форму редагування особи з id=XX або, якщо це додавання, — порожню форму.
![]() | ![]() |
Код методу [doEditPersonne] такий:
- GET має за ціль URL-адресу типу [/do/edit?id=XX]. У рядку 5 ми отримуємо значення [id]. Далі можливі два випадки:
- id відмінний від -1. Тоді це редагування, і потрібно відобразити форму, попередньо заповнену даними особи, яку потрібно редагувати. У рядку 10 ця особа запитується на рівні [service].
- id дорівнює -1. У цьому випадку йдеться про додавання, і потрібно відобразити порожню форму. Для цього у рядках 13–14 створюється порожня особа.
- Отриманий об’єкт [Personne] розміщується в шаблоні сторінки [edit.jsp], описаному в розділі 14.8.2. Цей шаблон містить такі елементи: [erreurEdit, id, version, prenom, erreurPrenom, nom, erreurNom, dateNaissance, erreurDateNaissance, marie, nbEnfants, erreurNbEnfants]. Ці елементи ініціалізуються у рядках 17–30, за винятком тих, значенням яких є порожній рядок [erreurPrenom, erreurNom, erreurDateNaissance, erreurNbEnfants]. Відомо, що за їхньої відсутності в шаблоні бібліотека JSTL відображатиме порожній рядок як їхнє значення. Хоча елемент [erreurEdit] також має значенням порожній рядок, він все ж ініціалізується, оскільки на сторінці [edit.jsp] виконується перевірка його значення.
- Після того як шаблон готовий, управління передається на сторінку [edit.jsp], рядки 32–33, яка згенерує представлення [edit].
Метод [doValidatePersonne]
Цей метод обробляє запит [POST /do/validate], який перевіряє форму оновлення. Цей POST запускається кнопкою [Valider]:

Нагадаємо елементи введення даних у формі HTML, показаній на малюнку вище:
Запит POST містить параметри [prenom, nom, dateNaissance, marie, nbEnfants, id, version] і надсилається на URL-адресу [/do/validate] (рядок 1). Він обробляється наступним методом [doValidatePersonne]:
- рядки 8–14: витягується параметр [prenom] із запиту POST і перевіряється його правильність. Якщо він виявляється некоректним, елемент [erreurPrenom] ініціалізується з повідомленням про помилку та розміщується в атрибутах запиту.
- рядки 16–22: аналогічні дії виконуються для параметра [nom]
- рядки 24–32: аналогічним чином обробляється параметр [dateNaissance]
- рядок 34: отримуємо параметр [marie]. Ми не перевіряємо його правильність, оскільки, як правило, він походить від значення перемикача. Однак ніщо не заважає програмі створити [POST /personnes-01/do/validate] разом із вигаданим параметром [marie]. Тому нам слід перевірити правильність цього параметра. Тут ми покладаємося на нашу систему обробки винятків, яка викликає відображення сторінки [exception.jsp], якщо контролер не обробляє їх самостійно. Отже, якщо перетворення параметра [marie] у логічне значення завершиться невдало у рядку 34, виникне виняток, що призведе до відправлення сторінки [exception.jsp] клієнту. Такий механізм нас влаштовує.
- рядки 34–54: отримуємо параметр [nbEnfants] і перевіряємо його значення.
- рядок 56: отримуємо параметр [id], не перевіряючи його значення
- рядок 58: те саме робимо для параметра [version]
- рядки 60–65: якщо форма містить помилки, її знову відображають разом із попередньо сформованими повідомленнями про помилки
- рядки 67–69: якщо форма є дійсною, створюється новий об’єкт [Personne] з елементами форми
- рядки 70–78: дані про особу зберігаються. Збереження може завершитися невдачею. У багатокористувацькому середовищі особа, яку потрібно змінити, могла бути видалена або вже змінена кимось іншим. У цьому випадку рівень [dao] викличе виняток, який ми обробляємо тут.
- рядок 80: якщо винятку не сталося, клієнта перенаправляють на URL-адресу [/do/list], щоб показати йому новий стан групи.
- рядок 75: якщо під час збереження сталося виключення, ми знову запитуємо перезавантаження початкової форми, передаючи їй повідомлення про помилку виключення (3-й параметр).
Метод [showFormulaire] (рядки 84–101) формує шаблон, необхідний для сторінки [edit.jsp], із введеними значеннями (request.getParameter(" ... ")). Нагадаємо, що повідомлення про помилки вже були вставлені в шаблон методом [doValidatePersonne]. Сторінка [edit.jsp] відображається у рядках 99–100.
14.9. Тестування веб-додатку
У розділі 14.1 було наведено низку тестів. Пропонуємо читачеві повторити їх. Тут ми наводимо інші знімки екрана, що ілюструють випадки конфліктів доступу до даних у багатокористувацькому середовищі:
[Firefox] — це браузер користувача U1. Він запитує URL-адресу [http://localhost:8080/personnes-01]:

[IE] — це браузер користувача U2. Він запитує той самий URL:

Користувач U1 переходить до редагування запису про особу [Lemarchand]:

Користувач U2 робить те саме:

Користувач U1 вносить зміни та підтверджує їх:
![]() |
Користувач U2 робить те саме:
![]() |
Користувач U2 повертається до списку осіб за посиланням [Annuler] у формі:

Він знаходить особу [Lemarchand] у тому вигляді, в якому її змінив U1. Тепер U2 видаляє [Lemarchand]:
![]() |
U1 як і раніше має власний список і хоче знову змінити [Lemarchand]:
![]() |
U1 використовує посилання [Retour à la liste], щоб дізнатися, про що йдеться:

Він виявляє, що [Lemarchand] дійсно більше не входить до списку...
14.10. Conclusion
Ми реалізували архітектуру MVC у трирівневій архітектурі [web, metier, dao] на прикладі простого управління списком осіб. Це дозволило нам застосувати концепції, представлені в попередніх розділах. У розглянутій версії список осіб зберігався в оперативній пам’яті. Незабаром ми розглянемо версії, в яких цей список зберігатиметься в таблиці бази даних.
Але спочатку ми ознайомимося з інструментом під назвою Spring IoC, який полегшує інтеграцію різних рівнів додатка ntier.

















