3. 第 2 条 - 三层 Web 架构示例
本文目标:
- 三层架构
- 基础 Web 架构 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年3月)。 IBExpert 支持通过图形界面管理 SGBD Firebird。
- Tomcat:http://jakarta.apache.org/tomcat/
- Eclipse 的 Tomcat 插件:http://www.sysdeo.com/eclipse/tomcatPlugin.html。另请参阅文档 https://tahe.developpez.com/java/eclipse/
理解本文需要具备若干先决条件。其中部分内容可参考我撰写的其他文档。在此情况下,我会予以引用。当然,这仅供参考,读者也可使用自己喜欢的文档。
- Java语言:[https://tahe.developpez.com/java/cours]
- Java Web 编程:[https://tahe.developpez.com/java/web/]
- 使用 Java、Eclipse 和 Tomcat 进行 Web 编程:[https://tahe.developpez.com/java/eclipse/]
- 使用Struts进行Web编程:[https://tahe.developpez.com/java/struts/]
- Spring 切面应用:IoC
- 标签库:JSTL(部分内容)
- Ibatis 文档 SqlMap:[https://prdownloads.sourceforge.net/ibatisnet/DevGuide.pdf?download]
- Firebird:[http://firebird.sourceforge.net/pdfmanual/Firebird-1.5-QuickStart.pdf](2005年3月)。
本文的灵感源于2004年夏天读到的一本书,这是Rod Johnson的一部杰作:《Wrox出版社出版的J2EE Development without EJB》。
3.1. webarticles应用程序
我们在此将介绍一个电子商务Web应用程序的部分内容。该应用程序将允许Web用户
- 浏览来自数据库的商品列表
- 将部分商品放入电子购物车
- 确认购物车。此确认操作仅会更新数据库中已购商品的库存。
向用户展示的视图包括:


- 视图 [PANIER],显示客户购物车中的商品

- 视图 [PANIERVIDE](用于客户购物车为空的情况)

- 视图 [ERREURS],用于报告应用程序中的任何错误

3.2. 应用程序的总体架构
我们希望构建一个具有以下三层结构的应用程序:
- 通过使用 Java 接口,使这三层相互独立
- 各层之间的集成由Spring实现
- 每层均对应独立的包:web(用户界面层)、domain(业务层)和dao(数据访问层)。
在此我们假设 [domain] 和 [dao] 这两层已建成。我们将仅关注 [web] 这一层,并计划通过多种方式构建它:
- 采用经典的Servlet控制器技术——JSP页面
- 使用 Struts 技术 MVC
- 使用 Spring 技术 MVC
无论采用哪种方式,该应用程序都将遵循 MVC 架构(模型-视图-控制器)。若参照上文的分层图,MVC 架构的整合方式如下:
处理客户端请求的流程如下:
- 客户端向控制器发送请求。该控制器是一个Servlet,负责处理所有客户端请求,是应用程序的入口点,即MVC中的“C”。
- 控制器处理该请求。为此,它可能需要业务层的协助,即在MVC架构中所谓的M模型。
- 控制器从业务层收到响应。客户端的请求已得到处理。这可能导致多种可能的响应。一个典型的例子是
- 如果请求无法正确处理,则显示错误页面
- 否则则返回确认页面
- 控制器选择要发送给客户端的响应(即视图)。该响应通常是一个包含动态元素的页面。控制器将这些元素提供给视图。
- 视图被发送给客户端。这就是MVC中的“V”。
3.3. 模型
这里我们研究的是 MVC 中的 M。该模型由以下元素组成:
- 业务类
- 数据访问类
- 数据库
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 应用程序的 [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]:使用工具 SqlMap 实现 [IArticlesDao] 接口的类 | 数据访问层——完全位于Web应用程序三层架构的[dao]层中 |
istia.st.articles.domain | - 包含 [istia.st.articles.domain] 包,该包本身包含以下组件: - [IArticlesDomain]:访问 [domain] 层的接口。这是 Web 层唯一可见的接口,它看不到其他接口。 - [AchatsArticles]:一个实现 [IArticlesDomain] 的类 - [Achat]:表示客户购买记录的类 - [Panier]:表示客户所有购买记录的类 | 代表网络购物模型- 完全位于Web应用程序三层架构的[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) {
// 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项信息
- 常被称为 getter/setter 的访问器,用于读取和写入这 5 项信息。 这些方法的命名遵循 JavaBean 标准。在 DAO 层中使用 JavaBean 对象与 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 配置文件来完成。 在此,我们计划使用名为 SqlMap 的开源产品来实现 IArticlesDao 接口。这将使我们能够从 Java 代码中移除所有 SQL 语句。
实现类 [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] 类需要一个配置文件,该文件的名称作为参数传递给类的构造函数。该配置文件定义了以下必要信息:
在本示例中,该文件名为 [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] 用于定义如何根据 SGBD 中的 [ARTICLES] 表中的一行数据,构建 [istia.st.articles.dao.Article] 类的实例。 它还定义了 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 :表行ARTI CLES - 类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)
| 返回由 [idArticle] 标识的 [Article] 对象 |
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 |
| 计算购物总金额 |
| 返回购物车的ID字符串 |
| 返回购物清单 |
软件包代码 [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] 层的测试
[dao] 层的测试类 JUnit 如下所示。通过阅读该类,可以了解 [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] 提供。上文中调用了名为 [articlesDao] 的 Spring Bean。该 Bean 在 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 的并发访问。 由于类 [istia.st.articles.dao.ArticlesDaoSqlMap] 中的方法 [changerStockArticle] 已进行同步,因此该测试通过。若移除同步,则测试将不再通过。负责更新库存的类如下:
| 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.3.6.2. [domain] 层的测试
[domain] 层的测试类 JUnit 如下:
| 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] 提供。上文中,我们请求了名为 [articlesDomain] 的 Spring Bean。该 Bean 在 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] 的一个实例。 该类有一个构造函数,其参数为类型为 [IArticlesDao] 的 [dao] 层访问对象。 在此,配置文件指定该对象即名为 [articlesDao] 的 Bean。这迫使 Spring 实例化该 Bean。关于 Bean [articlesDao] 的实例化已在前文说明。因此最终共实例化了两个 Bean:
- 型号为 [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");
|
此时获取到 Bean [articlesDomain]。在第二次调用 Spring 时:
| // 获取数据访问实例
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-domain.xml"))).getBean("articlesDao");
|
[Spring] 仅返回对 [articlesDao] 这个 Bean 的引用,该 Bean 已在之前的调用中创建。这就是单例模式的原理。如果向 Spring 请求一个 Bean,它会在该 Bean 不存在时进行实例化,否则返回对现有 Bean 的引用。
3.4. 三层Web应用程序MVC
接下来,我们将构建以下三层Web应用程序:
该应用程序将采用 MVC 架构。模型 M 已编写并经过测试,即前文所述的模型。它以三个压缩包的形式提供给我们 [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception]。我们需要编写控制器 C 和视图 V。
首先,我们考虑一种经典的方法,即:
- 控制器 C 由单个 Servlet 实现
- 视图 V 由 JSP 页面实现
3.5. 基于控制器Servlet和页面JSP的MVC架构
该应用程序的 MVC 架构如下:
| 业务类、数据访问类和数据库 |
| 页面JSP |
| 处理客户端请求的Servlet |
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] 的单一 Servlet 组成。它将处理来自客户端的各种请求。这些请求将通过客户端请求 HTTP 中包含参数 [action] 来体现:
| 含义 | 控制器操作 | 可能的响应 |
| 客户想要 商品列表 | - 向业务层请求商品列表 业务层 | - [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
- Spring 配置文件 [springConfigFileName] 的名称,该文件将用于创建访问业务层和 DAO 的单例对象
- 视图 [/vues/index.jsp],当客户端请求的 URL 为 /<context> 时将显示该视图,其中 <context> 是应用程序的上下文
3.5.6. 接口实现类的变更
遵循三层架构理念,各层之间应相互隔离。这种隔离通过以下方式实现:
- 各层之间通过接口而非具体类进行通信
- 某一层级的代码绝不会自行实例化另一层级的类以供使用。它仅需向外部工具(此处为 [Spring])请求所需使用层级接口的实现实例。 为此,我们知道它无需知道实现类的名称,只需知道其希望获取引用所对应的 Spring Bean 的名称即可。
在我们的应用程序中,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] Bean。Spring 随后将实例化一个 [istia.st.articles.domain.AchatsArticles] 类型的对象。 为了进行此实例化,它需要一个类型为 [articlesDao] 的 Bean,即一个类型为 [istia.st.articles.dao.ArticlesDaoSqlMap] 的对象。 Spring 随后将实例化该对象。此实例化操作将基于 [sqlmap-config-firebird.xml] 文件中的信息进行,该文件是 SqlMap 访问数据的配置文件。 操作完成后,请求了 Bean [articlesDomain] 的类 [UI] 便拥有了将其与 SGBD 数据连接起来的完整链:
3.5.7. 与 SGBD 或数据库相关的变更
Web 应用程序对 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 :表行ARTI CLES - 类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 Web应用程序就像一个由众多元素组成的拼图。采用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 是相对路径(没有 /),因此系统将在包含 <jsp:include> 标签的页面所在的同一文件夹中查找页面 [entete.jsp]。
代码:
| <%@ 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 对象 - 页面底部显示的消息 |
文章数组 HTML 中的每个链接 [Infos] 都有一个 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 对象 - 数量出错时显示的提示信息 |
| 字符串对象 - 在输入字段中显示的值[Qte] |
当数量输入出错时,将使用字段 [msg] 和 [qte]:

该页面包含一个表单,通过按钮 [Acheter] 提交。POST 的目标 URL 为 [?action=achat&id=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] 的对象——待显示的购物车 |
购物车购买列表 HTML 中的每个链接 [Retirer] 都有一个 URL,其格式为 [?action=retirerachat&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 请求后显示的。控制器请求的参数如下:
代码:
| <%@ 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. 控制器
接下来需要编写我们Web应用程序的核心——控制器。其作用在于:
- 获取客户端的请求,
- 利用业务类处理其请求的操作,
- 并返回相应的视图。
3.5.10.1. 控制器初始化
当控制器类被Servlet服务器加载时,其方法[init]会被执行。这仅会发生一次。一旦加载到内存中,控制器将驻留其中并处理来自不同客户端的请求。每个客户端都会有一个执行线程,因此控制器的方法将由不同的线程同时执行。 需要提醒的是,正因如此,控制器不应包含其方法可能修改的字段。其字段必须为只读。这些字段由方法 [init] 进行初始化,这也是该方法的主要作用。该方法的特殊之处在于,它仅由单个线程执行一次。 因此,在此方法中不会出现对控制器字段的并发访问问题。方法 [init] 的目的是初始化 Web 应用程序所需的对象,这些对象将作为只读资源由所有客户端线程共享。这些共享对象可放置在两个位置:
- 控制器的私有字段
- 应用程序的执行上下文(ServletContext)
应用程序 [webarticles] 中的方法 [init] 将执行以下操作:
- 检查文件 [web.xml] 中是否包含应用程序正常运行所需的参数。这些参数已在第 3.5.5 节中描述。
- 将私有字段 [ArrayList erreurs] 初始化为可能出现的错误列表。若无错误,该列表将为空,但该字段仍会存在。
- 若发生错误,[init]方法将在此终止。否则,它将创建一个[IArticlesDomain]类型的对象,该对象将成为控制器用于处理业务需求的业务对象。 如 3.5.6 节所述,控制器将向 Spring 框架请求所需的 Bean。此实例化操作可能引发各种错误。若发生此类情况,这些错误同样会被记录在控制器的 [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 | - 向 业务类 - 显示 | [INFOS] 或 [ERREURS] |
| POST /main?action=achat&id=ID - 购买数量属于提交的参数之一 | - 向 业务类中 - 将其加入购物车,并 客户会话中 | [LISTE] 或 [INFOS] 或 [ERREURS] |
| GET /main?action=退货&id=ID | - 从 购物车中的 客户会话 | [PANIER] |
| GET /main?action=panier | - 显示 客户会话 | [PANIER] 或 [PANIERVIDE] |
| GET /main?action=validationpanier | - 在数据库中减少 库存 库存 GET | [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() {
// 获取Servlet的初始化参数
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 {
// 检查Servlet初始化是否成功
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();
// 获取请求的ID
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;
}
// 请求键值 id 的条目
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"/>
因此,系统已重定向至网址 [http://localhost:8080/webarticles/main?action=liste],这从截图中的浏览器网址栏可见一斑。 因此请求了 URL [/main?action=liste]。在 [web.xml] 中,URL /main 关联到了 Servlet [webarticles]:
<servlet-mapping>
<servlet-name>webarticles</servlet-name>
<url-pattern>/main</url-pattern>
</servlet-mapping>
同样在 [web.xml] 中,Servlet [webarticles] 与 Servlet [ istia.st.articles.web.WebArticles] 相关联:
<servlet-name>webarticles</servlet-name>
<servlet-class>istia.st.articles.web.WebArticles</servlet-class>
因此,如果 [ istia.st.articles.web.WebArticles] Servlet 尚未被加载,Tomcat Servlet 容器将加载它,并执行其 [init] 方法:
| public void init() {
// 获取 Servlet 的初始化参数
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] 仅在 Servlet 初始加载时执行一次。随后,将根据客户端请求的类型 [GET, POST] 执行其中一个 [doGet, doPost] 方法。 在此,这两个方法的功能相同,相关代码已移至 [doGet] 中:
| public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// 检查Servlet初始化是否成功
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] 已将应用程序模型(领域层)的访问服务存储在 Servlet 的私有字段中:
// 私有字段
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());
}
|
| // 有错误吗?
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);
|
在本例中,一切顺利,我们成功获得了视图 [LISTE]。 建议读者重新阅读视图 [LISTE] 的代码,以确认该视图所需的动态参数确实已由控制器在上方提供。对于每个视图,都需要进行同样的验证:
- 查找视图的动态参数
- 确保控制器已将这些参数放入传递给视图的请求属性中
现在,我们仅列出应用程序用户所经历的屏幕路径。请读者每次都像之前那样进行类似的推理:
从文章列表中,用户可以选择一篇文章:
买家可以在这里购买第 3 号商品。让我们在数量上输入一个错误:
错误已报告。现在,让我们购买一些商品:
购买已记录,商品列表已重新显示。让我们查看一下购物车:
该商品已成功加入购物车。现在将其移出:
商品已从购物车中移除,购物车页面已刷新。此时购物车为空。
现在购买100件第3号商品和2件第4号商品:
由于库存仅有30件,而您想购买100件,因此无法购买商品编号3。该商品仍保留在购物车中:
而第4号商品已成功购买,其新库存量显示为39(40-1):
3.6. 基于Struts的MVC架构
3.6.1. 应用程序总体架构
让我们回顾一下该应用程序的 MVC 架构:
在之前的版本中:
- 控制器由一个 Servlet 负责
- 视图由 JSP 页面实现
- 模型由三个 .jar 文件组成
在 Struts 版本中:
- 控制器将由一个从 Struts 通用 Servlet [ActionServlet] 派生的 Servlet 负责
- 视图将由与之前几乎完全相同的 JSP 页面负责
- 模型将由相同的三个归档文件实现
我们将发现,将前一个应用程序迁移到 Struts 主要涉及以下任务:
- 原先在Servlet/控制器特定方法中处理的操作,现在由Struts中[Action]类的派生类实例来处理
- 编写配置文件 [web.xml] 和 [struts-config.xml]
- 对页面 JSP 进行一些修改
回顾 STRUTS 所使用的通用架构 MVC:
| 业务类、数据访问类和数据库 |
| 页面JSP |
| 处理客户端请求的Servlet、[Action]对象以及与表单关联的[ActionForm] Bean。 |
- 控制器是应用程序的核心。所有客户端请求都通过它处理。这是一个由 STRUTS 提供的通用 Servlet。 在某些情况下可能需要对其进行派生。对于简单的情况,则无需如此。该通用 Servlet 通常从一个名为 struts-config.xml 的文件中获取所需信息。
- 如果客户端请求包含表单参数,控制器会将其放入一个 Bean 对象中。随着时间推移创建的这些 Bean 对象将存储在会话或客户端请求中。此项可配置。如果已创建过,则无需重新创建。
- 在配置文件 struts-config.xml 中,对于每个需要通过程序处理的 URL(因此不对应于可直接请求的 JSP 视图),会关联以下信息:
- 负责处理该请求的 Action 类名称。同样,实例化的 Action 对象可保存在会话或请求中。
- 如果请求的 URL 带有参数(例如向控制器提交表单的情况),则会指定负责存储表单信息的 Bean 的名称。
- 凭借配置文件提供的这些信息,当控制器接收到客户端发来的 URL 请求时,便能确定是否需要创建 Bean 以及具体创建哪个 Bean。一旦实例化,该 Bean 即可验证其存储的表单数据是否有效。 控制器会自动调用该 Bean 中的 validate 方法。Bean 由开发人员构建,因此开发人员需在 validate 方法中编写验证表单数据有效性的代码。若数据被判定为无效,控制器将不再继续处理,而是将控制权移交给配置文件中指定的视图。至此,交互流程结束。 需要注意的是,开发者可以要求不验证表单的有效性。他同样在文件 struts-config.xml 中进行此设置。在这种情况下,控制器不会调用 Bean 的 validate 方法。
- 如果 Bean 数据正确,或者未进行验证,或者不存在 Bean,控制器将控制权移交给与 URL 关联的 Action 类型对象。 它通过调用该对象的 `execute` 方法来实现,并将可能已构建的 Bean 引用传递给该方法。此时开发者需要完成相应的工作:可能需要调用业务类或数据访问类。处理完成后,Action 对象将应返回给客户端的视图名称返回给控制器。
- 在配置文件中,控制器将找到与被要求显示的视图名称关联的 URL。随后它便发送该视图。与客户端的交互至此结束。
在我们的应用程序中,我们不会将 [Bean] 对象用作客户端与 [Action] 类之间的缓冲对象。 [Action] 对象将直接从其接收到的 [HttpServletRequest] 对象中获取客户端请求的参数。这有助于我们初始应用程序的移植。因此,我们应用程序的最终架构如下:
| 业务类、数据访问类和数据库 |
| 页面JSP |
| 处理客户端请求的Servlet、对象 [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-INF] 文件夹中的 [web.xml, struts-config.xml]:

文件 [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](欢迎文件)
- 格式为 *.do 的 URL 请求将被重定向至 [strutswebarticles] Servlet(servlet-mapping)
- Servlet [strutswebarticles] 是类 [ istia.st.articles.web.struts.MainServlet] 的一个实例(servlet-name, servlet-class)
- 该 Servlet 支持两个初始化参数
- 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>
|
该配置文件说明了什么?
| 用于显示文章列表 |
| 用于显示文章列表 |
| 用于显示特定商品的信息 |
| 购买指定商品 |
| 查看购物车 |
| 从购物车中移除商品 |
| 用于确认购物车 |
- 上述操作与上一版本中由Servlet处理的操作一一对应。针对每项操作,均提供了以下信息:
- 负责处理该操作的类名称
- 处理该操作后可能产生的响应(即视图)。控制器将从中选择其中一个。
- 应用程序的消息资源文件名称(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 是相对路径(没有 /),因此系统将在包含 <jsp:include> 标签的页面所在的同一文件夹中查找页面 [entete.jsp]。
代码:
| <%@ 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 对象 - 页面底部显示的消息 |
文章数组 HTML 中的每个链接 [Infos] 都有一个 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] 提交。POST 的目标 URL 为 [/achat.do?id=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] 的对象——待显示的购物车 |
购物车购买列表 HTML 中的每个链接 [Retirer] 都有一个 URL,其格式为 [retirerachat.do?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 请求后显示的。控制器请求的参数如下:
代码:
| <%@ 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] 的通用控制器。 众所周知,Servlet 提供了一个名为 [init] 的方法,用于在应用程序启动时进行初始化。如果我们使用 Struts 的通用控制器 [ActionServlet],则无法访问其 [init] 方法。 在此,我们需要在应用程序启动时执行一些操作,主要是实例化一个模型访问对象。因此我们需要一个 [init] 方法。于是,我们在以下 [MainServlet] 类中派生了 [Actionservlet] 类:
| 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();
// 获取Servlet的初始化参数
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] 会调用父类 [ActionServlet] 中的方法 [init]。该方法将负责解析 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>
|
所有以 .do 结尾的 URL 方法,都将由 [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 {
// 控制Servlet
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] 的代码
- 控制器实例中已存储了若干信息。可通过以下方式获取该实例的引用:
// 控制 Servlet
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());
}
|
| // 是否有错误?
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");
|
实际发送给客户的视图由 [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 {
// 控制 Servlet
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();
// 获取请求的ID
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");
}
// 请求键值对 id
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 中。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 {
// 控制Servlet
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);
|
// 返回商品列表
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] 访问:
与链接 [Voir le panier] 关联的代码 HTML 如下:
<a href="panier.do">Voir le panier</a>
在 [struts-config.xml] 中,操作 [panier.do] 的配置如下:
<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 {
// 控制Servlet
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] 操作后,购物车将重新显示(如上图右侧所示)。如果查看上图左侧链接 [Valider le panier] 对应的代码 HTML,内容如下:
<a href="retirerachat.do?id=3">Retirer</a>
因此,操作 [retirerachat.do] 接收的参数是需从购物车中移除的商品 ID。该操作在 [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 {
// 控制Servlet
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");
}
// 获取待移除商品的ID
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
此操作用于确认客户的购买。具体而言,这体现为一项操作:在数据库中,所购商品的库存数量将减少购买的数量。此操作来自以下菜单:
链接 [Valider le panier] 的代码 HTML 如下:
<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 {
// 控制Servlet
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. 基于Spring的MVC架构
3.7.1. 应用程序的总体架构
让我们回顾一下应用程序的 MVC 架构:
在第一个版本中:
- 控制器由一个 Servlet 负责
- 视图由 JSP 页面实现
- 模型由三个 .jar 文件组成
在 Struts 版本中:
- 控制器由一个从 Struts 通用 Servlet [ActionServlet] 派生的 Servlet 实现
- 视图由与 [Struts] 版本相同的 JSP 页面提供
- 模型由相同的三个归档文件实现
在 Spring 版本中:
- 控制器将由 Spring 提供的 Servlet [DispatcherServlet] 负责
- 视图将由与之前几乎完全相同的 JSP 页面提供
- 模型将由相同的三个归档文件提供
我们将发现,如果愿意不使用正统 Spring 架构(MVC)所推荐的所有组件,那么将我们的应用程序从 Struts 迁移到 Spring 其实很简单。主要变更如下:
- 在 Struts 中,原本由 Servlet/控制器中的特定方法处理,或由 [Action] 类派生类的实例处理的操作,现在由实现 Spring 接口 [Controller] 的类实例来处理
- 所需的配置文件如下:
- [web.xml],因为这是一个Web应用程序。该文件包含一个监听器,该监听器将在应用程序初始化时调用文件 [applicationContext.xml]
- [applicationContext.xml],从而创建应用程序所需的Bean,特别是用于访问模型的服务Bean
- JSP视图将与Struts中的视图相同。我们将需要创建一个新的视图。
回顾上一版本中使用的架构 MVC STRUTS:
| 业务类、数据访问类和数据库 |
| 页面JSP |
| 处理客户端请求的Servlet、对象 [Action] |
使用 Spring 时,我们采用相同的架构:
| 业务类、数据访问类和数据库 |
| 页面JSP |
| 处理客户端请求的Servlet,以及实现该接口的对象 [Controller] |
- 控制器是应用程序的核心。所有客户端请求都通过它处理。这是一个由 SPRING 提供的通用 Servlet。其类型为 [DispatcherServlet]。此后我们将该控制器称为 [Spring] 控制器。
- 控制器[Spring]将把客户端的请求定向到[Controller]类型的某个实例。 每个待处理的操作都会对应一个这样的实例。该实例在请求的 URL 中定义,与 Struts 的方式类似。因此,如果请求的 URL 是 [liste.do],则可知请求的操作是 [liste]
- 若 C 代表应用程序上下文,则控制器 [Spring] 将调用文件 [C-servlet.xml],该文件在 Struts 版本中起着配置文件 struts-config.xml 的作用。 对于应用程序需要处理的每个操作,都会关联一个负责处理该请求的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-INF] 文件夹中的 [web.xml, applicationContext.xml, springwebarticles-servlet.xml]:

文件 [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>
<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](欢迎文件)
- 格式为 *.do 的 URL 请求将被重定向至 [springwebarticles] Servlet(servlet-mapping)
- Servlet [springwebarticles] 是 Spring 提供的 [org.springframework.web.servlet.DispatcherServlet] 类的实例(servlet-name, servlet-class)
- 监听器 [org.springframework.web.context.ContextLoaderListener] 将在应用程序启动时被调用。其主要作用是实例化 [applicationContext.xml] 文件中定义的 Spring Bean
文件 [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>
<!-- Web 应用程序配置-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
</beans>
|
其中既有熟悉的内容,也有不太常见的部分。在应用程序初始化时,将实例化三个 Bean:
- 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>
|
该配置文件说明了什么?
| 用于显示文章列表 |
| 用于显示文章列表 |
| 用于显示特定商品的信息 |
| 购买指定商品 |
| 查看购物车 |
| 从购物车中移除商品 |
| 用于确认购物车 |
- 上述操作与之前版本中由控制器处理的操作一一对应。每项操作都明确了负责处理它的类名。以操作 [/panier.do] 为例:
- 它应由 Bean [VoirPanierController] 处理。该名称可自由定义,仅作为简单标识符。
<prop key="/panier.do">VoirPanierController</prop>
- 键 [VoirPanierController] 是同一配置文件中定义的 Bean 的名称:
<bean id="VoirPanierController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
- bean [VoirPanierController] 定义:
- 待实例化的类 [ istia.st.articles.web.spring.VoirPanierController],用于处理
- 如何实例化。此处,由 [applicationContext.xml] 定义并在应用程序启动时实例化的 Bean [config] 作为参数提供。此操作将适用于所有 [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控制器返回一个键,以指示其应显示哪个视图。 基于该键,可能存在多种策略来生成与该键关联的视图。所采用的策略是由 Bean [viewResolver] 定义的。在此,该 Bean 关联了类 [org.springframework.web.servlet.view.InternalResourceViewResolver],并带有不同的初始化参数。 简而言之,Bean [viewResolver]在此规定:若视图键为“XX”,则生成的视图为[/vues/XX.jsp]。发送给客户端的视图类型可通过多种方式进行更改:
- 更改 Bean [viewResolver] 的实现类
- 修改实现类的初始化参数
因此,只需通过以下方式即可将视图从 HTML 切换为 XML:更改 Bean [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]

发生了什么?
| <?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>
<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>
<!-- Web 应用程序配置-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
</beans>
|
- 上述 Bean 在应用程序上下文中被创建
- 随后加载了配置文件 [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] Bean 也被创建
- 现在一切就绪,可以处理客户端的请求了。该请求为:[http://localhost:8080/springwebarticles]。 此处请求的不是上下文中的 URL,而是上下文本身。因此,使用的是文件 [web.xml] 中的 [welcome-file] 部分。
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
<%@ 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] 交由 Bean [ListController] 处理。
- 客户端的请求被传递给 Bean [ListController] 中的方法 [handleRequest]。该方法执行其任务,并将要显示的视图键返回给控制器。在此处,如果一切正常,该键将是 [liste]。
- Spring控制器使用配置文件[springwebarticles-servlet.xml]中的Bean [viewResolver]来确定与该键关联的视图。在此情况下,该视图为[/vues/liste.jsp]
- 视图 [/vues/liste.jsp] 被发送给客户端
3.7.6. Spring 应用程序的初始化
我们提到,在应用程序启动时,[applicationContext.xml]文件中的Bean会被实例化:
| <?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>
<!-- Web 应用程序配置-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
</beans>
|
我们了解 [articlesDao, articlesDomain] 这个 Bean,但不了解 [config] 这个 Bean。后者由以下 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);
// 获取器-设置器
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;
}
// Web 应用程序初始化
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;
}
}
|
该类执行与 Web 应用程序 Servlet 中 [init] 方法相同的功能。它负责初始化应用程序。在此,初始化过程如下:
- 因为在 [applicationContext.xml] 中,Bean [config] 是这样定义的:
<!-- Web 应用程序的配置-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
在创建时,其私有字段 [articlesDomain] 已被初始化
- 随后,由于上述 Bean 的 [init-method="init"] 属性,与该 Bean 关联的类的 [init] 方法被执行。 在此,它初始化了三个字典 [hActionListe, hActionPanier, hActionValidationPanier],用于生成提供给用户的菜单中可能出现的三个链接。
- 创建了公共访问器,以便处理操作的 [Controller] 类型实例能够访问这些私有字段。
3.7.7. Spring 应用程序的 [Controller] 操作
3.7.7.1. 简介
每个 Spring 动作都会对应一个类型为 [Controller] 的类。在 Struts 版本中,每个动作对应一个类型为 [Action] 的类。编写 [Controller] 类通常包括:
- 复制粘贴 Struts 版本中曾使用的 [Action] 类
- 将代码调整为符合 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 {
// Web 应用配置
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]。当 [ListController] Bean 被实例化时,该字段由 Spring 初始化:
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
从上文可以看出,[ListController] 中的字段 [config] 被初始化为 Bean [config]。 这是什么?这是在 [applicationContext.xml]、c.a.d 中定义的 Bean [config],即上文所述的 [istia.st.articles.web.spring.Config] 的一个实例。
- 编写类 [Controller] 的代码,本质上就是编写其方法 [handleRequest] 的代码
- 向模型请求文章列表。可通过 [config.getArticlesDomain()] 访问该模型。若发生异常,则发送视图 [ERREURS]。 [handleRequest] 返回的结果必须是 [ModelAndView] 类型。可以通过多种方式实例化该类。在此处(以及后续所有情况),我们通过传入要显示的视图键来构建 [ModelAndView] 的实例。 需要提醒的是,根据 [viewResolver] Bean 的配置,请求显示键为 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");
}
|
| // 显示商品列表
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 {
// Web 应用程序配置
Config config;
public void setConfig(Config config) {
this.config = config;
}
// 处理请求
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// 错误列表
ArrayList erreurs = new ArrayList();
// 获取请求的ID
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");
}
// 请求键值对 id
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 中。该 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 {
// Web 应用配置
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);
|
// 返回商品列表
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 {
// Web 应用配置
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] 接收的参数是需从购物车中移除的商品 ID。该操作在 [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 {
// Web 应用配置
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");
}
// 获取待移除商品的ID
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
此操作用于确认客户的购买行为。具体而言,这体现为一项操作:在数据库中,所购商品的库存数量将减少购买的数量。此操作来自以下菜单:

链接 [Valider le panier] 的代码 HTML 如下:
<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 {
// Web应用配置
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]。