3. Стаття 2 — Приклади трирівневих веб-архітектур
Мета статті:
- тришарові архітектури
- базова веб-архітектура MVC
- архітектура Struts MVC
- архітектура Spring MVC
Використані інструменти:
- Spring: http://www.springframework.org/
- Ibatis SqlMap: http://www.ibatis.com/
- Junit: http://www.junit.org/index.htm
- Eclipse: http://www.eclipse.org/
- Struts: http://struts.apache.org/
- Firebird: http://firebird.sourceforge.net/: SGBD, драйвер JDBC. Насправді підійде будь-який драйвер JDBC.
- IBExpert, персональна версія: http://www.hksoftware.net/download/ibep_2005.2.14.1_full.exe (березень 2005 р.). IBExpert дозволяє графічно адмініструвати SGBD Firebird.
- Tomcat: http://jakarta.apache.org/tomcat/
- Плагін Tomcat для Eclipse: http://www.sysdeo.com/eclipse/tomcatPlugin.html. Див. також документ https://tahe.developpez.com/java/eclipse/
Для розуміння цього документа необхідні певні попередні знання. Деякі з них можна отримати з документів, які я написав. У цьому випадку я наводжу посилання на них. Зрозуміло, що це лише рекомендація, і читач може користуватися своїми улюбленими документами.
- мова Java: [https://tahe.developpez.com/java/cours]
- веб-програмування на Java: [https://tahe.developpez.com/java/web/]
- веб-програмування з використанням Java, Eclipse, Tomcat: [https://tahe.developpez.com/java/eclipse/]
- веб-програмування за допомогою Struts: [https://tahe.developpez.com/java/struts/]
- використання аспекту IoC у Spring: [https://tahe.developpez.com/java/springioc]
- Бібліотека тегів JSTL: [https://tahe.developpez.com/java/eclipse/] (частково)
- документація Ibatis SqlMap: [https://prdownloads.sourceforge.net/ibatisnet/DevGuide.pdf?download]
- Firebird: [http://firebird.sourceforge.net/pdfmanual/Firebird-1.5-QuickStart.pdf] (березень 2005 р.).
Ідеї, викладені в цьому документі, походять із книги, прочитаної влітку 2004 року — чудової праці Рода Джонсона: J2EE «Development without EJB», виданої видавництвом Wrox.
3.1. Веб-додаток «webarticles»
Тут ми хочемо навести деякі елементи веб-додатку для електронної комерції. Цей додаток дозволить користувачам Інтернету
- переглядати перелік товарів із бази даних
- додавати деякі з них до електронного кошика
- підтвердити його. Єдиним наслідком такого підтвердження буде оновлення в базі даних запасів придбаних товарів.
Користувачеві будуть доступні такі екрани:
- екран [LISTE], що відображає перелік товарів, які продаються

- вигляд [INFOS], що містить додаткову інформацію про товар:

- погляд [PANIER], що відображає вміст кошика клієнта

- погляд [PANIERVIDE] на випадок, якщо кошик клієнта порожній

- вигляд [ERREURS], який повідомляє про будь-яку помилку в роботі додатка

3.2. Загальна архітектура додатка
Ми хочемо створити додаток із такою трирівневою структурою:
- три рівні є незалежними завдяки використанню інтерфейсів Java
- інтеграція різних рівнів здійснюється за допомогою Spring
- кожен рівень реалізовано в окремих пакетах: web (рівень користувацького інтерфейсу), domain (бізнес-рівень) та dao (рівень доступу до даних).
Тут ми припустимо, що рівні [domain] та [dao] вже готові. Ми зосередимося лише на рівні [web], який пропонуємо реалізувати кількома способами:
- за допомогою класичної технології сервлетів-контролерів — сторінки JSP
- за допомогою технології Struts MVC
- за допомогою технології Spring — MVC
У будь-якому випадку додаток відповідатиме архітектурі MVC (Модель — Вигляд — Контролер). Якщо повернутися до наведеної вище схеми з шарами, архітектура MVC вписується в неї наступним чином:
Обробка запиту клієнта відбувається за такими етапами:
- клієнт надсилає запит до контролера. Цей контролер — це сервлет, через який проходять усі запити клієнтів. Це вхідні ворота додатка. Це літера «C» у MVC.
- контролер обробляє цей запит. Для цього йому може знадобитися допомога бізнес-шару, який у структурі MVC позначається літерою «M».
- контролер отримує відповідь від бізнес-шару. Запит клієнта було оброблено. Це може призвести до кількох можливих відповідей. Класичним прикладом є
- сторінка з повідомленням про помилку, якщо запит не вдалося обробити належним чином
- сторінка підтвердження в іншому випадку
- контролер обирає відповідь (= представлення), яку слід надіслати клієнту. Найчастіше це сторінка, що містить динамічні елементи. Контролер передає їх до представлення.
- Вигляд надсилається клієнту. Це «V» у MVC.
3.3. Модель
Тут ми розглядаємо «М» у MVC. Модель у цьому випадку складається з таких елементів:
- бізнес-класи
- класи доступу до даних
- база даних
3.3.1. База даних
База даних містить лише одну таблицю з назвою ARTICLES. Вона була згенерована за допомогою таких команд SQL:
| CREATE TABLE ARTICLES (
ID INTEGER NOT NULL,
NOM VARCHAR(30) NOT NULL,
PRIX NUMERIC(15,2) NOT NULL,
STOCKACTUEL INTEGER NOT NULL,
STOCKMINIMUM INTEGER NOT NULL
);
/* обмеження */
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKACTUEL check (STOCKACTUEL>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKMINIMUM check (STOCKMINIMUM>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_PRIX check (PRIX>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_NOM check (NOM<>'');
/* первинний ключ */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
|
| первинний ключ, що однозначно ідентифікує товар |
| назва товару |
| її ціна |
| поточний запас |
| запас, при недосягненні якого необхідно оформити замовлення на поповнення запасів |
У наведених нижче тестах використовувалася база даних [Firebird]. [Firebird] є «відкритим» варіантом SGBD. Драйвер JDBC [firebirdsql-full.jar] розміщено в папці [WEB-INF/lib] веб-додатку.
3.3.2. Пакеты шаблона
Модель M надається у вигляді трьох архівів:
- istia.st.articles.dao: містить класи доступу до даних шару [dao]
- istia.st.articles.exception: містить клас винятків для цієї системи управління товарами
- istia.st.articles.domain: містить бізнес-класи рівня [domain]
archive | contenu | rôle |
istia.st.articles.dao | - містить пакет [istia.st.articles.dao], який, у свою чергу, містить такі елементи: - [IArticlesDao]: інтерфейс доступу до рівня Dao. Це єдиний інтерфейс, який бачить рівень [domain]. Інших він не бачить. - [Article]: клас, що визначає статтю - [ArticlesDaoSqlMap]: клас реалізації інтерфейсу [IArticlesDao] за допомогою інструменту SqlMap | шар доступу до даних — повністю розміщений у шарі [dao] трирівневої архітектури веб-додатку |
istia.st.articles.domain | - містить пакет [istia.st.articles.domain], який, у свою чергу, містить такі елементи: - [IArticlesDomain]: інтерфейс доступу до шару [domain]. Це єдиний інтерфейс, який бачить веб-шар. Інших він не бачить. - [AchatsArticles]: клас, що реалізує [IArticlesDomain] - [Achat]: клас, що представляє покупку клієнта - [Panier]: клас, що представляє сукупність покупок клієнта | представляє модель здійснення покупок в Інтернеті — повністю розміщується в шарі [domain] трирівневої архітектури веб-додатку |
istia.st.articles.exception | - містить пакет [istia.st.articles.exception], який, у свою чергу, містить такі елементи: - [UncheckedAccessArticlesException]: клас, що визначає виняток типу [RuntimeException]. Цей тип винятку генерується шаром [dao], щойно виникає проблема з доступом до даних. | |
3.3.3. Пакет [istia.st.articles.dao]
Клас, що визначає статтю, має такий вигляд:
| package istia.st.articles.dao;
import istia.st.articles.exception.UncheckedAccessArticlesException;
/**
* @author ST - ISTIA
*
*/
public class Article {
private int id;
private String nom;
private double prix;
private int stockActuel;
private int stockMinimum;
/**
* constructeur par défaut
*/
public Article() {
}
public Article(int id, String nom, double prix, int stockActuel,
int stockMinimum) {
// ініціалізація атрибутів екземпляра
setId(id);
setNom(nom);
setPrix(prix);
setStockActuel(stockActuel);
setStockMinimum(stockMinimum);
}
// геттери — сеттери
public int getId() {
return id;
}
public void setId(int id) {
// ідентифікатор дійсний?
if (id < 0)
throw new UncheckedAccessArticlesException("id[" + id + "] invalide");
this.id = id;
}
public String getNom() {
return nom;
}
public void setNom(String nom) {
// ім'я дійсне?
if(nom==null || nom.trim().equals("")){
throw new UncheckedAccessArticlesException("Le nom est [null] ou vide");
}
this.nom = nom;
}
public double getPrix() {
return prix;
}
public void setPrix(double prix) {
// ціна дійсна?
if(prix<0) throw new UncheckedAccessArticlesException("Prix["+prix+"]invalide");
this.prix = prix;
}
public int getStockActuel() {
return stockActuel;
}
public void setStockActuel(int stockActuel) {
// Чи правильний запас?
if (stockActuel < 0)
throw new UncheckedAccessArticlesException("stockActuel[" + stockActuel + "] invalide");
this.stockActuel = stockActuel;
}
public int getStockMinimum() {
return stockMinimum;
}
public void setStockMinimum(int stockMinimum) {
// запаси дійсні?
if (stockMinimum < 0)
throw new UncheckedAccessArticlesException("stockMinimum[" + stockMinimum + "] invalide");
this.stockMinimum = stockMinimum;
}
public String toString() {
return "[" + id + "," + nom + "," + prix + "," + stockActuel + ","
+ stockMinimum + "]";
}
}
|
Цей клас надає:
- конструктор, що дозволяє задати 5 параметрів товару
- методи доступу, які часто називають геттерами/сеттерами, що слугують для зчитування та запису цих 5 елементів інформації. Назви цих методів відповідають стандарту JavaBean. Використання об’єктів JavaBean у шарі DAO для взаємодії з даними SGBD є стандартною практикою.
- перевірка даних, введених у товарну позицію. У разі виявлення помилкових даних генерується виняток.
- метод toString, який дозволяє отримати значення позиції у вигляді символьного рядка. Це часто буває корисним для налагодження програми.
Інтерфейс [IArticlesDao] визначено наступним чином:
| package istia.st.articles.dao;
import istia.st.articles.domain.Article;
import java.util.List;
/**
* @author ST-ISTIA
*
*/
public interface IArticlesDao {
/**
* @return : liste de tous les articles
*/
public List getAllArticles();
/**
* @param unArticle :
* l'article à ajouter
*/
public int ajouteArticle(Article unArticle);
/**
* @param idArticle :
* id de l'article à supprimer
*/
public int supprimeArticle(int idArticle);
/**
* @param unArticle :
* l'article à modifier
*/
public int modifieArticle(Article unArticle);
/**
* @param idArticle :
* id de l'article cherché
* @return : l'article trouvé ou null
*/
public Article getArticleById(int idArticle);
/**
* vide la table des articles
*/
public void clearAllArticles();
/**
*
* @param idArticle id de l'article dont on change le stock
* @param mouvement valeur à ajouter au stock (valeur signée)
*/
public int changerStockArticle(int idArticle, int mouvement);
}
|
Роль різних методів інтерфейсу така:
| повертає всі позиції з таблиці ARTICLES у вигляді списку об’єктів [Article] |
| очищає таблицю ARTICLES |
| повертає об’єкт [Article], ідентифікований за його первинним ключем |
| дозволяє додати запис до таблиці ARTICLES |
| дозволяє змінити запис у таблиці [ARTICLES] |
| дозволяє видалити товар із таблиці [ARTICLES] |
| дозволяє змінити залишок товару в таблиці [ARTICLES] |
Інтерфейс надає клієнтським програмам певну кількість методів, визначених виключно їхніми сигнатурами. Він не визначає, як ці методи будуть фактично реалізовані. Це забезпечує гнучкість у роботі додатка. Клієнтська програма звертається до інтерфейсу, а не до його конкретної реалізації.
Вибір конкретної реалізації здійснюється за допомогою конфігураційного файлу Spring. Тут ми пропонуємо реалізувати інтерфейс IArticlesDao за допомогою продукту з відкритим кодом під назвою SqlMap. Це дозволить нам видалити всі інструкції SQL з коду Java.
Клас реалізації [ArticlesDaoSqlMap] визначається наступним чином:
| package istia.st.articles.dao;
// Імпорт
import com.ibatis.sqlmap.client.SqlMapClient;
import istia.st.articles.domain.Article;
import java.util.List;
public class ArticlesDaoSqlMap implements IArticlesDao {
// Поля
private SqlMapClient sqlMap;
// Конструктори
public ArticlesDaoSqlMap(String sqlMapConfigFileName) { }
// Методи
public SqlMapClient getSqlMap() {}
public void setSqlMap(SqlMapClient sqlMap) { }
public synchronized List getAllArticles() {}
public synchronized int ajouteArticle(Article unArticle) {}
public synchronized int supprimeArticle(int idArticle) {}
public synchronized int modifieArticle(Article unArticle) {}
public synchronized Article getArticleById(int idArticle) {}
public synchronized void clearAllArticles() { }
public synchronized int changerStockArticle(int idArticle, int mouvement) {}
}
|
Усі методи доступу до даних було синхронізовано, щоб уникнути проблем з паралельним доступом до джерела даних. У будь-який момент часу лише один потік має доступ до певного методу.
Клас [ArticlesDaoSqlMap] використовує інструмент [Ibatis SqlMap]. Перевага цього інструменту полягає в тому, що він дозволяє винести код SQL, що забезпечує доступ до даних, з коду Java. Після цього він розміщується у файлі конфігурації. Ми ще повернемося до цього пізніше. Для своєї інстанціації клас [ArticlesDaoSqlMap] потребує конфігураційного файлу, ім’я якого передається як параметр конструктору класу. Цей конфігураційний файл визначає інформацію, необхідну для:
- доступ до SGBD, у якому містяться статті
- управління пулом з’єднань
- управління транзакціями
У нашому прикладі він матиме назву [sqlmap-config-firebird.xml] і визначатиме доступ до бази даних Firebird:
| <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-config-2.dtd">
<sqlMapConfig>
<transactionManager type="JDBC">
<dataSource type="SIMPLE">
<property name="JDBC.Driver" value="org.firebirdsql.jdbc.FBDriver"/>
<property name="JDBC.ConnectionURL"
value="jdbc:firebirdsql:localhost/3050:D:/data/Databases/firebird/dbarticles.gdb"/>
<property name="JDBC.Username" value="sysdba"/>
<property name="JDBC.Password" value="masterkey"/>
<property name="JDBC.DefaultAutoCommit" value="true"/>
</dataSource>
</transactionManager>
<sqlMap resource="articles.xml"/>
</sqlMapConfig>
|
Згаданий вище файл конфігурації [articles.xml] дозволяє визначити, як створити екземпляр класу [istia.st.articles.dao.Article] на основі рядка з таблиці [ARTICLES] у SGBD. Він також визначає запити SQL, які дозволять шару [dao] отримувати дані з джерела даних Firebird.
| <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap namespace="Articles">
<!-- псевдонім для класу istia.st.articles.dao.Article -->
<typeAlias alias="article" type="istia.st.articles.dao.Article"/>
<!-- маппінг ORM : рядок таблиці ARTICLES — екземпляр класу Article -->
<resultMap id="article" class="article">
<result property="id" column="ID"/>
<result property="nom" column="NOM"/>
<result property="prix" column="PRIX"/>
<result property="stockActuel" column="STOCKACTUEL"/>
<result property="stockMinimum" column="STOCKMINIMUM"/>
</resultMap>
<!-- запит SQL для отримання всіх статей -->
<statement id="getAllArticles" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum from ARTICLES
</statement>
<!-- запит SQL для видалення всіх статей -->
<statement id="clearAllArticles">delete from ARTICLES</statement>
<!-- запит SQL для додавання товару -->
<statement id="insertArticle">
insert into ARTICLES (id, nom, prix,
stockactuel, stockminimum) values
(#id#,#назва#,#ціна#,#поточнийзапас#,#мінімальнийзапас#)
</statement>
<!-- запит SQL для видалення певного товару -->
<statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>
<!-- запит SQL для редагування певного товару -->
<statement id="modifyArticle">
update ARTICLES set nom=#назва#,
prix=#ціна#,поточнийзапас=#поточнийзапас#,мінімальнийзапас=#мінімальнийзапас# where
id=#id#
</statement>
<!-- запит SQL для отримання певного товару -->
<statement id="getArticleById" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>
<!-- запит SQL для зміни запасу певного товару -->
<statement id="changerStockArticle">
update ARTICLES set
stockActuel=stockActuel+#рух#
where id=#id# та stockActuel+#рух#>=0
</statement>
</sqlMap>
|
Код пакета [dao] наведено у додатку.
3.3.4. Пакет [istia.st.articles.domain]
Інтерфейс [IArticlesDomain] відокремлює рівень [métier] від рівня [web]. Останній отримує доступ до шару [métier/domain] через цей інтерфейс, не зважаючи на те, який саме клас його фактично реалізує. Інтерфейс визначає такі дії для доступу до бізнес-шару:
| package istia.st.articles.domain;
// Імпорт
import java.util.ArrayList;
import java.util.List;
public abstract interface IArticlesDomain {
// Методи
void acheter(Panier panier);
List getAllArticles();
Article getArticleById(int idArticle);
ArrayList getErreurs();
}
|
| повертає список об’єктів [Article] для відображення клієнту |
Article getArticleById(int idArticle)
| повертає об’єкт [Article], ідентифікований за [idArticle] |
void acheter(Panier panier)
| підтверджує кошик клієнта, зменшуючи запаси придбаних товарів на кількість, що була придбана — може завершитися невдачею, якщо запасів недостатньо |
| повертає список помилок, що виникли — порожній, якщо помилок немає |
Тут інтерфейс [IArticlesDomain] буде реалізовано за допомогою наступного класу [AchatsArticles]:
| package istia.st.articles.domain;
// Імпорт
import istia.st.articles.dao.IArticlesDao;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.List;
public class AchatsArticles implements IArticlesDomain {
// Поля
private IArticlesDao articlesDao;
private ArrayList erreurs;
// Конструктори
public AchatsArticles(IArticlesDao articlesDao) { }
// Методи
public ArrayList getErreurs() {}
public List getAllArticles() {}
public Article getArticleById(int id) {}
public void acheter(Panier panier) { }
}
|
Цей клас реалізує чотири методи інтерфейсу [IArticlesDomain]. Він має два приватні поля:
| об’єкт доступу до даних, наданий рівнем доступу до даних |
| список можливих помилок |
Щоб створити екземпляр класу, потрібно надати об’єкт, що забезпечує доступ до даних SGBD:
public AchatsArticles(IArticlesDao articlesDao)
| конструктор |
Клас [Achat] представляє покупку клієнта:
| package istia.st.articles.domain;
public class Achat {
// Поля
private Article article;
private int qte;
// Конструктори
public Achat(Article article, int qte) { }
// Методи
public double getTotal() {}
public Article getArticle() {}
public void setArticle(Article article) { }
public int getQte() {}
public void setQte() { }
public String toString() {}
}
|
Клас [Achat] є класом JavaBean із такими полями та методами:
| придбаний товар |
| кількість, що була придбана |
| сума покупки |
| ідентифікаційний рядок об'єкта |
Клас [Panier] представляє сукупність покупок клієнта:
| package istia.st.articles.domain;
// Імпорт
import java.util.ArrayList;
public class Panier {
// Поля
private ArrayList achats;
// Конструктори
public Panier() { }
// Методи
public ArrayList getAchats() {}
public void ajouter(Achat unAchat) { }
public void enlever(int idAchat) { }
public double getTotal() {}
public String toString() { }
}
|
Клас [Panier] є класом JavaBean із такими полями та методами:
| список покупок клієнта — список об’єктів типу [Achat] |
void ajouter(Achat unAchat)
| додає покупку до списку покупок |
void enlever(int idArticle)
| видаляє покупку товару idArticle |
| показує загальну суму покупок |
| повертає ідентифікаційний рядок кошика |
| повертає список покупок |
Код пакета [domain] наведено в додатку.
3.3.5. Пакет [istia.st.articles.exception]
Цей пакет містить клас, що визначає виняток, який генерує рівень [dao] у разі виникнення проблеми з доступом до джерела даних:
| package istia.st.articles.exception;
public class UncheckedAccessArticlesException
extends RuntimeException {
public UncheckedAccessArticlesException() {
super();
}
public UncheckedAccessArticlesException(String mesg) {
super(mesg);
}
public UncheckedAccessArticlesException(String mesg, Throwable th) {
super(mesg, th);
}
}
|
3.3.6. Тестування моделі
Модель M було протестовано в Eclipse з такою конфігурацією:

Коментарі:
- у [WEB-INF/lib] містяться:
- архіви, необхідні для інструменту [ibatis SqlMap], який відповідає за доступ до SGBD Firebird: ibatis-*.jar
- архів, необхідний для інструменту [spring]: spring.jar
- драйвер JDBC для SGBD та [Firebird]: firebirdsql-full.jar
- архіви, необхідні для логів: log4-*.jar, commons-logging.jar
- три архіви тестованої моделі: istia.st.articles.*.jar
- архів, необхідний для інструменту тестування [junit]
- у [WEB-INF/src] містяться файли конфігурації, які Eclipse автоматично скопіює до [WEB-INF/classes]:
- конфігураційні файли інструменту [sqlmap]: sqlmap-config-firebird.xml, articles.xml
- конфігураційний файл інструменту [spring]: spring-config-test-dao.xml, spring-config-test-domain.xml
- файл конфігурації інструменту [log4j]: log4j.properties
- у пакеті [istia.st.articles.tests] містяться класи тестів моделі
3.3.6.1. Тести шару [dao]
Клас тестування JUnit для шару [dao] має такий вигляд. Його аналіз дозволяє зрозуміти, як використовуються методи інтерфейсу [IArticlesDao]:
| package istia.st.articles.tests.dao;
import java.util.List;
import junit.framework.TestCase;
import istia.st.articles.dao.IArticlesDao;
import istia.st.articles.dao.Article;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
// тестування класу ArticlesDaoSqlMap
public class JunitModeleDaoArticles extends TestCase {
// екземпляр класу, що тестується
private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// отримує екземпляр доступу до даних
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-dao.xml"))).getBean("articlesDao");
}
public void testGetAllArticles() {
// відображає товари
listArticles();
}
public void testClearAllArticles() {
// очищає таблицю статей
articlesDao.clearAllArticles();
// зчитує таблицю ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
}
public void testAjouteArticle() {
// видалення вмісту з ARTICLES
articlesDao.clearAllArticles();
// зчитує таблицю ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// вставка
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// зчитує таблицю ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
//— відображення
listArticles();
}
public void testSupprimeArticle() {
// видалення вмісту з ARTICLES
articlesDao.clearAllArticles();
// зчитує таблицю ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// вставка
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// читає таблицю ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
// видалення
articlesDao.supprimeArticle(4);
// читає таблицю ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(1, articles.size());
// відображає таблицю
listArticles();
}
public void testModifieArticle() {
// видалення вмісту з ARTICLES
articlesDao.clearAllArticles();
// зчитує таблицю ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// вставка
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// читає таблицю ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
// getById
Article unArticle = articlesDao.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getNom(), "article4");
// зміна
articlesDao.modifieArticle(new Article(4, "article4", 44, 44, 44));
// getById
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getPrix(), 44, 1e-6);
// відображає таблицю
listArticles();
}
public void testGetArticleById() {
// видалення вмісту з ARTICLES
articlesDao.clearAllArticles();
// читає таблицю ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// вставка
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// читає таблицю ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
// getById
Article unArticle = articlesDao.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getNom(), "article4");
}
private void listArticles() {
// зчитує таблицю ARTICLES
List articles = articlesDao.getAllArticles();
// відображаються прочитані статті
for (int i = 0; i < articles.size(); i++) {
System.out.println(((Article) articles.get(i)).toString());
}
}
public void testChangerStockArticle() throws InterruptedException {
// видалення вмісту з ARTICLES
articlesDao.clearAllArticles();
// вставка
int nbArticles = articlesDao.ajouteArticle(new Article(3, "article3",
30, 101, 3));
assertEquals(nbArticles, 1);
nbArticles = articlesDao.ajouteArticle(new Article(4, "article4", 40,
40, 4));
assertEquals(nbArticles, 1);
// створення 100 потоків для оновлення запасів товару № 3
Thread[] taches = new Thread[100];
for (int i = 0; i < taches.length; i++) {
taches[i] = new ThreadMajStock("thread-" + i, articlesDao);
taches[i].start();
}
// очікування завершення потоків
for (int i = 0; i < taches.length; i++) {
taches[i].join();
}
// отримати товар № 3 та перевірити його запаси
Article unArticle = articlesDao.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
assertEquals(1, unArticle.getStockActuel());
// зміна запасів товару № 4
boolean erreur = false;
int nbLignes = articlesDao.changerStockArticle(4, -100);
assertEquals(0, nbLignes);
// виводить таблицю
listArticles();
}
}
|
Коментарі:
- тестовий клас за допомогою свого методу setUp зберігає екземпляр класу, що тестується:
| // екземпляр тестованого класу
private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// отримує екземпляр класу доступу до даних
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-dao.xml"))).getBean("articlesDao");
}
|
- об’єкт, що тестується, надається методом [Spring]. Вище запитується Spring-бін із назвою [articlesDao]. Цей бін визначений у конфігураційному файлі Spring [spring-config-test-dao.xml]:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- клас доступу до даних -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
</beans>
|
Як бачимо вище, bean [articlesDao] є екземпляром класу [istia.st.articles.dao.ArticlesDaoSqlMap]. Цей клас має конструктор, який як параметр очікує ім’я конфігураційного файлу інструменту [SqlMap]. Ця назва вказана тут. Це [sqlmap-config-firebird.xml]. Останній вже було описано. Він надає всю необхідну інформацію для доступу до даних SGBD.
Метод [testChangerStockArticle] створює 100 потоків, завданням яких є зменшення запасів певного товару. Мета цього тесту — перевірити паралельний доступ до SGBD. Оскільки метод [changerStockArticle] класу [istia.st.articles.dao.ArticlesDaoSqlMap] було синхронізовано, цей тест проходить успішно. Якщо прибрати синхронізацію, тест більше не проходить. Клас, відповідальний за оновлення запасів, має такий вигляд:
| package istia.st.articles.tests;
import istia.st.articles.dao.IArticlesDao;
public class ThreadMajStock extends Thread {
/**
* nom du thread
*/
private String name;
/**
* objet d'accès aux données
*/
private IArticlesDao articlesDao;
/**
*
* @param name
* le nom du thread afin de l'identifier
* @param articlesDao
* l'objet d'accès aux données du sgbd
*/
public ThreadMajStock(String name, IArticlesDao articlesDao) {
this.name = name;
this.articlesDao = articlesDao;
}
/**
* décrémente le stock de l'article 3 d'une unité fait un suivi écran des
* opérations
*/
public void run() {
// відстеження
System.out.println(name + " lancé");
// зміна запасів товару № 3
articlesDao.changerStockArticle(3, -1);
// відстеження
System.out.println(name + " terminé");
}
}
|
- зазначений вище клас зменшує запас товару № 3 на 1
3.3.6.2. Тести рівня [domain]
Тестовий клас JUnit для шару [domain] має такий вигляд:
| package istia.st.articles.tests.domain;
import java.util.List;
import junit.framework.TestCase;
import istia.st.articles.dao.Article;
import istia.st.articles.dao.IArticlesDao;
import istia.st.articles.domain.Achat;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.domain.Panier;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
// тестування класу ArticlesDaoSqlMap
public class JunitModeleDomainArticles extends TestCase {
// екземпляр класу доступу до домену
private IArticlesDomain articlesDomain;
// екземпляр класу доступу до даних
private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// отримує екземпляр класу доступу до домену
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource("spring-config-test-domain.xml")))
.getBean("articlesDomain");
// отримує екземпляр класу доступу до даних
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-domain.xml"))).getBean("articlesDao");
}
// отримання конкретного товару
public void testGetArticleById() {
// видалення вмісту з ARTICLES
articlesDao.clearAllArticles();
// читає таблицю ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// вставка
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// читає таблицю ARTICLES
articles = articlesDomain.getAllArticles();
assertEquals(2, articles.size());
// getById
Article unArticle = articlesDomain.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getNom(), "article4");
}
// виведення на екран
private void listArticles() {
// зчитує таблицю ARTICLES
List articles = articlesDomain.getAllArticles();
// відображаються прочитані статті
for (int i = 0; i < articles.size(); i++) {
System.out.println(((Article) articles.get(i)).toString());
}
}
// придбання статей
public void testAchatPanier(){
// видалення вмісту з ARTICLES
articlesDao.clearAllArticles();
// зчитує таблицю ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// вставка
Article article3=new Article(3, "article3", 30, 30, 3);
articlesDao.ajouteArticle(article3);
Article article4=new Article(4, "article4", 40, 40, 4);
articlesDao.ajouteArticle(article4);
// читає таблицю ARTICLES
articles = articlesDomain.getAllArticles();
assertEquals(2, articles.size());
// створення кошика з двома товарами
Panier panier=new Panier();
panier.ajouter(new Achat(article3,10));
panier.ajouter(new Achat(article4,10));
// перевірка
assertEquals(700.0,panier.getTotal(),1e-6);
assertEquals(2,panier.getAchats().size());
// підтвердження кошика
articlesDomain.acheter(panier);
// перевірки
assertEquals(0,articlesDomain.getErreurs().size());
assertEquals(0,panier.getAchats().size());
// пошук товару № 3
article3=articlesDomain.getArticleById(3);
assertEquals(20,article3.getStockActuel());
// пошук товару № 4
article4=articlesDomain.getArticleById(4);
assertEquals(30,article4.getStockActuel());
// новий кошик
panier.ajouter(new Achat(article3,100));
// підтвердження кошика
articlesDomain.acheter(panier);
// перевірка — куплено забагато
// мабуть, сталася помилка
assertEquals(1,articlesDomain.getErreurs().size());
// пошук товару № 3
article3=articlesDomain.getArticleById(3);
// запаси, мабуть, не змінилися
assertEquals(20,article3.getStockActuel());
}
// вилучити покупки
public void testRetirerAchats(){
// видалення вмісту з ARTICLES
articlesDao.clearAllArticles();
// читає таблицю ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// вставка
Article article3=new Article(3, "article3", 30, 30, 3);
articlesDao.ajouteArticle(article3);
Article article4=new Article(4, "article4", 40, 40, 4);
articlesDao.ajouteArticle(article4);
// читає таблицю ARTICLES
articles = articlesDomain.getAllArticles();
assertEquals(2, articles.size());
// створення кошика з двома товарами
Panier panier=new Panier();
panier.ajouter(new Achat(article3,10));
panier.ajouter(new Achat(article4,10));
// перевірка
assertEquals(700.0,panier.getTotal(),1e-6);
assertEquals(2,panier.getAchats().size());
// додавання товару, який вже було придбано
panier.ajouter(new Achat(article3,10));
// перевірки
// сума повинна бути збільшена до 1000
assertEquals(1000.0,panier.getTotal(),1e-6);
// у кошику як і раніше 2 товари
assertEquals(2,panier.getAchats().size());
// кількість товару № 3 має бути змінена на 20
Achat achat=(Achat)panier.getAchats().get(0);
assertEquals(20,achat.getQte());
// товар № 3 видаляється з кошика
panier.enlever(3);
// перевірки
// загальна сума має бути змінена на 400
assertEquals(400.0,panier.getTotal(),1e-6);
// у кошику має бути лише 1 товар
assertEquals(1,panier.getAchats().size());
// це має бути товар № 4
assertEquals(4,((Achat)panier.getAchats().get(0)).getArticle().getId());
}
}
|
Коментарі:
- клас тестування за допомогою свого методу setUp зберігає екземпляр класу, що тестується, а також екземпляр класу доступу до даних. Останній пункт є спірним. Теоретично тестовий клас не повинен мати доступу до шару [dao], про існування якого він навіть не повинен знати. У даному випадку ми пішли в обхід цієї «етики», дотримання якої змусило б нас створити нові методи в нашому інтерфейсі [IArticlesDomain].
| // екземпляр класу доступу до домену
private IArticlesDomain articlesDomain;
// екземпляр класу доступу до даних
private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// отримує екземпляр доступу до домену
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource("spring-config-test-domain.xml")))
.getBean("articlesDomain");
// отримує екземпляр класу доступу до даних
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-domain.xml"))).getBean("articlesDao");
}
|
- об’єкт, що тестується, надається [Spring]. Вище ми запитуємо Spring-бін під назвою [articlesDomain]. Цей бін визначений у конфігураційному файлі Spring [spring-config-test-domain.xml]:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- клас доступу до даних -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- бізнес-клас -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
</beans>
|
Як бачимо вище, bean [articlesDomain] є екземпляром класу [istia.st.articles.domain.AchatsArticles]. Цей клас має конструктор, який як параметр очікує об’єкт доступу до шару [dao] типу [IArticlesDao]. У цьому випадку файл конфігурації вказує, що цим об’єктом є бін із назвою [articlesDao]. Це змушує Spring створити екземпляр цього біна. Про створення екземпляра біна [articlesDao] йшлося раніше. Отже, у підсумку було створено два екземпляри бінів:
- [articlesDao] типу [istia.st.articles.dao.ArticlesDaoSqlMap]
- [articlesDomain] типу [ istia.st.articles.domain.AchatsArticles]
Ці дві інстанції були створені внаслідок першого виклику Spring:
| // отримує екземпляр доступу до домену
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource("spring-config-test-domain.xml")))
.getBean("articlesDomain");
|
У результаті отримуємо бін [articlesDomain]. Під час другого виклику Spring:
| // отримує екземпляр доступу до даних
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-domain.xml"))).getBean("articlesDao");
|
[Spring] просто повертає посилання на бін [articlesDao], який вже був створений під час попереднього виклику. Це принцип синглтона. Якщо ми запитуємо бін у Spring, він створює його екземпляр, якщо він ще не існує, інакше повертає посилання на існуючий бін.
3.4. Трирівневий веб-додаток MVC
Далі ми хочемо створити такий трирівневий веб-додаток:
Додаток матиме архітектуру MVC. Модель M вже написано та протестовано. Це та сама модель, що описана раніше. Вона надається нам у трьох архівах [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception]. Нам потрібно написати контролер C та представлення V.
Спочатку розглянемо класичний підхід, за якого:
- контролер C реалізується за допомогою єдиного сервлета
- види V забезпечуються сторінками JSP
3.5. Архітектура MVC на основі сервлета-контролера та сторінок JSP
Архітектура MVC додатка буде такою:
| бізнес-класи, класи доступу до даних та база даних |
| сторінки JSP |
| сервлет для обробки запитів клієнтів |
3.5.1. Модель
Вона була представлена раніше. Складається з Java-архівів [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].
3.5.2. Види
Види відповідають тим, що були представлені на початку документа:
| liste.jsp | Види зібрані у папці [vues] додатка  |
| infos.jsp |
| panier.jsp |
| paniervide.jsp |
| erreurs.jsp |
3.5.3. Контролер
Контролер складатиметься з єдиного сервлету під назвою [WebArticles]. Він оброблятиме різні запити клієнтів. Ці запити будуть формалізовані наявністю параметра [action] у запиті HTTP клієнта:
| значення | дія контролера | можливі відповіді |
| клієнт хоче отримати список товарів | - запитує перелік товарів у бізнес-шарі бізнес-шару | - [LISTE] - [ERREURS] |
| клієнт запитує інформацію про один із товарів, відображених у вікні [LISTE] | - надсилає запит щодо товару до бізнес-шару | - [INFOS] - [ERREURS] |
| клієнт купує товар | - надсилає запит на товар до бізнес-шару та додає її до кошика клієнта | - [INFOS], якщо сталася помилка з кількістю - [LISTE], якщо помилки немає |
| клієнт хоче видалити товар зі свого кошика | - отримує кошик із сесії та змінює його | - [PANIER] - [PANIERVIDE] - [ERREURS] |
| клієнт хоче переглянути свій кошик | - отримує кошик із сесії | - [PANIER] - [PANIERVIDE] - [ERREURS] |
| клієнт завершив покупки і переходить до етапу оплати | - оновлює в базі даних залишки придбаних товарів - видалити з кошика клієнта товари, щодо яких придбання яких було підтверджено | - [LISTE] - [ERREURS] |
3.5.4. Налаштування додатка
Ми прагнемо налаштувати додаток таким чином, щоб він був максимально гнучким щодо таких змін, як:
- зміна URL-адрес різних переглядів
- зміна класів, що реалізують інтерфейси [IArticlesDao] та [IArticlesDomain]
- зміна SGBD, бази даних, таблиці статей
3.5.5. Зміни URL-адрес
Назви URL-адрес переглядів будуть розміщені у файлі конфігурації додатка [web.xml] разом з деякими іншими параметрами:
| <?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
<servlet-name>webarticles</servlet-name>
<servlet-class>istia.st.articles.web.WebArticles</servlet-class>
<init-param>
<param-name>springConfigFileName</param-name>
<param-value>spring-config-sqlmap-firebird.xml</param-value>
</init-param>
<init-param>
<param-name>urlMain</param-name>
<param-value>/main</param-value>
</init-param>
<init-param>
<param-name>urlErreurs</param-name>
<param-value>/vues/erreurs.jsp</param-value>
</init-param>
<init-param>
<param-name>urlListe</param-name>
<param-value>/vues/liste.jsp</param-value>
</init-param>
<init-param>
<param-name>urlInfos</param-name>
<param-value>/vues/infos.jsp</param-value>
</init-param>
<init-param>
<param-name>urlPanier</param-name>
<param-value>/vues/panier.jsp</param-value>
</init-param>
<init-param>
<param-name>urlPanierVide</param-name>
<param-value>/vues/paniervide.jsp</param-value>
</init-param>
<init-param>
<param-name>urlDebug</param-name>
<param-value>/vues/debug.jsp</param-value>
</init-param>
</servlet>
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
<servlet-mapping>
<servlet-name>webarticles</servlet-name>
<url-pattern>/main</url-pattern>
</servlet-mapping>
</web-app>
|
У файлі [web.xml] містяться
- URL-адреси різних подань додатка
- назва [springConfigFileName] файлу конфігурації Spring, який дозволить створювати об’єкти-сінглтони для доступу до бізнес-шару та шару DAO
- представлення [/vues/index.jsp], яке відображатиметься, коли URL-адреса, запитувана клієнтом, буде /<context>, де <context> — це контекст додатка
3.5.6. Зміна класів реалізації інтерфейсів
Відповідно до принципів тришарової архітектури, шари повинні бути ізольованими один від одного. Ця ізоляція досягається таким чином:
- шари взаємодіють між собою через інтерфейси, а не через конкретні класи
- код одного рівня ніколи не створює екземпляр класу іншого рівня для його використання. Він просто запитує у зовнішнього інструменту, в даному випадку [Spring], екземпляр реалізації інтерфейсу того рівня, який він хоче використовувати. Для цього, як ми знаємо, їй не потрібно знати ім’я класу реалізації, а лише ім’я Spring-біна, посилання на який вона хоче отримати.
У нашому додатку файл конфігурації Spring може виглядати так:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- клас доступу до даних -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- бізнес-клас -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
</beans>
|
Щоб отримати доступ до шару [métier], клас шару [Interface utilisateur, UI] може запросити бін [articlesDomain]. Тоді Spring створить екземпляр об’єкта типу [istia.st.articles.domain.AchatsArticles]. Для цього йому потрібен bean типу [articlesDao], тобто об’єкт типу [istia.st.articles.dao.ArticlesDaoSqlMap]. Тоді Spring створить екземпляр такого об’єкта. Це створення відбудеться на основі інформації, що міститься у файлі [sqlmap-config-firebird.xml] — файлі конфігурації доступу до даних через SqlMap. По завершенні операції клас [UI], який запросив бін [articlesDomain], має повний ланцюжок, що пов’язує його з даними SGBD:
3.5.7. Зміни, пов’язані з SGBD або базою даних
Незалежність веб-додатку від змін, пов’язаних із SGBD або базою даних, тут забезпечується конфігураційними файлами SqlMap. Їх є два:
- файл [sql-map-config-firebird.xml]
| <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-config-2.dtd">
<sqlMapConfig>
<transactionManager type="JDBC">
<dataSource type="SIMPLE">
<property name="JDBC.Driver" value="org.firebirdsql.jdbc.FBDriver"/>
<property name="JDBC.ConnectionURL"
value="jdbc:firebirdsql:localhost/3050:d:/data/databases/firebird/dbarticles.gdb"/>
<property name="JDBC.Username" value="sysdba"/>
<property name="JDBC.Password" value="masterkey"/>
<property name="JDBC.DefaultAutoCommit" value="true"/>
</dataSource>
</transactionManager>
<sqlMap resource="articles.xml"/>
</sqlMapConfig>
|
Цей файл вказує на базу даних Firebird. Достатньо змінити ім’я драйвера JDBC, щоб працювати з іншим — SGBD.
- файл [articles.xml], який містить різні інструкції SQL, необхідні для роботи програми:
| <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap namespace="Articles">
<!-- псевдонім класу istia.st.articles.dao.Article -->
<typeAlias alias="article" type="istia.st.articles.dao.Article"/>
<!-- маппінг ORM: р ядок таблиці ARTICLES — екземпляр класу Article -->
<resultMap id="article" class="article">
<result property="id" column="ID"/>
<result property="nom" column="NOM"/>
<result property="prix" column="PRIX"/>
<result property="stockActuel" column="STOCKACTUEL"/>
<result property="stockMinimum" column="STOCKMINIMUM"/>
</resultMap>
<!-- запит SQL для отримання всіх статей -->
<statement id="getAllArticles" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum from ARTICLES
</statement>
<!-- запит SQL для видалення всіх статей -->
<statement id="clearAllArticles">delete from ARTICLES</statement>
<!-- запит SQL для додавання товару -->
<statement id="insertArticle">
insert into ARTICLES (id, nom, prix,
stockactuel, stockminimum) values
(#id#,#назва#,#ціна#,#поточнийзапас#,#мінімальнийзапас#)
</statement>
<!-- запит SQL для видалення певного товару -->
<statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>
<!-- запит SQL для редагування певного товару -->
<statement id="modifyArticle">
update ARTICLES set nom=#назва#,
prix=#ціна#,поточнийзапас=#поточнийзапас#,мінімальнийзапас=#мінімальнийзапас# where
id=#id#
</statement>
<!-- запит SQL для отримання певного товару -->
<statement id="getArticleById" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>
<!-- запит SQL для зміни запасу певного товару -->
<statement id="changerStockArticle">
update ARTICLES set
stockActuel=stockActuel+#рух#
where id=#id# та stockActuel+#рух#>=0
</statement>
</sqlMap>
|
Якщо назви таблиці товарів або стовпців зміняться, доведеться переписати запити в цьому конфігураційному файлі, не змінюючи при цьому код Java. Те саме стосується й випадків, коли запит замінюється збереженою процедурою з міркувань продуктивності.
3.5.8. Загальна архітектура додатка [webarticles]
Веб-додаток на Java — це пазл із безліччю елементів. Впровадження архітектури MVC зазвичай збільшує їхню кількість. Структура додатка [webarticles] під [eclipse] є такою:
structure générale - on voit ci-dessous les
archives Java utilisées par le projet
Eclipse.
spring : pour Spring
ibatis : pour SqlMap
log4j, commons-logging : pour les logs
de Spring et Sqlmap
firebird : pour le SGBD firebird
mysql : pour le SGBD MySQL
jstl, standard : pour la bibliothèque
de balises JSTL
|
le dossier des sources java : contient le code Java
ainsi que les fichiers de configuration
spring et sqlmap. Eclipse recopie
automatiquement ces fichiers
dans [WEB-INF/classes].
C'est là que l'application les trouvera.
|
 |  |
le dossier [WEB-INF] de l'application : contient le
descripteur [web.xml] de l'application ainsi que le
fichiers de définition de la bibliothèques JSTL
| |
 |  |
3.5.9. Види JSP
Види JSP використовують бібліотеку тегів JSTL.
3.5.9.1. entete.jsp
Щоб забезпечити певну однорідність різних переглядів, вони матимуть спільний заголовок, у якому відображається назва додатка та меню:
Меню є динамічним і визначається контролером. Контролер додає до запиту, що надсилається на сторінку JSP, атрибут-ключ «actions», значенням якого є масив типу Hastable[]. Кожен елемент цього масиву є словником, призначеним для формування пункту меню у заголовку. Кожен словник має два ключі:
- href: URL-адреса, пов’язана з пунктом меню
- lien: текст меню
Інші подання додатка використовуватимуть заголовок, визначений [entete.jsp], за допомогою такого тегу JSP:
<jsp:include page="entete.jsp"/>
Під час виконання цей тег призведе до того, що в код сторінки JSP, яка його містить, буде включено код сторінки [entete.jsp]. Оскільки URL-адреса сторінки є відносною (без символу /), сторінка [entete.jsp] буде шукатися в тій самій папці, що й сторінка, яка містить тег <jsp:include>.
Код:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<html>
<head>
<title>webarticles</title>
</head>
<body>
<table>
<tr>
<td><h2>Magasin virtuel</h2></td>
<c:forEach items="${actions}" var="action">
<td>|</td>
<td><a href="<c:out value="${action.href}"/>"><c:out value="${action.lien}"/></a></td>
</c:forEach>
</tr>
</table>
<hr>
|
3.5.9.2. liste.jsp
Цей вигляд відображає список товарів, доступних для продажу:
Він відображається після запиту /main?action=liste або /main?action=validationpanier. Елементи запиту контролера такі:
| об’єкт Hashtable[] — масив опцій меню |
| ArrayList об’єктів типу [Article] |
| об'єкт String — повідомлення, яке потрібно відобразити внизу сторінки |
Кожне посилання [Infos] з масиву HTML статей має URL-адресу у вигляді [?action=infos&id=ID], де ID — це поле id відображуваної статті.
Код:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Liste des articles</h2>
<table border="1">
<tr>
<th>NOM</th><th>PRIX</th>
</tr>
<c:forEach var="article" items="${listarticles}">
<tr>
<td><c:out value="${article.nom}"/></td>
<td><c:out value="${article.prix}"/></td>
<td><a href="<c:out value="?action=infos&id=${article.id}"/>">Infos</a></td>
</tr>
</c:forEach>
</table>
<p>
<c:out value="${message}"/>
</body>
</html>
|
3.5.9.3. infos.jsp
Цей вигляд відображає інформацію про товар, а також дозволяє його придбати:

Вона відображається після запиту /main?action=infos&id=ID або запиту /main?action=achat&id=ID, коли кількість, що купується, вказана неправильно. Елементи запиту контролера такі:
| об’єкт Hashtable[] — масив опцій меню |
| об’єкт типу [Article] — товар, який потрібно відобразити |
| об'єкт String — повідомлення, яке слід відобразити у разі помилки щодо кількості |
| об'єкт String — значення, яке потрібно відобразити в полі введення [Qte] |
Поля [msg] та [qte] використовуються у разі помилки введення кількості:

Ця сторінка містить форму, яка відправляється за допомогою кнопки [Acheter]. Цільова URL-адреса POST — [?action=achat&id=ID], де ID — це ідентифікатор придбаного товару.
Код:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Article d'id [<c:out value="${article.id}"/>]</h2>
<table border="1">
<tr>
<th>NOM</th><th>PRIX</th><th>STOCK ACTUEL</th><th>STOCK MINIMUM</th>
</tr>
<tr>
<td><c:out value="${article.nom}"/></td>
<td><c:out value="${article.prix}"/></td>
<td><c:out value="${article.stockActuel}"/></td>
<td><c:out value="${article.stockMinimum}"/></td>
</tr>
</table>
<p>
<form method="post" action="?action=achat&id=<c:out value="${article.id}"/>"/>
<table>
<tr>
<td><input type="submit" value="Acheter"></td>
<td>Qte <input type="text" name="qte" size="3" value="<c:out value="${qte}"/>"></td>
<td><c:out value="${msg}"/></td>
</tr>
</table>
</form>
</body>
</html>
|
3.5.9.4. panier.jsp
Цей вигляд відображає вміст кошика:

Він відображається після запиту /main?action=panier або /main?action=retirerachat&id=ID. Елементи запиту контролера такі:
| об’єкт Hashtable[] — масив опцій меню |
| об’єкт типу [Panier] — кошик, який потрібно відобразити |
Кожне посилання [Retirer] у масиві HTML покупок у кошику має URL-адресу у вигляді [?action=retirerachat&id=ID], де ID — це поле [id]товару, який потрібно видалити з кошика.
Код:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Contenu de votre panier</h2>
<table border="1">
<tr>
<td>Article</td><td>Qte</td><td>Pu</td><td>Total</td>
</tr>
<c:forEach var="achat" items="${panier.achats}">
<tr>
<td><c:out value="${achat.article.nom}"/></td>
<td><c:out value="${achat.qte}"/></td>
<td><c:out value="${achat.article.prix}"/></td>
<td><c:out value="${achat.total}"/></td>
<td><a href="<c:out value="?action=retirerachat&id=${achat.article.id}"/>">Retirer</a></td>
</tr>
</c:forEach>
</table>
<p>
Total de la commande : <c:out value="${panier.total}"/> euros
</body>
</html>
|
3.5.9.5. paniervide.jsp
Цей вигляд відображає інформацію про те, що кошик порожній:

Вона відображається після запиту /main?action=panier або /main?action=retirerachat&id=ID. Елементи запиту контролера такі:
| об’єкт Hashtable[] — масив опцій меню |
Код:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Contenu de votre panier</h2>
<p>
Votre panier est vide.
</body>
</html>
|
3.5.9.6. erreurs.jsp
Цей екран відображається у разі помилок:

Вона відображається після будь-якого запиту, що призвів до помилки, за винятком дії купівлі з неправильною кількістю, яка обробляється у вікні [INFOS]. Елементи запиту контролера такі:
| об’єкт Hashtable[] — масив опцій меню |
| ArrayList об’єктів String, що представляють повідомлення про помилки, які потрібно відобразити |
Код:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Les erreurs suivantes se sont produites</h2>
<ul>
<c:forEach var="erreur" items="${erreurs}">
<li><c:out value="${erreur}"/></li>
</c:forEach>
</ul>
</body>
</html>
|
3.5.9.7. index.jsp
Ця сторінка визначена як головна сторінка додатка у файлі [web.xml] додатка:
| <?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
....
</servlet>
<servlet-mapping>
....
</servlet-mapping>
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
</web-app>
|
Вигляд [index.jsp] просто перенаправляє клієнта до точки входу в додаток:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main?action=liste"/>
|
3.5.10. Контролер
Залишилося написати основну частину нашого веб-додатку — контролер. Його завдання полягає в тому, щоб:
- отримати запит від клієнта,
- обробити запитувану клієнтом дію за допомогою бізнес-класів,
- відправити у відповідь відповідний вигляд.
3.5.10.1. Ініціалізація контролера
Коли клас контролера завантажується сервером сервлетів, виконується його метод [init]. Це відбудеться лише один раз. Після завантаження в пам’ять контролер залишатиметься там і оброблятиме запити різних клієнтів. Для кожного клієнта створюється окремий потік виконання, і таким чином методи контролера виконуються одночасно різними потоками. Нагадаємо, що саме з цієї причини контролер не повинен мати полів, які його методи могли б змінювати. Його поля мають бути доступними лише для читання. Вони ініціалізуються методом [init], що є його основною функцією. Цей метод має ту особливість, що виконується лише один раз одним потоком. Отже, у цьому методі не виникає проблем з паралельним доступом до полів контролера. Метод [init] призначений для ініціалізації об’єктів, необхідних для веб-додатка, які будуть спільними для всіх клієнтських потоків у режимі «тільки для читання». Ці спільні об’єкти можна розмістити у двох місцях:
- у приватних полях контролера
- контекст виконання додатка (ServletContext)
Метод [init] додатка [webarticles] виконає такі дії:
- перевірить наявність у файлі [web.xml] параметрів, необхідних для коректної роботи додатка. Вони були описані в розділі 3.5.5.
- заповнить приватне поле [ArrayList erreurs] списком можливих помилок. Цей список буде порожнім, якщо помилок немає, але він все одно існуватиме.
- якщо помилки були, метод [init] на цьому завершується. В іншому випадку він створює об’єкт типу [IArticlesDomain], який буде бізнес-об’єктом, що контролер використовуватиме для своїх потреб. Як пояснено в розділі 3.5.6, контролер запитує у фреймворку Spring необхідний йому бін. Ця операція інстанціювання може призвести до різних помилок. У такому випадку вони, знову ж таки, зберігаються у полі [erreurs] контролера.
3.5.10.2. Методи doGet, doPost
Ці два методи обробляють запити HTTP, GET та POST від клієнтів. Ці запити оброблятимуться однаково. Таким чином, метод [doPost] може перенаправляти до методу [doGet] або навпаки. Запит клієнта оброблятиметься наступним чином:
- буде перевірено поле [erreurs]. Якщо воно не порожнє, це означає, що під час ініціалізації додатка сталися помилки і він не може працювати. У такому разі у відповідь буде надіслано представлення [ERREURS].
- параметр [action] запиту буде отримано та перевірено. Якщо він не відповідає жодній відомій дії, надсилається представлення [ERREURS] із відповідним повідомленням про помилку.
- Якщо параметр [action] є дійсним, запит клієнта передається для обробки до процедури, специфічної для цієї дії. Процедура, що обробляє дію [uneAction], матиме такий підпис:
| /**
* @param request la requête du client
* @param response la réponse au client
* @throws IOException
* @throws ServletException
*/
private void doUneAction(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
|
3.5.10.3. Обробка різних дій
Методи, що обробляють різні можливі дії додатка, є такими:
метод | запит | обробка | можливі відповіді |
| GET /main?action=liste | - запитати список статей у бізнес-класі - відобразити його | [LISTE] або [ERREURS] |
| GET /main?action=infos&id=ID | - отримати статтю з id=ID з бізнес-класу - відобразити його | [INFOS] або [ERREURS] |
| POST /main?action=achat&id=ID - кількість, що була придбана, входить до параметрів, що передаються | - запитати товар з id=ID у бізнес-класу - додати його до кошика в сесії клієнта | [LISTE] або [INFOS] або [ERREURS] |
| GET /main?action=retirerachat&id=ID | - видалити товар з id=ID зі списку покупок у кошику сесії клієнта | [PANIER] |
| GET /main?action=panier | - відобразити кошик сесії клієнта | [PANIER] або [PANIERVIDE] |
| GET /main?action=validationpanier | - зменшити в базі даних залишки всіх товарів , що знаходяться у кошику сесії клієнта | [LISTE] або [ERREURS] |
3.5.10.4. Код
| package istia.st.articles.web;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import istia.st.articles.domain.Achat;
import istia.st.articles.dao.Article;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.domain.Panier;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
/**
* @author ST
*
*/
public class WebArticles extends HttpServlet {
// приватні поля
private ArrayList erreurs = new ArrayList();
private IArticlesDomain articlesDomain = null;
private final String URL_MAIN = "urlMain";
private final String URL_ERREURS = "urlErreurs";
private final String URL_LISTE = "urlListe";
private final String URL_INFOS = "urlInfos";
private final String URL_PANIER = "urlPanier";
private final String URL_PANIER_VIDE = "urlPanierVide";
private final String URL_DEBUG = "urlDebug";
private final String SPRING_CONFIG_FILENAME = "springConfigFileName";
private final String[] parameters =
{
URL_MAIN,
URL_ERREURS,
URL_LISTE,
URL_INFOS,
URL_PANIER,
URL_PANIER_VIDE,
URL_DEBUG,
SPRING_CONFIG_FILENAME };
private ServletConfig config;
private final String ACTION_LISTE = "liste";
private final String ACTION_PANIER = "panier";
private final String ACTION_ACHAT = "achat";
private final String ACTION_INFOS = "infos";
private final String ACTION_RETIRER_ACHAT = "retirerachat";
private final String ACTION_VALIDATION_PANIER = "validationpanier";
private String urlActionListe;
private final String lienActionListe = "Liste des articles";
private String urlActionPanier;
private final String lienActionPanier = "Voir le panier";
private String urlActionValidationPanier;
private final String lienActionValidationPanier = "Valider le panier";
private Hashtable hActionListe = new Hashtable(2);
private Hashtable hActionPanier = new Hashtable(2);
private Hashtable hActionValidationPanier = new Hashtable(2);
public void init() {
// отримуємо параметри ініціалізації сервлету
config = getServletConfig();
String param = null;
for (int i = 0; i < parameters.length; i++) {
param = config.getInitParameter(parameters[i]);
if (param == null) {
// зберігаємо інформацію про помилку
erreurs.add(
"Paramètre ["
+ parameters[i]
+ "] absent dans le fichier [web.xml]");
}
}
// чи є помилки?
if (erreurs.size() != 0) {
return;
}
// створюється об’єкт IArticlesDomain для доступу до бізнес-шару
try {
articlesDomain =
(IArticlesDomain)
(
new XmlBeanFactory(
new ClassPathResource(
(String) config.getInitParameter(
SPRING_CONFIG_FILENAME)))).getBean(
"articlesDomain");
} catch (Exception ex) {
// зберігаємо інформацію про помилку
erreurs.add(
"Erreur de configuration de l'accès aux données : "
+ ex.toString());
return;
}
// зберігаються певні URL-адреси додатка
hActionListe.put("href", "?action=" + ACTION_LISTE);
hActionListe.put("lien", lienActionListe);
hActionPanier.put("href", "?action=" + ACTION_PANIER);
hActionPanier.put("lien", lienActionPanier);
hActionValidationPanier.put(
"href",
"?action=" + ACTION_VALIDATION_PANIER);
hActionValidationPanier.put("lien", lienActionValidationPanier);
// процес завершено
return;
}
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// перевіряється, як пройшла ініціалізація сервлету
if (erreurs.size() != 0) {
// чи є URL-адреса сторінки помилок?
if (config.getInitParameter(URL_ERREURS) == null) {
throw new ServletException(erreurs.toString());
}
// відображаємо сторінку помилок
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] {
});
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// кінець
return;
}
// обробляємо дію
String action = request.getParameter("action");
if (action == null) {
// список статей
doListe(request, response);
return;
}
if (action.equals(ACTION_LISTE)) {
// список статей
doListe(request, response);
return;
}
if (action.equals(ACTION_INFOS)) {
// інформація про товар
doInfos(request, response);
return;
}
if (action.equals(ACTION_ACHAT)) {
// придбання товару
doAchat(request, response);
return;
}
if (action.equals(ACTION_PANIER)) {
// перегляд кошика
doPanier(request, response);
return;
}
if (action.equals(ACTION_RETIRER_ACHAT)) {
// видалення товару з кошика
doRetirerAchat(request, response);
return;
}
if (action.equals(ACTION_VALIDATION_PANIER)) {
// підтвердження кошика
doValidationPanier(request, response);
return;
}
// невідома дія
ArrayList erreurs = new ArrayList();
erreurs.add("action [" + action + "] inconnue");
// відображення сторінки помилок
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
// кінець
return;
}
private void doValidationPanier(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// покупець підтвердив свій кошик
Panier panier = (Panier) request.getSession().getAttribute("panier");
// підтверджується цей кошик
try {
articlesDomain.acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// ненормальний стан
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// отримуються помилки
ArrayList erreurs = articlesDomain.getErreurs();
if (erreurs.size() != 0) {
request.setAttribute(
"actions",
new Hashtable[] { hActionListe, hActionPanier });
afficheErreurs(request, response, erreurs);
return;
}
// відображається список товарів
request.setAttribute("message", "Votre panier a été validé");
doListe(request, response);
// кінець
return;
}
private void doRetirerAchat(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// вилучення товару з кошика
try {
Panier panier =
(Panier) request.getSession().getAttribute("panier");
String strIdAchat = request.getParameter("id");
panier.enlever(Integer.parseInt(strIdAchat));
} catch (NumberFormatException ignored) {
} catch (NullPointerException ignored) {
}
// відображення кошика
doPanier(request, response);
}
private void doPanier(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// відображення кошика
Panier panier = (Panier) request.getSession().getAttribute("panier");
// кошик порожній?
if (panier == null || panier.getAchats().size() == 0) {
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_PANIER_VIDE))
.forward(request, response);
// кінець
return;
}
// у кошику є товари
request.setAttribute("panier", panier);
request.setAttribute(
"actions",
new Hashtable[] { hActionListe, hActionValidationPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_PANIER))
.forward(request, response);
// кінець
return;
}
private void doAchat(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// придбання товару
// отримано кількість
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// неправильна кількість
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
String url =
config.getInitParameter(URL_MAIN)
+ "?action=infos&id="
+ request.getParameter("id");
getServletContext().getRequestDispatcher(url).forward(
request,
response);
// кінець
return;
}
// отримання сеансу клієнта
HttpSession session = request.getSession();
// створення покупки
Article article = (Article) session.getAttribute("article");
Achat achat = new Achat(article, qté);
// додаємо покупку до кошика клієнта
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
// повернення до списку товарів
String url = config.getInitParameter(URL_MAIN) + "?action=liste";
getServletContext().getRequestDispatcher(url).forward(
request,
response);
// кінець
return;
}
private void afficheDebugInfos(
HttpServletRequest request,
HttpServletResponse response,
ArrayList infos)
throws ServletException, IOException {
// відображається список товарів
request.setAttribute("infos", infos);
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_DEBUG))
.forward(request, response);
// кінець
return;
}
public void doPost(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// те саме, що get
doGet(request, response);
}
private void doInfos(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// список помилок
ArrayList erreurs = new ArrayList();
// отримується запитуваний ідентифікатор
String strId = request.getParameter("id");
// щось?
if (strId == null) {
// ненормально
erreurs.add("action incorrecte([infos,id=null]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// перетворюємо strId у ціле число
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// ненормально
erreurs.add("action incorrecte([infos,id=" + strId + "]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// запитується ідентифікатор ключа
Article article = null;
try {
article=articlesDomain.getArticleById(id);
} catch (UncheckedAccessArticlesException ex) {
// ненормально
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
if (article == null) {
// ненормально
erreurs.add("Article de clé [" + id + "] inexistant");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// запис додано до сесії
request.getSession().setAttribute("article", article);
// відображається інформаційна сторінка
request.setAttribute("actions", new Hashtable[] { hActionListe });
/ / request.setAttribute("urlMain",config.getInitParameter(URL_MAIN));
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_INFOS))
.forward(request, response);
// кінець
return;
}
private void afficheErreurs(
HttpServletRequest request,
HttpServletResponse response,
ArrayList erreurs)
throws ServletException, IOException {
// відображається сторінка помилок
request.setAttribute("erreurs", erreurs);
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// кінець
return;
}
private void doListe(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// список помилок
ArrayList erreurs = new ArrayList();
// запит списку товарів
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// записування помилки
erreurs.add(
"Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
// є помилки?
if (erreurs.size() != 0) {
// відображається сторінка помилок
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// кінець
return;
}
// відображається список товарів
request.setAttribute("listarticles", articles);
request.setAttribute("message","");
request.setAttribute("actions", new Hashtable[] { hActionPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_LISTE))
.forward(request, response);
// кінець
return;
}
/**
* suivi console pour débogage
* @param message : le message à afficher
*/
private void affiche(String message) {
System.out.println(message);
}
}
|
Ми даємо читачеві час, щоб він міг прочитати та зрозуміти цей код. Сподіваємося, що коментарі допоможуть йому в цьому.
3.5.10.5. Тестування додатка
Покажемо кілька знімків екрана з тестів. По-перше, головна сторінка додатка:

Запитана URL-адреса насправді була [http://localhost:8080/webarticles]. Читач побачить, що у файлі [web.xml] ми визначаємо головну сторінку додатка:
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
Вигляд [index.jsp] визначено наступним чином:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main?action=liste"/>
Отже, відбулося перенаправлення на URL-адресу [http://localhost:8080/webarticles/main?action=liste], про що свідчить URL-адреса браузера на знімку екрана. Отже, було зроблено запит на URL-адресу [/main?action=liste]. У [web.xml] URL-адреса /main пов’язана з сервлетом [webarticles]:
<servlet-mapping>
<servlet-name>webarticles</servlet-name>
<url-pattern>/main</url-pattern>
</servlet-mapping>
Також у [web.xml] сервлет [webarticles] пов’язаний із сервлетом [ istia.st.articles.web.WebArticles]:
<servlet-name>webarticles</servlet-name>
<servlet-class>istia.st.articles.web.WebArticles</servlet-class>
Отже, сервлет [ istia.st.articles.web.WebArticles] завантажується контейнером сервлетів Tomcat, якщо він ще не був завантажений, і виконується його метод [init]:
| public void init() {
// отримуються параметри ініціалізації сервлету
config = getServletConfig();
String param = null;
for (int i = 0; i < parameters.length; i++) {
param = config.getInitParameter(parameters[i]);
if (param == null) {
// зберігається інформація про помилку
erreurs.add(
"Paramètre ["
+ parameters[i]
+ "] absent dans le fichier [web.xml]");
}
}
// чи є помилки?
if (erreurs.size() != 0) {
return;
}
// створюється об’єкт IArticlesDomain для доступу до бізнес-шару
try {
articlesDomain =
(IArticlesDomain)
(
new XmlBeanFactory(
new ClassPathResource(
(String) config.getInitParameter(
SPRING_CONFIG_FILENAME)))).getBean(
"articlesDomain");
} catch (Exception ex) {
// зберігаємо помилку
erreurs.add(
"Erreur de configuration de l'accès aux données : "
+ ex.toString());
return;
}
// зберігаються певні URL-адреси додатка
hActionListe.put("href", "?action=" + ACTION_LISTE);
hActionListe.put("lien", lienActionListe);
hActionPanier.put("href", "?action=" + ACTION_PANIER);
hActionPanier.put("lien", lienActionPanier);
hActionValidationPanier.put(
"href",
"?action=" + ACTION_VALIDATION_PANIER);
hActionValidationPanier.put("lien", lienActionValidationPanier);
// процес завершено
return;
}
|
Коментарі: метод [init]
- перевіряє наявність певних параметрів конфігурації
- створює екземпляр служби доступу до домену додатка за допомогою Spring
- заповнює список помилок, щоб вказати на можливі помилки ініціалізації
- деякі приватні поля:
- помилки: список помилок, виявлених [init]
- [hActionListe, hActionPanier, hActionValidationPanier]: словники. Кожен із них містить інформацію, необхідну для відображення опції головного меню, яке відображається у вигляді [entete.jsp]
- acticlesDomain: служба доступу до моделі додатка
Метод [init] виконується лише один раз — під час початкового завантаження сервлету. Далі виконується один із методів [doGet, doPost] залежно від типу [GET, POST] запиту клієнта. У даному випадку обидва методи виконують одну й ту саму дію, і код було розміщено в [doGet]:
| public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// перевіряється, як пройшла ініціалізація сервлету
if (erreurs.size() != 0) {
// чи є URL-адреса сторінки помилок?
if (config.getInitParameter(URL_ERREURS) == null) {
throw new ServletException(erreurs.toString());
}
// відображаємо сторінку помилок
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] {
});
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// кінець
return;
}
// обробляємо дію
String action = request.getParameter("action");
if (action == null) {
// список статей
doListe(request, response);
return;
}
if (action.equals(ACTION_LISTE)) {
// список статей
doListe(request, response);
return;
}
if (action.equals(ACTION_INFOS)) {
// інформація про товар
doInfos(request, response);
return;
}
if (action.equals(ACTION_ACHAT)) {
// придбання товару
doAchat(request, response);
return;
}
if (action.equals(ACTION_PANIER)) {
// перегляд кошика
doPanier(request, response);
return;
}
if (action.equals(ACTION_RETIRER_ACHAT)) {
// видалення товару з кошика
doRetirerAchat(request, response);
return;
}
if (action.equals(ACTION_VALIDATION_PANIER)) {
// підтвердження кошика
doValidationPanier(request, response);
return;
}
// невідома дія
ArrayList erreurs = new ArrayList();
erreurs.add("action [" + action + "] inconnue");
// відображається сторінка помилок
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
// кінець
return;
}
|
- метод [doGet] спочатку перевіряє, чи не сталося помилок ініціалізації після виконання методу [init]. Якщо так, він відображає представлення [ERREURS], і на цьому все.
- В іншому випадку він отримує параметр [action] із запиту клієнта. Нагадаємо, що додаток було створено для обробки запитів, у яких обов’язково має бути присутній параметр [action].
- Вона запускає метод, пов’язаний із цією дією. У даному випадку це буде метод [doListe].
Метод [doListe] має такий вигляд:
| private void doListe(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// список помилок
ArrayList erreurs = new ArrayList();
// запит списку статей
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// записування помилки
erreurs.add(
"Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
// є помилки?
if (erreurs.size() != 0) {
// відображається сторінка помилок
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// кінець
return;
}
// відображається список товарів
request.setAttribute("listarticles", articles);
request.setAttribute("message","");
request.setAttribute("actions", new Hashtable[] { hActionPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_LISTE))
.forward(request, response);
// кінець
return;
}
|
- Нагадаємо, що метод [init] зберіг службу доступу до моделі додатка (доменний шар) у приватному полі сервлета:
// приватні поля
private IArticlesDomain articlesDomain = null;
- За допомогою цієї служби доступу можна отримати список статей:
| // список помилок
ArrayList erreurs = new ArrayList();
// запит списку товарів
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// запис помилки
erreurs.add(
"Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
|
- у разі виникнення помилок надсилається представлення [ERREURS]:
| // є помилки?
if (erreurs.size() != 0) {
// відображається сторінка помилок
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// кінець
return;
}
|
- в іншому випадку надсилається погляд [LISTE]:
| // відображається список товарів
request.setAttribute("listarticles", articles);
request.setAttribute("message","");
request.setAttribute("actions", new Hashtable[] { hActionPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_LISTE))
.forward(request, response);
|
У нашому прикладі все пройшло успішно, і ми отримали вигляд [LISTE]. Пропоную читачеві ще раз переглянути код подання [LISTE], щоб переконатися, що динамічні параметри, які очікує це подання, дійсно надані вище контролером. Таку саму перевірку слід проводити для кожного подання:
- знайти динамічні параметри подання
- переконатися, що контролер дійсно вказує їх в атрибутах запиту, що передається до подання
Тепер ми просто наведемо послідовність екранів, з якими стикається користувач додатка. Читачеві пропонується щоразу повторювати міркування, аналогічні попереднім:
Зі списку статей користувач може вибрати статтю:
Тут покупець може придбати товар № 3. Давайте зробимо помилку при введенні кількості:
Помилка була виявлена. Тепер придбаємо кілька товарів:
Покупка зареєстрована, список товарів оновлено. Перевіримо кошик:
Товар дійсно знаходиться у кошику. Вилучимо його:
Товар було вилучено з кошика, і кошик знову відобразився. Тут він порожній.
Купимо 100 одиниць товару № 3 та 2 одиниці товару № 4:
Придбати товар № 3 не вдалося, оскільки ми хотіли купити 100 штук, а на складі було лише 30. Цей товар залишився у кошику:
Товар № 4 було придбано, про що свідчить його новий залишок у кількості 39 (40-1):
3.6. Архітектура MVC із використанням Struts
3.6.1. Загальна архітектура додатка
Повернемося до архітектури MVC додатка:
У попередній версії:
- функцію контролера виконував сервлет
- відображення забезпечувалося сторінками JSP
- модель забезпечувалася набором із трьох архівів .jar
У версії Struts:
- контролер буде реалізовано за допомогою сервлета, похідного від загального сервлета [ActionServlet] з Struts
- представлення забезпечуватимуться тими самими сторінками JSP, що й раніше, за винятком деяких дрібних деталей
- модель буде реалізована тими самими трьома архівами
Ми побачимо, що перехід попереднього додатка на Struts полягає в таких завданнях:
- дії, які раніше оброблялися в окремих методах сервлета/контролера, тепер обробляються екземплярами класів, похідних від класу [Action] з Struts
- створити конфігураційні файли [web.xml] та [struts-config.xml]
- внести деякі зміни до сторінок JSP
Нагадаємо загальну архітектуру MVC, яку використовує STRUTS:
| бізнес-класи, класи доступу до даних та база даних |
| сторінки JSP |
| сервлет для обробки запитів клієнтів, об’єкти [Action] та біни [ActionForm], пов’язані з формами. |
- контролер є серцем додатка. Усі запити клієнта проходять через нього. Це загальний сервлет, що надається STRUTS. У деяких випадках може знадобитися його похідна реалізація. У простих випадках це не потрібно. Цей загальний сервлет отримує необхідну інформацію з файлу, який найчастіше називається struts-config.xml.
- Якщо запит клієнта містить параметри форми, контролер поміщає їх в об’єкт Bean. Об’єкти Bean, створені таким чином з часом, зберігаються в сесії або в запиті клієнта. Цей параметр можна налаштувати. Їх не потрібно створювати заново, якщо вони вже існують.
- У конфігураційному файлі struts-config.xml до кожного URL, який має оброблятися програмою (тобто не відповідає представленню JSP, яке можна було б запросити безпосередньо), прив’язується певна інформація:
- ім'я класу типу Action, відповідального за обробку запиту. І в цьому випадку інстанційований об'єкт Action може зберігатися в сесії або в запиті.
- якщо запитуваний URL має параметри (наприклад, у разі надсилання форми до контролера), вказується ім’я біна, відповідального за збереження інформації з форми.
- Маючи цю інформацію, надану його конфігураційним файлом, при отриманні запиту URL від клієнта контролер здатний визначити, чи потрібно створювати bean і який саме. Після створення екземпляра bean може перевірити, чи є дані, які він зберіг і які походять із форми, дійсними чи ні. Контролер автоматично викликає метод біна під назвою validate. Бін створюється розробником. Отже, розробник розміщує в методі validate код, що перевіряє правильність даних форми. Якщо дані виявляються недійсними, контролер не продовжує обробку. Він передає управління представленню, ім’я якого знайде у своєму файлі конфігурації. На цьому обмін завершується. Слід зазначити, що розробник може вимагати, щоб валідність форми не перевірялася. Він робить це також у файлі struts-config.xml. У цьому випадку контролер не викликає метод validate біна.
- Якщо дані біна правильні, або якщо перевірка не проводиться, або якщо біна немає, контролер передає управління об’єкту типу Action, пов’язаному з URL. Він робить це, викликаючи метод `execute` цього об’єкта, якому передає посилання на бін, який він, можливо, створив. Саме тут розробник виконує необхідні дії: йому, можливо, доведеться викликати бізнес-класи або класи доступу до даних. По завершенні обробки об’єкт `Action` повертає контролеру назву подання, яке той повинен надіслати у відповідь клієнту.
- У своєму конфігураційному файлі контролер знайде URL, пов’язаний з назвою подання, яке йому було доручено відобразити. Після цього він надсилає це подання. Обмін даними з клієнтом завершено.
У нашому додатку ми не будемо використовувати об’єкти [Bean] як проміжні об’єкти між клієнтом та класами [Action]. Об’єкт [Action] безпосередньо отримуватиме параметри запиту клієнта з об’єкта [HttpServletRequest], який він отримає. Це полегшує перенесення нашого початкового додатка. Отже, остаточна архітектура нашого додатка буде такою:
| бізнес-класи, класи доступу до даних та база даних |
| сторінки JSP |
| сервлет для обробки запитів клієнтів, об’єкти [Action] |
3.6.2. Модель
Вона була представлена раніше. Складається з Java-архівів [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].
3.6.3. Конфігурація додатка
3.6.3.1. Загальна архітектура
Загальна архітектура проекту Eclipse така:

3.6.3.2. Налаштування доступу до даних
Оскільки інтерфейс доступу до даних не змінюється, відповідні файли конфігурації залишаються такими самими, як і в попередній версії. Вони визначені у файлі [WEB-INF/src]:

На знімку екрана вище файли [articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties] належать до попередньої версії.
3.6.3.3. Каталог архівів
У [WEB-INF/lib] містяться ті самі архіви, що й у попередній версії, а також архів, необхідний для Struts:

3.6.3.4. Налаштування додатка
Налаштування додатка здійснюється за допомогою двох файлів: [web.xml, struts-config.xml] у папці [WEB-INF]:

Файл [web.xml] має такий вигляд:
| <?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
<servlet-name>strutswebarticles</servlet-name>
<servlet-class>istia.st.articles.web.struts.MainServlet</servlet-class>
<init-param>
<param-name>config</param-name>
<param-value>/WEB-INF/struts-config.xml</param-value>
</init-param>
<init-param>
<param-name>springConfigFileName</param-name>
<param-value>spring-config-sqlmap-firebird.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>strutswebarticles</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
</web-app>
|
Що міститься в цьому файлі?
- Головна сторінка додатка — [vues/index.jsp] (welcome-file)
- запити на URL-адреси у форматі *.do будуть перенаправлені на сервлет [strutswebarticles] (servlet-mapping)
- сервлет [strutswebarticles] є екземпляром класу [ istia.st.articles.web.struts.MainServlet] (servlet-name, servlet-class)
- Цей сервлет приймає два параметри ініціалізації
- ім’я файлу конфігурації Struts (config)
- ім'я файлу конфігурації Spring (springConfigFileName)
Файл [struts-config.xml] має такий вигляд:
| <?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE struts-config PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 1.1//EN"
"http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd">
<struts-config>
<action-mappings>
<action path="/main" type="istia.st.articles.web.struts.ListeArticlesAction">
<forward name="afficherListeArticles" path="/vues/liste.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
<action path="/liste" type="istia.st.articles.web.struts.ListeArticlesAction">
<forward name="afficherListeArticles" path="/vues/liste.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
<action path="/infos" type="istia.st.articles.web.struts.InfosArticleAction">
<forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
<action
path="/achat" type="istia.st.articles.web.struts.AchatArticleAction">
<forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
<forward name="afficherListeArticles" path="/main.do"/>
</action>
<action
path="/panier" type="istia.st.articles.web.struts.VoirPanierAction">
<forward name="afficherPanier" path="/vues/panier.jsp"/>
<forward name="afficherPanierVide" path="/vues/paniervide.jsp"/>
</action>
<action
path="/retirerachat" type="istia.st.articles.web.struts.RetirerAchatAction">
<forward name="afficherPanier" path="/panier.do"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
<action
path="/validerpanier" type="istia.st.articles.web.struts.ValiderPanierAction">
<forward name="afficherListeArticles" path="/main.do"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
</action-mappings>
<message-resources parameter="ApplicationResources" null="false" />
</struts-config>
|
Що вказано в цьому конфігураційному файлі?
- що наш контролер оброблятиме такі URL-адреси:
| для відображення списку статей |
| для відображення списку статей |
| для перегляду інформації про конкретний товар |
| щоб придбати певний товар |
| для перегляду кошика покупок |
| щоб видалити товар з кошика |
| для підтвердження кошика покупок |
- зазначені вище дії один в один відповідають діям, що оброблялися сервлетом у попередній версії. Для кожної з них вказано таку інформацію:
- назва класу, відповідального за обробку цієї дії
- можливі відповіді (= перегляди) після обробки дії. Контролер обирає лише одну з них.
- ім’я файлу повідомлень для додатка (message-resources). У цьому випадку файл існуватиме, але буде порожнім. Він не використовуватиметься. Його слід розмістити у [ClassPath] додатка. Тут він буде розміщений у [WEB-INF/classes]. У середовищі Eclipse цей результат можна отримати, розмістивши його у [WEB-INF/src]:

3.6.4. Види JSP
Використовувані подання JSP також є тими самими, що й у попередній версії. Змінено дуже мало: це URL-адреси у форматі [?action=XX?id=YY& ...], які стають [/XX.do?id=YY&....]. Ми повторюємо тут вже надані пояснення, щоб користувачеві не довелося повертатися назад. Важливо розуміти, що інформація, яка передається контролером до подання, є абсолютно однаковою в обох версіях. У цьому плані нічого не змінилося.
3.6.4.1. entete.jsp
Щоб забезпечити певну уніфікованість різних вікон, вони матимуть спільний заголовок, у якому відображається назва додатка разом із меню:
Меню є динамічним і визначається контролером. Контролер додає до запиту, що надсилається на сторінку JSP, атрибут-ключ «actions», значенням якого є масив типу Hastable[]. Кожен елемент цього масиву є словником, призначеним для генерації пункту меню у заголовку. Кожен словник має два ключі:
- href: URL-адреса, пов’язана з пунктом меню
- lien: текст меню
Інші подання додатка використовуватимуть заголовок, визначений [entete.jsp], за допомогою такого тегу JSP:
<jsp:include page="entete.jsp"/>
Під час виконання ця тег призведе до включення в код сторінки JSP, яка її містить, коду сторінки [entete.jsp]. Оскільки URL-адреса сторінки є відносною (без символу /), сторінка [entete.jsp] буде шукатися в тій самій папці, що й сторінка з тегом <jsp:include>.
Код:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<html>
<head>
<title>webarticles</title>
</head>
<body>
<table>
<tr>
<td><h2>Magasin virtuel</h2></td>
<c:forEach items="${actions}" var="action">
<td>|</td>
<td><a href="<c:out value="${action.href}"/>"><c:out value="${action.lien}"/></a></td>
</c:forEach>
</tr>
</table>
<hr>
|
Коментарі: змін порівняно з попередньою версією немає
3.6.4.2. liste.jsp
Цей вигляд відображає список товарів, доступних для продажу:
Він відображається після запиту /main.do або /validerpanier.do. Елементи запиту контролера такі:
| об’єкт Hashtable[] — масив опцій меню |
| ArrayList об’єктів типу [Article] |
| об'єкт String — повідомлення, яке відображається внизу сторінки |
Кожне посилання [Infos] у масиві HTML статей має URL-адресу у вигляді [/infos.do?id=ID], де ID — це поле id відображуваної статті.
Код:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Liste des articles</h2>
<table border="1">
<tr>
<th>NOM</th><th>PRIX</th>
</tr>
<c:forEach var="article" items="${listarticles}">
<tr>
<td><c:out value="${article.nom}"/></td>
<td><c:out value="${article.prix}"/></td>
<td><a href="<c:out value="infos.do?id=${article.id}"/>">Infos</a></td>
</tr>
</c:forEach>
</table>
<p>
<c:out value="${message}"/>
</body>
</html>
|
Коментарі: одна зміна (виділена вище)
3.6.4.3. infos.jsp
Цей екран відображає інформацію про товар, а також дозволяє його придбати:

Вона відображається після запиту /infos.do?id=ID або запиту /achat.do?id=ID, коли кількість, що купується, є неправильною. Елементи запиту контролера такі:
| об’єкт Hashtable[] — масив опцій меню |
| об’єкт типу [Article] — товар, який потрібно відобразити |
| об’єкт String — повідомлення, яке слід відобразити у разі помилки щодо кількості |
| об'єкт String — значення, яке потрібно відобразити в полі введення [Qte] |
Поля [msg] та [qte] використовуються у разі помилки при введенні кількості:

Ця сторінка містить форму, яка відправляється за допомогою кнопки [Acheter]. Цільова URL-адреса POST — [/achat.do?id=ID], де ID — це ідентифікатор придбаного товару.
Код:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Article d'id [<c:out value="${article.id}"/>]</h2>
<table border="1">
<tr>
<th>NOM</th><th>PRIX</th><th>STOCK ACTUEL</th><th>STOCK MINIMUM</th>
</tr>
<tr>
<td><c:out value="${article.nom}"/></td>
<td><c:out value="${article.prix}"/></td>
<td><c:out value="${article.stockActuel}"/></td>
<td><c:out value="${article.stockMinimum}"/></td>
</tr>
</table>
<p>
<form method="post" action="achat.do?id=<c:out value="${article.id}"/>"/>
<table>
<tr>
<td><input type="submit" value="Acheter"></td>
<td>Qte <input type="text" name="qte" size="3" value="<c:out value="${qte}"/>"></td>
<td><c:out value="${msg}"/></td>
</tr>
</table>
</form>
</body>
</html>
|
Коментарі: одна зміна (виділена вище)
3.6.4.4. panier.jsp
Цей вигляд відображає вміст кошика:

Він відображається після запиту /panier.do або /retirerachat.do?id=ID. Елементи запиту контролера такі:
| об’єкт Hashtable[] — масив опцій меню |
| об’єкт типу [Panier] — кошик, який потрібно відобразити |
Кожне посилання [Retirer] у масиві HTML, що містить товари з кошика, має URL-адресу у вигляді [retirerachat.do?id=ID], де ID — це поле [id]товару, який потрібно видалити з кошика.
Код:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Contenu de votre panier</h2>
<table border="1">
<tr>
<td>Article</td><td>Qte</td><td>Pu</td><td>Total</td>
</tr>
<c:forEach var="achat" items="${panier.achats}">
<tr>
<td><c:out value="${achat.article.nom}"/></td>
<td><c:out value="${achat.qte}"/></td>
<td><c:out value="${achat.article.prix}"/></td>
<td><c:out value="${achat.total}"/></td>
<td><a href="<c:out value="retirerachat.do?id=${achat.article.id}"/>">Retirer</a></td>
</tr>
</c:forEach>
</table>
<p>
Total de la commande : <c:out value="${panier.total}"/> euros
</body>
</html>
|
Коментарі: одна зміна (виділена вище)
3.6.4.5. paniervide.jsp
На цьому екрані відображається інформація про те, що кошик порожній:

Вона відображається після запиту /panier.do або /retirerachat.do?id=ID. Елементи запиту контролера такі:
| об’єкт Hashtable[] — масив опцій меню |
Код:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Contenu de votre panier</h2>
<p>
Votre panier est vide.
</body>
</html>
|
Коментарі: змін немає.
3.6.4.6. erreurs.jsp
Цей вигляд відображається у разі помилок:

Вона відображається після будь-якого запиту, що призвів до помилки, за винятком дії купівлі з неправильною кількістю, яка обробляється у вікні [INFOS]. Елементи запиту контролера такі:
| об’єкт Hashtable[] — масив опцій меню |
| ArrayList об’єктів типу String, що представляють повідомлення про помилки, які потрібно відобразити |
Код:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Les erreurs suivantes se sont produites</h2>
<ul>
<c:forEach var="erreur" items="${erreurs}">
<li><c:out value="${erreur}"/></li>
</c:forEach>
</ul>
</body>
</html>
|
Коментарі: змін немає.
3.6.4.7. index.jsp
Ця сторінка визначена як головна сторінка додатка у файлі [web.xml] додатка:
| <?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
....
</servlet>
<servlet-mapping>
....
</servlet-mapping>
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
</web-app>
|
Вигляд [index.jsp] просто перенаправляє клієнта до точки входу в додаток:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main.do"/>
Коментарі: одна зміна (виділена вище)
3.6.5. Контролер Struts
Struts має загальний контролер під назвою [ActionServlet]. Відомо, що сервлет має метод [init], який дозволяє ініціалізувати додаток під час його запуску. Якщо ми використовуємо загальний контролер Struts [ActionServlet], ми не маємо доступу до його методу [init]. Тут нам потрібно виконати певні дії під час запуску додатка, а саме — створити екземпляр об’єкта для доступу до моделі. Тому нам потрібен метод [init]. Отже, ми похідний клас [Actionservlet] у наступному класі [MainServlet]:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.IArticlesDomain;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import org.apache.struts.action.ActionServlet;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
/**
* @author ST - ISTIA
*
*/
public class MainServlet extends ActionServlet {
// приватні поля
private ArrayList erreurs = new ArrayList();
private IArticlesDomain articlesDomain = null;
private final String SPRING_CONFIG_FILENAME = "springConfigFileName";
private final String[] parameters = { SPRING_CONFIG_FILENAME };
private ServletConfig config;
private final String ACTION_LISTE = "liste.do";
private final String ACTION_PANIER = "panier.do";
private final String ACTION_ACHAT = "achat.do";
private final String ACTION_INFOS = "infos.do";
private final String ACTION_RETIRER_ACHAT = "retirerachat.do";
private final String ACTION_VALIDATION_PANIER = "validerpanier.do";
private String urlActionListe;
private final String lienActionListe = "Liste des articles";
private String urlActionPanier;
private final String lienActionPanier = "Voir le panier";
private String urlActionValidationPanier;
private final String lienActionValidationPanier = "Valider le panier";
private Hashtable hActionListe = new Hashtable(2);
private Hashtable hActionPanier = new Hashtable(2);
private Hashtable hActionValidationPanier = new Hashtable(2);
// геттери — сеттери
public IArticlesDomain getArticlesDomain() {
return articlesDomain;
}
public void setArticlesDomain(IArticlesDomain articlesDomain) {
this.articlesDomain = articlesDomain;
}
public ArrayList getErreurs() {
return erreurs;
}
public void setErreurs(ArrayList erreurs) {
this.erreurs = erreurs;
}
public Hashtable getHActionListe() {
return hActionListe;
}
public void setHActionListe(Hashtable actionListe) {
hActionListe = actionListe;
}
public Hashtable getHActionPanier() {
return hActionPanier;
}
public void setHActionPanier(Hashtable actionPanier) {
hActionPanier = actionPanier;
}
public Hashtable getHActionValidationPanier() {
return hActionValidationPanier;
}
public void setHActionValidationPanier(Hashtable actionValidationPanier) {
hActionValidationPanier = actionValidationPanier;
}
public void init() throws ServletException{
// ініціалізація батьківського класу
super.init();
// отримано параметри ініціалізації сервлета
config = getServletConfig();
String param = null;
for (int i = 0; i < parameters.length; i++) {
param = config.getInitParameter(parameters[i]);
if (param == null) {
// зберігаємо інформацію про помилку
erreurs.add("Paramètre [" + parameters[i]
+ "] absent dans le fichier [web.xml]");
}
}
// чи є помилки?
if (erreurs.size() != 0) {
return;
}
// створюється об’єкт IArticlesDomain для доступу до бізнес-шару
try {
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource((String) config
.getInitParameter(SPRING_CONFIG_FILENAME))))
.getBean("articlesDomain");
} catch (Exception ex) {
// зберігаємо помилку
erreurs.add("Erreur de configuration de l'accès aux données : "
+ ex.toString());
return;
}
// зберігаються певні URL-адреси додатка
hActionListe.put("href", ACTION_LISTE);
hActionListe.put("lien", lienActionListe);
hActionPanier.put("href", ACTION_PANIER);
hActionPanier.put("lien", lienActionPanier);
hActionValidationPanier.put("href", ACTION_VALIDATION_PANIER);
hActionValidationPanier.put("lien", lienActionValidationPanier);
// процес завершено
return;
}
}
|
Коментарі:
- цінність класу полягає в його методі [init] та його приватних полях
- метод [init] виконує те саме, що й метод [init] контролера попередньої версії:
- він перевіряє наявність певних параметрів конфігурації
- створює екземпляр сервісу доступу до домену додатка за допомогою Spring
- вона ініціалізує список помилок для вказівки можливих помилок ініціалізації
- перед початком роботи метод [init] викликає метод [init] батьківського класу [ActionServlet]. Саме він оброблятиме файл конфігурації Struts [struts-config.xml].
- Визначено певну кількість приватних полів із відповідними методами доступу:
- помилки: список помилок, виявлених [init]
- [hActionListe, hActionPanier, hActionValidationPanier]: словники. Кожен із них містить інформацію, необхідну для відображення пункту головного меню, що відображається поданням [entete.jsp]
- acticlesDomain: служба доступу до моделі додатка
- контролер додатка Struts доступний для класів [Action], які відповідають за обробку різних можливих дій. Ці класи матимуть доступ до вищезазначених приватних полів, оскільки вони мають публічні методи доступу.
Інстанціювання цього контролера забезпечується файлом [web.xml]:
| <web-app>
<servlet>
<servlet-name>strutswebarticles</servlet-name>
<servlet-class>istia.st.articles.web.struts.MainServlet</servlet-class>
<init-param>
<param-name>config</param-name>
<param-value>/WEB-INF/struts-config.xml</param-value>
</init-param>
<init-param>
<param-name>springConfigFileName</param-name>
<param-value>spring-config-sqlmap-firebird.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>strutswebarticles</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
|
Будь-який файл URL, що закінчується на .do, оброблятиметься екземпляром класу [istia.st.articles.web.struts.MainServlet]
3.6.6. Дії додатка Struts
3.6.6.1. Вступ
Кожна дія Struts буде об’єктом класу. У попередній версії кожна дія була об’єктом методу в контролері додатка. Написання класу [Action] найчастіше полягає в:
- копіювання та вставлення методу, який використовувався у попередній версії
- адаптації коду до конвенцій Struts
3.6.6.2. main.do, liste.do
Ці дві дії є ідентичними та визначені у [struts-config.xml] таким чином:
| <action path="/main" type="istia.st.articles.web.struts.ListeArticlesAction">
<forward name="afficherListeArticles" path="/vues/liste.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
<action path="/liste" type="istia.st.articles.web.struts.ListeArticlesAction">
<forward name="afficherListeArticles" path="/vues/liste.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
|
Коли одна з цих дій [main.do, liste.do] виконується в браузері, отримуємо такий результат:

Код класу [ListeArticlesAction] має такий вигляд:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST - ISTIA
*
*/
public class ListeArticlesAction extends Action {
/**
* affichage de la liste des articles - s'appuie sur la couche [domain]
*
* @param mapping :
* configuration de l'action dans struts-config.xml
* @param form :
* le formulaire passé à l'action - ici aucun
* @param request :
* la requête HTTP du client
* @param response :
* la réponse HTTP au client
*/
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// сервлет контролю
MainServlet mainServlet = (MainServlet) this.getServlet();
// помилки ініціалізації?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// відображається сторінка помилок
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// об’єкт доступу до домену
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
// список помилок
ArrayList erreurs = new ArrayList();
// запит списку товарів
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// зберігається інформація про помилку
erreurs.add("Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
// помилок?
if (erreurs.size() != 0) {
// відображається сторінка помилок
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// відображається список статей
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionPanier() });
return mapping.findForward("afficherListeArticles");
}
}
|
Коментарі:
- написання коду класу [Action] полягає, в основному, у написанні коду його методу [execute]
- у екземплярі контролера збережено певну кількість інформації. Отримати посилання на нього можна за допомогою:
// сервлет управління
MainServlet mainServlet = (MainServlet) this.getServlet();
- отримуємо список помилок ініціалізації, збережених контролером. Якщо цей список не порожній, клієнту надсилається представлення [ERREURS]:
| // помилки ініціалізації?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// відображається сторінка помилок
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
|
Вигляд, який фактично буде надіслано клієнту, надається нам [struts-config.xml]:
<action path="/main" type="istia.st.articles.web.struts.ListeArticlesAction">
<forward name="afficherListeArticles" path="/vues/liste.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
Це вигляд [/vues/erreurs.jsp]. Читачеві пропонується перевірити, що очікується від цього вигляду. Ця інформація надається тут дією в об’єкті [request] у вигляді атрибутів.
- Завдяки контролеру дія може отримати об’єкт доступу до моделі додатка (доменний шар):
// об'єкт доступу до домену
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
- Після цього можна отримати список статей:
| // список помилок
ArrayList erreurs = new ArrayList();
// запит списку товарів
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// записування помилки
erreurs.add("Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
|
- у разі виникнення помилок надсилається представлення [ERREURS]:
| // помилок?
if (erreurs.size() != 0) {
// відображається сторінка помилок
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
|
- в іншому випадку надсилається погляд [LISTE]:
| // відображається список статей
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionPanier() });
return mapping.findForward("afficherListeArticles");
|
Вигляд, який фактично буде надіслано клієнту, надається нам [struts-config.xml]:
<action path="/main" type="istia.st.articles.web.struts.ListeArticlesAction">
<forward name="afficherListeArticles" path="/vues/liste.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
Це вигляд [/vues/liste.jsp]. Просимо читача перевірити, що очікується від цього вигляду. Ця інформація надається тут дією в об’єкті [request] у вигляді атрибутів.
3.6.6.3. infos.do
Ця дія призначена для надання інформації про один із елементів, що відображаються у поданні [LISTE]:
Ця дія налаштована в [struts-config.xml] наступним чином:
<action path="/infos" type="istia.st.articles.web.struts.InfosArticleAction">
<forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
Код класу [InfosArticleAction] є таким:
| package istia.st.articles.web.struts;
import istia.st.articles.dao.Article;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST-ISTIA
*
*/
public class InfosArticleAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// сервлет контролю
MainServlet mainServlet = (MainServlet) this.getServlet();
// помилки ініціалізації?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// відображається сторінка помилок
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// об'єкт доступу до домену
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
// список помилок
ArrayList erreurs = new ArrayList();
// отримано запитуваний ідентифікатор
String strId = request.getParameter("id");
// щось?
if (strId == null) {
// ненормально
erreurs.add("action incorrecte([infos,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// перетворюємо strId у ціле число
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// ненормально
erreurs.add("action incorrecte([infos,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// запитується ідентифікатор ключа
Article article = null;
try {
article = articlesDomain.getArticleById(id);
} catch (UncheckedAccessArticlesException ex) {
// ненормально
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
if (article == null) {
// ненормально
erreurs.add("Article de clé [" + id + "] inexistant");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// запис додано до сесії
request.getSession().setAttribute("article", article);
// відображається інформаційна сторінка
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherInfosArticle");
}
}
|
Коментарі:
- початок методу [execute] ідентичний тому, що розглядався раніше. Це також стосується інших дій.
- Метод отримує параметр [id], який зазвичай має бути в URL-адресі. Ця адреса має мати вигляд [/infos.do?id=X]. Проводяться різні перевірки для виявлення наявності та правильності параметра [id]. У разі виникнення проблеми надсилається представлення [ERREURS].
- Якщо [id] є дійсним, відповідний товар запитується на рівні [domain]. Якщо на цьому рівні виникає виняток або товар не знайдено, знову надсилається представлення [ERREURS].
- Якщо все гаразд, отриманий товар додається до сесії. Це спірне питання. Тут виходимо з того, що клієнт, можливо, придбає цей товар. Якщо він це зробить, ми будемо шукати його в сесії, а не запитувати знову на рівні [domain].
- Нарешті, відображається вигляд [INFOS]. Вигляд, який фактично буде надіслано клієнту, надається нам [struts-config.xml]:
<action path="/infos" type="istia.st.articles.web.struts.InfosArticleAction">
<forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
Це вигляд [/vues/infos.jsp]. Пропоную читачеві перевірити, що саме очікується від цього вигляду. Ця інформація надається тут дією в об’єкті [request] у вигляді атрибутів.
3.6.6.4. achat.do
Ця дія призначена для придбання товару, відображеного у попередньому поданні [INFOS]:
Після придбання товару знову відображається вигляд [LISTE] (вигляд праворуч). Якщо розглянути код HTML з вигляду ліворуч вище, можна побачити, що тег <form> визначено таким чином:
| <form method="post" action="achat.do?id=3"/>
<table>
<tr>
<td><input type="submit" value="Acheter"></td>
<td>Qte <input type="text" name="qte" size="3" value=""></td>
<td></td>
</tr>
</table>
</form>
|
Отже, бачимо, що форма надсилається до контролера з дією [achat.do].
Ця дія налаштована в [struts-config.xml] наступним чином:
<action
path="/achat" type="istia.st.articles.web.struts.AchatArticleAction">
<forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
<forward name="afficherListeArticles" path="/main.do"/>
</action>
Код класу [AchatArticleAction] такий:
| package istia.st.articles.web.struts;
import istia.st.articles.dao.Article;
import istia.st.articles.domain.Achat;
import istia.st.articles.domain.Panier;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST
*
*/
public class AchatArticleAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// сервлет контролю
MainServlet mainServlet = (MainServlet) this.getServlet();
// помилки ініціалізації?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// відображається сторінка помилок
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// список помилок для цієї дії
ArrayList erreurs = new ArrayList();
// отримано кількість, що була придбана
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// неправильна кількість
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherInfosArticle");
}
// отримано сесію клієнта
HttpSession session = request.getSession();
// отримано товар, доданий до сесії
Article article = (Article) session.getAttribute("article");
// сесія закінчилася?
if(article==null){
// відображається сторінка помилок
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// створюється нова покупка
Achat achat = new Achat(article, qté);
// додаємо товар до кошика клієнта
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
// повернення до списку товарів
return mapping.findForward("afficherListeArticles");
}
}
|
Коментарі:
- початок методу [execute] ідентичний тим, що розглядалися раніше.
- Нагадаємо форму формуляра, що надсилається до контролера:
| <form method="post" action="achat.do?id=3"/>
<table>
<tr>
<td><input type="submit" value="Acheter"></td>
<td>Qte <input type="text" name="qte" size="3" value=""></td>
<td></td>
</tr>
</table>
</form>
|
- У запиті є два параметри: [id] — номер придбаного товару, [qte] — кількість, що була придбана.
- Перевіряється наявність та правильність параметра [qte]. Якщо цей параметр визнано неправильним, користувачеві повертається представлення [INFOS] разом із повідомленням про помилку:
| // список помилок щодо цієї дії
ArrayList erreurs = new ArrayList();
// отримано кількість, що була придбана
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// неправильна кількість
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherInfosArticle");
}
|
- придбаний товар отримується із сесії. Термін дії сесії міг закінчитися. У цьому випадку надсилається подання [ERREURS]:
| // отримано сесію клієнта
HttpSession session = request.getSession();
// отримано товар, доданий до сесії
Article article = (Article) session.getAttribute("article");
// сесія закінчилася?
if(article==null){
// відображається сторінка помилок
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
|
- якщо сесія не закінчилася, товар додається до кошика, який також отримується з сесії:
| // отримуємо сесію клієнта
HttpSession session = request.getSession();
// завантажуємо статтю, додану до сесії
Article article = (Article) session.getAttribute("article");
// сесія закінчилася?
if(article==null){
// відображається сторінка помилок
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// створюється нова покупка
Achat achat = new Achat(article, qté);
// додаємо товар до кошика клієнта
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
|
- нарешті, надсилається вигляд [LISTE]:
// повернення до списку товарів
return mapping.findForward("afficherListeArticles");
- Вигляд, який фактично буде надіслано клієнту, надається нам [struts-config.xml]:
<action
path="/achat" type="istia.st.articles.web.struts.AchatArticleAction">
<forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
<forward name="afficherListeArticles" path="/main.do"/>
</action>
Це подання [/main.do]. Це подання не є поданням, а дією. Отже, описана вище дія [/main.do] буде виконана та відобразить список статей.
3.6.6.5. panier.do
Ця дія призначена для відображення всіх покупок клієнта. Вона доступна через пункт меню [Voir le panier]:
Код HTML, пов'язаний із посиланням [Voir le panier], має такий вигляд:
<a href="panier.do">Voir le panier</a>
Дія [panier.do] налаштована в [struts-config.xml] наступним чином:
<action
path="/panier" type="istia.st.articles.web.struts.VoirPanierAction">
<forward name="afficherPanier" path="/vues/panier.jsp"/>
<forward name="afficherPanierVide" path="/vues/paniervide.jsp"/>
</action>
Код класу [VoirPanierAction] такий:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.Panier;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST-ISTIA
*
*/
public class VoirPanierAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// сервлет управління
MainServlet mainServlet = (MainServlet) this.getServlet();
// помилки ініціалізації?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// відображається сторінка помилок
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// відображення кошика
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null || panier.getAchats().size() == 0) {
// кошик порожній
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherPanierVide");
} else {
// у кошику є щось
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe(), mainServlet.getHActionValidationPanier() });
return mapping.findForward("afficherPanier");
}
}
}
|
Коментарі:
- початок методу [execute] ідентичний тим, що розглядалися раніше.
- кошик береться з сесії, в якій він зазвичай знаходиться. Сесія могла закінчитися, і тоді кошика немає. Цей випадок не розглядається як помилка, а просто вважається, що кошик порожній.
- якщо кошик порожній, відображається сторінка [PANIERVIDE]
- в іншому випадку — вигляд [PANIER]
Види, які фактично надсилаються клієнту, визначаються дією:
<action
path="/panier" type="istia.st.articles.web.struts.VoirPanierAction">
<forward name="afficherPanier" path="/vues/panier.jsp"/>
<forward name="afficherPanierVide" path="/vues/paniervide.jsp"/>
</action>
3.6.6.6. retirerachat.do
Ця дія призначена для видалення товару з кошика:
Після дії [retirerachat.do] кошик відображається знову (вигляд праворуч вище). Якщо розглянути код HTML посилання [Valider le panier] з вигляду ліворуч вище, отримаємо таке:
<a href="retirerachat.do?id=3">Retirer</a>
Отже, дія [retirerachat.do] отримує як параметр ідентифікатор товару, який потрібно видалити з кошика. Ця дія налаштована в [struts-config.xml] наступним чином:
<action
path="/retirerachat" type="istia.st.articles.web.struts.RetirerAchatAction">
<forward name="afficherPanier" path="/panier.do"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
Код класу [RetirerAchatAction] такий:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.Panier;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST-ISTIA
*
*/
public class RetirerAchatAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// сервлет управління
MainServlet mainServlet = (MainServlet) this.getServlet();
// помилки ініціалізації?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// відображається сторінка помилок
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// список помилок для цієї дії
ArrayList erreurs = new ArrayList();
// отримання кошика
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// сесія закінчилася
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherPanierVide");
}
// отримано ідентифікатор товару, який потрібно видалити
String strId = request.getParameter("id");
// щось?
if (strId == null) {
// ненормально
erreurs.add("action incorrecte([retirerachat,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// перетворюємо strId у ціле число
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// ненормально
erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// видаляємо покупку
panier.enlever(id);
// знову відображаємо кошик
return mapping.findForward("afficherPanier");
}
}
|
Коментарі:
- початок методу [execute] ідентичний тим, що розглядалися раніше.
- наступний код призначений для перевірки наявності та правильності параметра [id]. Якщо він виявляється неправильним, надсилається представлення [ERREURS].
- В іншому випадку товар видаляється з кошика:
// видаляємо покупку
panier.enlever(id);
- після чого кошик відображається знову:
// повторно відображається кошик
return mapping.findForward("afficherPanier");
Вигляд, який фактично надсилається клієнту, визначається дією:
<action
path="/retirerachat" type="istia.st.articles.web.struts.RetirerAchatAction">
<forward name="afficherPanier" path="/panier.do"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
Як бачимо, насправді буде запущено дію [/panier.do]. Вона вже була описана. Вона відобразить перегляд [PANIER] або [PANIERVIDE] залежно від стану кошика.
3.6.6.7. validerpanier.do
Ця дія призначена для підтвердження покупок, зроблених клієнтом. Конкретно це означає одну єдину дію: у базі даних запаси придбаних товарів зменшуються на кількість, що була куплена. Ця дія доступна в наступному меню:
Код HTML посилання [Valider le panier] виглядає наступним чином:
<a href="validerpanier.do">Valider le panier</a>
При активації цього посилання запаси зменшуються, і список товарів знову відображається.
Ця дія налаштована в [struts-config.xml] наступним чином:
<action
path="/validerpanier" type="istia.st.articles.web.struts.ValiderPanierAction">
<forward name="afficherListeArticles" path="/main.do"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
Код класу [ValiderPanierAction] такий:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.domain.Panier;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST-ISTIA
*
*/
public class ValiderPanierAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// сервлет контролю
MainServlet mainServlet = (MainServlet) this.getServlet();
// помилки ініціалізації?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// відобразити сторінку помилок
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// об'єкт доступу до домену
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
// список помилок для цієї дії
ArrayList erreurs = new ArrayList();
// покупець підтвердив свій кошик
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// сесія закінчилася
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// кошик підтверджено
try {
articlesDomain.acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// ненормальний стан
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// отримано можливі помилки
erreurs = articlesDomain.getErreurs();
if (erreurs.size() != 0) {
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe(),mainServlet.getHActionPanier() });
return mapping.findForward("afficherErreurs");
}
// все виглядає OK — відображається список товарів
return mapping.findForward("afficherListeArticles");
}
}
|
Коментарі:
- початок методу [execute] ідентичний тим, що розглядалися раніше.
- з сесії витягується кошик. Якщо термін дії сесії закінчився, надсилається представлення [ERREURS]:
| // список помилок щодо цієї дії
ArrayList erreurs = new ArrayList();
// покупець підтвердив свій кошик
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// сесія закінчилася
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
|
- підтверджуються покупки з кошика. Можуть виникнути помилки, якщо запасів недостатньо для задоволення покупок. У цьому випадку надсилається вигляд [ERREURS]:
| // кошик підтверджено
try {
articlesDomain.acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// аномалія
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// отримано можливі помилки
erreurs = articlesDomain.getErreurs();
if (erreurs.size() != 0) {
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe(),mainServlet.getHActionPanier() });
return mapping.findForward("afficherErreurs");
}
|
- якщо все пройшло успішно, знову відображається список товарів:
// все виглядає OK — відображаємо список товарів
return mapping.findForward("afficherListeArticles");
Вигляд, який фактично надсилається клієнту, визначається дією:
<action
path="/validerpanier" type="istia.st.articles.web.struts.ValiderPanierAction">
<forward name="afficherListeArticles" path="/main.do"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
Як бачимо, насправді буде запущено дію [/main.do]. Вона вже була описана. Ця дія відобразить вигляд [LISTE].
3.7. Архітектура MVC із використанням Spring
3.7.1. Загальна архітектура додатка
Повернемося до архітектури MVC додатка:
У першій версії:
- функцію контролера виконував сервлет
- відображення забезпечувалося сторінками JSP
- модель забезпечувалася набором із трьох архівів .jar
У версії Struts:
- контролер забезпечувався сервлетом, похідним від загального сервлета [ActionServlet] Struts
- представлення забезпечувалися тими самими сторінками JSP, що й у версії [Struts]
- модель забезпечувалася тими самими трьома архівами
У версії Spring:
- контролер буде реалізовано за допомогою сервлета, наданого Spring, — [DispatcherServlet]
- види забезпечуватимуться тими самими сторінками JSP, що й раніше, за винятком деяких дрібниць
- модель буде реалізована тими самими трьома архівами
Ми побачимо, що перехід нашого додатка зі Struts на Spring є простим, якщо погодитися не використовувати всі елементи, рекомендовані для ортодоксальної архітектури Spring MVC. Основні зміни такі:
- дії, які в Struts оброблялися в окремих методах сервлета/контролера або екземплярами класів, похідних від класу [Action], тепер обробляються екземплярами класів, що реалізують інтерфейс Spring [Controller]
- необхідні конфігураційні файли тепер мають такий вигляд:
- [web.xml], оскільки ми маємо справу з веб-додатком. Цей файл містить Listener, який під час ініціалізації додатка буде використовувати файл [applicationContext.xml]
- [applicationContext.xml], що дозволить створити біни, необхідні додатку, зокрема бін сервісу доступу до моделі
- види JSP будуть ідентичними тим, що використовуються в Struts. Нам доведеться створити новий.
Нагадаємо архітектуру MVC STRUTS, яка використовувалася в попередній версії:
| бізнес-класи, класи доступу до даних та база даних |
| сторінки JSP |
| сервлет для обробки запитів клієнтів, об’єкти [Action] |
У Spring ми використовуємо таку саму архітектуру:
| бізнес-класи, класи доступу до даних та база даних |
| сторінки JSP |
| сервлет для обробки запитів клієнтів, об’єкти, що реалізують інтерфейс [Controller] |
- контролер є серцевиною додатка. Усі запити клієнта проходять через нього. Це загальний сервлет, що надається SPRING. Він належить до типу [DispatcherServlet]. Відтепер ми будемо називати цей контролер контролером [Spring].
- Контролер [Spring] спрямує запит клієнта до одного з екземплярів типу [Controller]. Для кожної дії, що підлягає обробці, буде існувати один такий екземпляр. Він визначається у запитуваному URL-адресі, як і в Struts. Таким чином, ми знатимемо, що запитувана дія — це дія [liste], оскільки запитуваний URL-адреса — [liste.do]
- якщо C — це контекст додатка, контролер [Spring] використовує файл [C-servlet.xml], який виконує роль конфігураційного файлу struts-config.xml у версії Struts. Кожній дії, яку має обробити додаток, прив’язується ім’я класу типу Controller, відповідального за обробку запиту.
- контролер передає управління об’єкту типу Controller, пов’язаному з цією дією. Для цього він викликає метод handleRequest цього об’єкта, передаючи йому запит клієнта. Саме тут розробник виконує необхідні дії: за потреби йому доведеться звертатися до бізнес-класів або класів доступу до даних. По завершенні обробки об’єкт Controller повертає контролеру назву подання, яке той має надіслати клієнту у відповідь.
- У своєму конфігураційному файлі контролер знайде URL, пов’язаний з назвою подання, яке йому було доручено відобразити. Після цього він надсилає це подання. Обмін даними з клієнтом завершено.
3.7.2. Модель
Вона така сама, як і в двох попередніх версіях. Складається з Java-архівів [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].
3.7.3. Конфігурація додатка
3.7.3.1. Загальна архітектура
Загальна архітектура проекту Eclipse така:

3.7.3.2. Налаштування доступу до даних
Оскільки інтерфейс доступу до даних не змінюється, відповідні файли конфігурації залишаються такими самими, як і в попередній версії. Вони визначені у файлі [WEB-INF/src]:

На знімку екрана вище файли [articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties] належать до попередніх версій.
3.7.3.3. Каталог архівів
У [WEB-INF/lib] містяться ті самі архіви, що й у попередній версії, за винятком архівів Struts, які більше не потрібні:

3.7.3.4. Налаштування додатка
Налаштування додатка здійснюється за допомогою трьох файлів: [web.xml, applicationContext.xml, springwebarticles-servlet.xml] у папці [WEB-INF]:

Файл [web.xml] має такий вигляд:
| <?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<!-- завантажувач контексту Spring для додатка -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- сервлет -->
<servlet>
<servlet-name>springwebarticles</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<!-- маппінг URL-адрес -->
<servlet-mapping>
<servlet-name>springwebarticles</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<!-- вхідний документ -->
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
</web-app>
|
Що міститься в цьому файлі?
- Головна сторінка додатка — [/vues/index.jsp] (welcome-file)
- запити на URL-адреси у форматі *.do будуть перенаправлені на сервлет [springwebarticles] (servlet-mapping)
- сервлет [springwebarticles] є екземпляром класу [org.springframework.web.servlet.DispatcherServlet] (servlet-name, servlet-class), наданого Spring.
- Слухач [org.springframework.web.context.ContextLoaderListener] запускатиметься під час запуску додатка. Його основною функцією буде створення екземплярів бінів Spring, визначених у файлі [applicationContext.xml]
Файл [applicationContext.xml] має такий вигляд:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- клас доступу до даних -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- бізнес-клас -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
<!-- конфігурація веб-додатку-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
</beans>
|
Тут є як знайомі, так і менш знайомі елементи. Під час ініціалізації додатка буде створено три біни:
- articlesDao: сервіс доступу до шару [dao]
- articlesDomain: служба доступу до моделі
- config: bean, у якому ми будемо збирати інформацію, якою повинні ділитися всі клієнти. Цей bean виконуватиме роль, яку зазвичай виконує контекст додатка, але з типізованою, а не нетипізованою інформацією.
Останній файл [springwebarticles.xml] визначає дії, які приймає додаток, у спосіб, дуже схожий на той, що використовується у файлі Struts [struts-config.xml]. Його вміст такий:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- обробники дій = контролери -->
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="InfosController"
class="istia.st.articles.web.spring.InfosController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="AchatController"
class="istia.st.articles.web.spring.AchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="VoirPanierController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="RetirerAchatController"
class="istia.st.articles.web.spring.RetirerAchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="ValiderPanierController"
class="istia.st.articles.web.spring.ValiderPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<!-- маппінг додатка-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/liste.do">ListController</prop>
<prop key="/main.do">ListController</prop>
<prop key="/infos.do">InfosController</prop>
<prop key="/achat.do">AchatController</prop>
<prop key="/panier.do">VoirPanierController</prop>
<prop key="/retirerachat.do">RetirerAchatController</prop>
<prop key="/validerpanier.do">ValiderPanierController</prop>
</props>
</property>
</bean>
<!-- обробник подання -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass">
<value>org.springframework.web.servlet.view.JstlView</value>
</property>
<property name="prefix">
<value>/vues/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
<!-- файл повідомлень -->
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename">
<value>messages</value>
</property>
</bean>
</beans>
|
Що вказує цей файл конфігурації?
- що наш контролер оброблятиме такі URL-адреси:
| для відображення списку статей |
| для відображення списку статей |
| для перегляду інформації про конкретний товар |
| щоб придбати певний товар |
| для перегляду кошика покупок |
| щоб видалити товар з кошика |
| для підтвердження кошика |
- попередні дії один в один відповідають діям, що оброблялися контролером у попередніх версіях. Для кожної з них вказано ім'я класу, відповідального за її обробку. Розглянемо приклад дії [/panier.do]:
- вона має оброблятися біном [VoirPanierController]. Ця назва є довільною. Це просто ключ.
<prop key="/panier.do">VoirPanierController</prop>
- ключ [VoirPanierController] — це ім’я біна, визначеного в тому самому файлі конфігурації:
<bean id="VoirPanierController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
- біан [VoirPanierController] визначає:
- клас [ istia.st.articles.web.spring.VoirPanierController], який потрібно інстанціювати для обробки дії
- як створити його екземпляр. Тут як параметр передається бін [config], визначений [applicationContext.xml] та інстанційований під час запуску додатка. Це буде зроблено для всіх дій [Controller]. Таким чином, кожна з них матиме у приватному полі об’єкт [config], у якому міститься вся інформація, спільна для всіх клієнтів.
- Спосіб, у який мають вирішуватися імена переглядів:
<!-- менеджер подання -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass">
<value>org.springframework.web.servlet.view.JstlView</value>
</property>
<property name="prefix">
<value>/vues/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
Як і у випадку зі Struts, екземпляр [Controller], який обробляє дію, після обробки поверне ключ контролеру Spring, щоб вказати йому, який вигляд він повинен відобразити. На основі цього ключа можуть існувати різні стратегії для генерації подання, пов’язаного з цим ключем. Обраною стратегією є та, що визначена біном [viewResolver]. Тут цей бін пов’язаний із класом [org.springframework.web.servlet.view.InternalResourceViewResolver] із різними параметрами ініціалізації. Не вдаючись у подробиці, бін [viewResolver] тут вказує, що якщо ключ подання — «XX», то згенерованим поданням буде [/vues/XX.jsp]. Тип подань, що надсилаються клієнту, можна змінити різними способами:
- змінивши клас реалізації для біна [viewResolver]
- змінивши параметри ініціалізації класу реалізації
Таким чином, можна перейти від подання HTML до подання XML, просто змінивши значення біна [viewResolver]
- — ім’я файлу повідомлень для додатка (messageSource). У цьому випадку файл існуватиме, але буде порожнім. Він не використовуватиметься. Його слід розмістити в [ClassPath] додатка. Тут він буде розміщений у [WEB-INF/classes]. У середовищі Eclipse цей результат можна отримати, розмістивши його в [WEB-INF/src]:

3.7.4. Види JSP
Використовуватимуться перегляди JSP, які застосовує Struts. Жоден з них не змінюється:

Створюється лише один новий вигляд: redirpanier.jsp. Він служить виключно для перенаправлення клієнта до дії [/panier.do]. Його код такий:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/panier.do"/>
Читачеві пропонується ще раз ознайомитися з визначенням різних подань у версії Struts.
3.7.5. Обробка дій
Класи, необхідні для обробки різних дій, зібрано в пакеті [istia.st.articles.web.spring]:

Нагадаємо механізм роботи додатка Spring на прикладі:
- користувач надсилає запит на URL-адресу [http://localhost:8080/springwebarticles/main.do]

Що сталося?
- був відкритий файл [web.xml] з додатка [springwebarticles]:
| <?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<!-- завантажувач контексту Spring для додатка -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- сервлет -->
<servlet>
<servlet-name>springwebarticles</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<!-- маппінг URL-адрес -->
<servlet-mapping>
<servlet-name>springwebarticles</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<!-- вхідний документ -->
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
</web-app>
|
- якщо це був перший запит до додатка, відбулася низка подій:
- був завантажений прослуховувач [ org.springframework.web.context.ContextLoaderListener]
- він використав файл конфігурації [applicationContext.xml]:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- клас доступу до даних -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- бізнес-клас -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
<!-- конфігурація веб-додатку-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
</beans>
|
- вищезазначені біни були створені в контексті додатка
- потім було використано файл [springwebarticles-servlet.xml]:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- обробники дій = контролери -->
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="InfosController"
class="istia.st.articles.web.spring.InfosController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="AchatController"
class="istia.st.articles.web.spring.AchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="VoirPanierController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="RetirerAchatController"
class="istia.st.articles.web.spring.RetirerAchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="ValiderPanierController"
class="istia.st.articles.web.spring.ValiderPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<!-- маппінг додатка-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/liste.do">ListController</prop>
<prop key="/main.do">ListController</prop>
<prop key="/infos.do">InfosController</prop>
<prop key="/achat.do">AchatController</prop>
<prop key="/panier.do">VoirPanierController</prop>
<prop key="/retirerachat.do">RetirerAchatController</prop>
<prop key="/validerpanier.do">ValiderPanierController</prop>
</props>
</property>
</bean>
<!-- обробник подання -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass">
<value>org.springframework.web.servlet.view.JstlView</value>
</property>
<property name="prefix">
<value>/vues/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
<!-- файл повідомлень -->
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename">
<value>messages</value>
</property>
</bean>
</beans>
|
- біни [Controller], визначені цим файлом, також були створені
- тепер усе готово для обробки запиту клієнта. Він мав вигляд: [http://localhost:8080/springwebarticles]. Тут запитується не URL з контексту, а сам контекст. Тому використовується розділ [welcome-file] з файлу [web.xml].
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
- Вигляд [index.jsp] такий:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main.do"/>
- отже, клієнту пропонується перейти за URL-адресою [http://localhost:8080/springwebarticles/main.do]. Він це робить.
- Отже, контролер Spring отримує новий запит. Він використовує свій файл [springwebarticles-servlet.xml]:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- обробники дій = контролери -->
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
....
</bean>
<!-- маппінг додатка-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/liste.do">ListController</prop>
<prop key="/main.do">ListController</prop>
<prop key="/infos.do">InfosController</prop>
<prop key="/achat.do">AchatController</prop>
<prop key="/panier.do">VoirPanierController</prop>
<prop key="/retirerachat.do">RetirerAchatController</prop>
<prop key="/validerpanier.do">ValiderPanierController</prop>
</props>
</property>
</bean>
</beans>
|
- Цей файл вказує, що дію [/main.do] має обробити бін [ListController].
- Запит клієнта передається до методу [handleRequest] біна [ListController]. Цей метод виконує свою роботу та повертає контролеру ключ від подання, яке потрібно відобразити. У цьому випадку, якщо все йде добре, цим ключем буде [liste].
- Контролер Spring використовує бін [viewResolver] із файлу конфігурації [springwebarticles-servlet.xml], щоб визначити вигляд, пов’язаний із цим ключем. У цьому випадку це буде вигляд [/vues/liste.jsp]
- Вигляд [/vues/liste.jsp] надсилається клієнту
3.7.6. Ініціалізація додатка Spring
Ми вже зазначали, що під час запуску додатка створюються екземпляри бінів із файлу [applicationContext.xml]:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- клас доступу до даних -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- бізнес-клас -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
<!-- конфігурація веб-додатку-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
</beans>
|
Нам відомі біни [articlesDao, articlesDomain], але не бін [config]. Останній визначається наступним класом Java:
| package istia.st.articles.web.spring;
import java.util.Hashtable;
import istia.st.articles.domain.IArticlesDomain;
/**
* @author ST - ISTIA
*/
public class Config {
// приватні поля
private IArticlesDomain articlesDomain = null;
private final String ACTION_LISTE = "liste.do";
private final String ACTION_PANIER = "panier.do";
private final String ACTION_VALIDATION_PANIER = "validerpanier.do";
private final String lienActionListe = "Liste des articles";
private final String lienActionPanier = "Voir le panier";
private final String lienActionValidationPanier = "Valider le panier";
private Hashtable hActionListe = new Hashtable(2);
private Hashtable hActionPanier = new Hashtable(2);
private Hashtable hActionValidationPanier = new Hashtable(2);
// методи getter-setter
public IArticlesDomain getArticlesDomain() {
return articlesDomain;
}
public void setArticlesDomain(IArticlesDomain articlesDomain) {
this.articlesDomain = articlesDomain;
}
public Hashtable getHActionListe() {
return hActionListe;
}
public Hashtable getHActionPanier() {
return hActionPanier;
}
public Hashtable getHActionValidationPanier() {
return hActionValidationPanier;
}
// ініціалізація веб-додатку
public void init() {
// зберігаємо певні URL-адреси додатка
hActionListe.put("href", ACTION_LISTE);
hActionListe.put("lien", lienActionListe);
hActionPanier.put("href", ACTION_PANIER);
hActionPanier.put("lien", lienActionPanier);
hActionValidationPanier.put("href", ACTION_VALIDATION_PANIER);
hActionValidationPanier.put("lien", lienActionValidationPanier);
// все готово
return;
}
}
|
Цей клас виконує те саме, що й метод [init] сервлета веб-додатку. Він ініціалізує додаток. У даному випадку це відбувається наступним чином:
- оскільки бін [config] визначений у [applicationContext.xml] наступним чином:
<!-- конфігурація веб-додатку-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
під час створення його приватне поле [articlesDomain] було ініціалізовано
- потім, завдяки атрибуту [init-method="init"] вищезазначеного біна, виконується метод [init] класу, пов’язаного з цим біном. Тут він ініціалізує три словники [hActionListe, hActionPanier, hActionValidationPanier], які використовуються для генерації трьох можливих посилань у меню, що пропонується користувачеві.
- Створюються публічні методи доступу, щоб зробити ці приватні поля доступними для екземплярів типу [Controller], які оброблятимуть дії.
3.7.7. Дії [Controller] у додатку Spring
3.7.7.1. Вступ
Кожна дія Spring буде реалізована класом типу [Controller]. У версії Struts кожна дія реалізовувалася класом типу [Action]. Написання класу [Controller] найчастіше полягає в:
- скопіювання та вставлення класу [Action], який використовувався у версії Struts,
- адаптації коду до конвенцій Spring
3.7.7.2. main.do, liste.do
Ці дві дії є ідентичними та визначені в [springwebarticles-servlet.xml] наступним чином:
| <!-- маппінг додатка-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/liste.do">ListController</prop>
<prop key="/main.do">ListController</prop>
...
</props>
</property>
</bean>
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
|
Вони пов’язані з класом [ istia.st.articles.web.spring.ListController], який ми незабаром детально розглянемо. Коли одна з цих дій виконується в браузері, отримуємо такий результат:

Код класу [istia.st.articles.web.spring.ListController] такий:
| package istia.st.articles.web.spring;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class ListController implements Controller {
// конфігурація веб-додатку
Config config;
public void setConfig(Config config) {
this.config = config;
}
// обробка запиту
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// запит списку товарів
List articles = null;
try {
articles = config.getArticlesDomain().getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// зберігання інформації про помилку
ArrayList erreurs = new ArrayList();
erreurs.add("Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
// відображається сторінка помилок
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
// надіслати перегляд помилок
return new ModelAndView("erreurs");
}
// відображається список статей
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { config
.getHActionPanier() });
// надіслати перегляд
return new ModelAndView("liste");
}
}
|
Коментарі:
- клас має приватне поле [config]. Це поле було ініціалізовано Spring під час створення екземпляра біна [ListController]:
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
Як бачимо вище, поле [config] у [ListController] ініціалізується за допомогою біна [config]. Що це таке? Це бін [config], визначений у [applicationContext.xml], c.a.d — екземпляр [istia.st.articles.web.spring.Config], описаний вище.
- Написання коду класу [Controller] полягає, по суті, у написанні коду його методу [handleRequest]
- з моделі запитується список статей. Доступ до неї здійснюється через [config.getArticlesDomain()]. У разі виникнення винятку надсилається представлення [ERREURS]. Результат, що повертається методом [handleRequest], повинен бути типу [ModelAndView]. Цей клас можна інстанціювати різними способами. Тут, як і в усіх інших випадках, створюється екземпляр класу [ModelAndView], передаючи йому ключ подання, яке потрібно відобразити. Нагадаємо, що згідно з конфігурацією біна [viewResolver] запит на відображення подання з ключем XX призведе до відправлення подання [/vues/XX.jsp].
| // запит списку статей
List articles = null;
try {
articles = config.getArticlesDomain().getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// зберігається інформація про помилку
ArrayList erreurs = new ArrayList();
erreurs.add("Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
// відображається сторінка помилок
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
// надіслати перегляд помилок
return new ModelAndView("erreurs");
}
|
- якщо помилок немає, надсилається представлення [LISTE]:
| // відображається список товарів
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { config
.getHActionPanier() });
// надіслати перегляд
return new ModelAndView("liste");
|
3.7.7.3. infos.do
Ця дія призначена для надання інформації про один із елементів, що відображаються у поданні [LISTE]:
Ця дія визначена в [springwebarticles-servlet.xml] наступним чином:
| <!-- маппінг додатка-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/infos.do">InfosController</prop>
...
</props>
</property>
</bean>
...
<bean id="InfosController"
class="istia.st.articles.web.spring.InfosController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
|
Код класу [InfosController] такий:
| package istia.st.articles.web.spring;
import istia.st.articles.dao.Article;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class InfosController implements Controller {
// конфігурація веб-додатку
Config config;
public void setConfig(Config config) {
this.config = config;
}
// обробка запиту
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// список помилок
ArrayList erreurs = new ArrayList();
// отримано запитуваний ідентифікатор
String strId = request.getParameter("id");
// щось таке?
if (strId == null) {
// ненормально
erreurs.add("action incorrecte([infos,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
// перетворення strId у ціле число
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// ненормально
erreurs.add("action incorrecte([infos,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
// запитується ідентифікатор ключа
Article article = null;
try {
article = config.getArticlesDomain().getArticleById(id);
} catch (UncheckedAccessArticlesException ex) {
// ненормально
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
if (article == null) {
// ненормально
erreurs.add("Article de clé [" + id + "] inexistant");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
// запис додано до сесії
request.getSession().setAttribute("article", article);
// відображається інформаційна сторінка
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("infos");
}
}
|
Коментарі:
- метод [handleRequest] отримує параметр [id], який зазвичай має бути в URL-адресі. Ця адреса має мати вигляд [/infos.do?id=X]. Проводяться різні перевірки для виявлення наявності та правильності параметра [id]. У разі проблеми надсилається запит [ERREURS].
- Якщо [id] є дійсним, відповідний товар запитується на рівні [domain]. Якщо на цьому рівні виникає виняток або товар не знайдено, знову надсилається представлення [ERREURS].
- Якщо все гаразд, отриманий товар додається до сесії. Це спірне питання. Тут виходимо з того, що клієнт, можливо, придбає цей товар. Якщо він це зробить, ми будемо шукати його в сесії, а не запитувати знову на рівні [domain].
- Нарешті, відображається вікно [INFOS].
3.7.7.4. achat.do
Ця дія призначена для купівлі товару, відображеного в попередньому вигляді [INFOS]:

Якщо розглянути код HTML цього виду, можна побачити, що тег <form> визначено таким чином:
<form method="post" action="achat.do?id=3"/>
<table>
<tr>
<td><input type="submit" value="Acheter"></td>
<td>Qte <input type="text" name="qte" size="3" value=""></td>
<td></td>
</tr>
</table>
</form>
Отже, бачимо, що форма надсилається до контролера за допомогою дії [achat.do].
Ця дія налаштована в [springwebarticles-servlet.xml] наступним чином:
<!-- маппінг додатка-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
...
<prop key="/achat.do">AchatController</prop>
...
</props>
</property>
</bean>
<bean id="AchatController"
class="istia.st.articles.web.spring.AchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
Код класу [AchatController] такий:
| package istia.st.articles.web.spring;
import istia.st.articles.dao.Article;
import istia.st.articles.domain.Achat;
import istia.st.articles.domain.Panier;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class AchatController implements Controller {
// конфігурація веб-додатку
Config config;
public void setConfig(Config config) {
this.config = config;
}
// обробка запиту
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// список помилок для цієї дії
ArrayList erreurs = new ArrayList();
// отримано кількість, що була придбана
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// неправильна кількість
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("infos");
}
// отримано сесію клієнта
HttpSession session = request.getSession();
// отримано товар, доданий до сесії
Article article = (Article) session.getAttribute("article");
// сесія закінчилася?
if (article == null) {
// відображається сторінка помилок
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// створюється нова покупка
Achat achat = new Achat(article, qté);
// додаємо товар до кошика клієнта
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
// повернення до списку товарів
return new ModelAndView("index");
}
}
|
Коментарі:
- нагадаємо форму бланка, що надсилається контролеру:
<form method="post" action="achat.do?id=3"/>
<table>
<tr>
<td><input type="submit" value="Acheter"></td>
<td>Qte <input type="text" name="qte" size="3" value=""></td>
<td></td>
</tr>
</table>
</form>
- У запиті є два параметри: [id] — номер придбаного товару, [qte] — кількість, що була придбана.
- Перевіряється наявність та правильність параметра [qte]. Якщо цей параметр визнано неправильним, користувачеві повертається вигляд [INFOS] разом із повідомленням про помилку:
| // список помилок щодо цієї дії
ArrayList erreurs = new ArrayList();
// отримано кількість, що була придбана
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// неправильна кількість
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("infos");
}
|
- придбаний товар отримується із сесії. Термін дії сесії міг закінчитися. У цьому випадку надсилається подання [ERREURS]:
| // отримано сесію клієнта
HttpSession session = request.getSession();
// отримано товар, доданий до сесії
Article article = (Article) session.getAttribute("article");
// сесія закінчилася?
if (article == null) {
// відображається сторінка помилок
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
|
- якщо сесія не закінчилася, товар додається до кошика, який також отримується з сесії:
| // створюється нова покупка
Achat achat = new Achat(article, qté);
// додаємо товар до кошика клієнта
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
|
- нарешті, надсилається вигляд [LISTE]:
// повернення до списку товарів
return new ModelAndView("index");
- Вище наведено відправлення сторінки [/vues/index.jsp]. Відомо, що вона вимагає від браузера клієнта перенаправлення на URL-адресу [/main.do]. Саме це перенаправлення відобразить список товарів.
3.7.7.5. panier.do
Ця дія призначена для відображення всіх покупок клієнта. Вона доступна через меню:

Код HTML, пов'язаний із наведеним вище посиланням, має такий вигляд:
<a href="panier.do">Voir le panier</a>
Сторінка, на яку веде це посилання, виглядає так:

Ця дія налаштована в [springwebarticles-servlet.xml] наступним чином:
| <!-- маппінг додатка-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
...
<prop key="/panier.do">VoirPanierController</prop>
...
</props>
</property>
</bean>
...
<bean id="VoirPanierController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
|
Код класу [VoirPanierController] є таким:
| package istia.st.articles.web.spring;
import istia.st.articles.domain.Panier;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class VoirPanierController implements Controller {
// конфігурація веб-додатку
Config config;
public void setConfig(Config config) {
this.config = config;
}
// обробка запиту
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// відображення кошика
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null || panier.getAchats().size() == 0) {
// кошик порожній
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("paniervide");
} else {
// у кошику є щось
request.setAttribute("actions", new Hashtable[] {
config.getHActionListe(), config.getHActionValidationPanier() });
return new ModelAndView("panier");
}
}
}
|
Коментарі:
- кошик береться з сесії, в якій він зазвичай знаходиться. Термін дії сесії міг закінчитися, і тоді кошика немає. Цей випадок не розглядається як помилка, а просто вважається, що кошик порожній.
- якщо кошик порожній, відображається сторінка [PANIERVIDE]
- в іншому випадку — вигляд [PANIER]
3.7.7.6. retirerachat.do
Ця дія призначена для видалення товару з кошика:

Якщо розглянути код HTML з посилання вище, то отримаємо таке:
<a href="retirerachat.do?id=3">Retirer</a>
Отже, дія [retirerachat.do] отримує як параметр ідентифікатор товару, який потрібно видалити з кошика. Ця дія налаштована в [springwebarticles-servlet.xml] наступним чином:
| <!-- маппінг додатка-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
...
<prop key="/retirerachat.do">RetirerAchatController</prop>
</props>
</property>
</bean>
...
<bean id="RetirerAchatController"
class="istia.st.articles.web.spring.RetirerAchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
|
Код класу [RetirerAchatController] такий:
| package istia.st.articles.web.spring;
import istia.st.articles.domain.Panier;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class RetirerAchatController implements Controller {
// конфігурація веб-додатку
Config config;
public void setConfig(Config config) {
this.config = config;
}
// обробка запиту
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// список помилок цієї дії
ArrayList erreurs = new ArrayList();
// отримання кошика
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// сесія закінчилася
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// отримано ідентифікатор товару, який потрібно видалити
String strId = request.getParameter("id");
// щось?
if (strId == null) {
// ненормально
erreurs.add("action incorrecte([retirerachat,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// перетворюємо strId на ціле число
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// ненормально
erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// видаляємо покупку
panier.enlever(id);
// знову відображаємо кошик
request.setAttribute("actions",
new Hashtable[] { config.getHActionListe() });
return new ModelAndView("redirpanier");
}
}
|
Коментарі:
- код перевіряє наявність та правильність параметра [id]. Якщо він виявляється неправильним, надсилається подання [ERREURS].
- в іншому випадку товар видаляється з кошика:
// видаляємо покупку
panier.enlever(id);
- після чого кошик відображається знову:
// повторно відображається кошик
request.setAttribute("actions",
new Hashtable[] { config.getHActionListe() });
return new ModelAndView("redirpanier");
Нагадаємо код подання [/vues/redirpanier.jsp]:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/panier.do"/>
Бачимо, що клієнт буде перенаправлений до дії [/panier.do]. Ця дія вже була описана. Вона відобразить вигляд [PANIER] або [PANIERVIDE] залежно від стану кошика.
3.7.7.7. validerpanier.do
Ця дія служить для підтвердження покупок, зроблених клієнтом. Конкретно це означає єдину дію: у базі даних запаси придбаних товарів зменшуються на кількість, що була придбана. Ця дія вибирається з наступного меню:

Код HTML для посилання [Valider le panier] є таким:
<a href="validerpanier.do">Valider le panier</a>
Коли це посилання активовано, запаси зменшуються, і список товарів знову відображається.
Ця дія налаштована в [springwebarticles-servlet.xml] наступним чином:
| <!-- маппінг додатка-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
...
<prop key="/validerpanier.do">ValiderPanierController</prop>
</props>
</property>
</bean>
...
<bean id="ValiderPanierController"
class="istia.st.articles.web.spring.ValiderPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
|
Код класу [ValiderPanierController] має такий вигляд:
| package istia.st.articles.web.spring;
import istia.st.articles.domain.Panier;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class ValiderPanierController implements Controller {
// конфігурація веб-додатку
Config config;
public void setConfig(Config config) {
this.config = config;
}
// обробка запиту
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// список помилок цієї дії
ArrayList erreurs = new ArrayList();
// покупець підтвердив свій кошик
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// сесія закінчилася
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// кошик підтверджено
try {
config.getArticlesDomain().acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// ненормальний стан
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// отримано можливі помилки
erreurs = config.getArticlesDomain().getErreurs();
if (erreurs.size() != 0) {
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe(), config.getHActionPanier() });
return new ModelAndView("erreurs");
}
// все виглядає OK — відображається список товарів
return new ModelAndView("index");
}
}
|
Коментарі:
- кошик отримується з сесії. Якщо термін дії сесії закінчився, надсилається екран [ERREURS]:
| // список помилок щодо цієї дії
ArrayList erreurs = new ArrayList();
// покупець підтвердив свій кошик
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// сесія закінчилася
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
|
- підтверджуємо покупки з кошика. Можуть виникнути помилки, якщо запасів недостатньо для виконання замовлення. У цьому випадку надсилається вигляд [ERREURS]:
| // підтверджується кошик
try {
config.getArticlesDomain().acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// ненормальний стан
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// отримано можливі помилки
erreurs = config.getArticlesDomain().getErreurs();
if (erreurs.size() != 0) {
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe(), config.getHActionPanier() });
return new ModelAndView("erreurs");
}
|
- якщо все пройшло успішно, знову відображається список товарів:
// все виглядає OK — відображається список товарів
return new ModelAndView("index");
Відомо, що представлення [/vues/index.jsp] перенаправляє клієнта до дії [/main.do]. Ця дія відобразить представлення [LISTE].