Skip to content

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用户

  • 浏览来自数据库的商品列表
  • 将部分商品放入电子购物车
  • 确认购物车。此确认操作仅会更新数据库中已购商品的库存。

向用户展示的视图包括:

  • 视图 [LISTE],展示待售商品列表

Image

  • 视图 [INFOS],提供产品的详细信息:

Image

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

Image

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

Image

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

Image

3.2. 应用程序的总体架构

我们希望构建一个具有以下三层结构的应用程序:

  • 通过使用 Java 接口,使这三层相互独立
  • 各层之间的集成由Spring实现
  • 每层均对应独立的包:web(用户界面层)、domain(业务层)和dao(数据访问层)。

在此我们假设 [domain] 和 [dao] 这两层已建成。我们将仅关注 [web] 这一层,并计划通过多种方式构建它:

  • 采用经典的Servlet控制器技术——JSP页面
  • 使用 Struts 技术 MVC
  • 使用 Spring 技术 MVC

无论采用哪种方式,该应用程序都将遵循 MVC 架构(模型-视图-控制器)。若参照上文的分层图,MVC 架构的整合方式如下:

处理客户端请求的流程如下:

  1. 客户端向控制器发送请求。该控制器是一个Servlet,负责处理所有客户端请求,是应用程序的入口点,即MVC中的“C”。
  2. 控制器处理该请求。为此,它可能需要业务层的协助,即在MVC架构中所谓的M模型。
  3. 控制器从业务层收到响应。客户端的请求已得到处理。这可能导致多种可能的响应。一个典型的例子是
    • 如果请求无法正确处理,则显示错误页面
    • 否则则返回确认页面
  4. 控制器选择要发送给客户端的响应(即视图)。该响应通常是一个包含动态元素的页面。控制器将这些元素提供给视图。
  5. 视图被发送给客户端。这就是MVC中的“V”。

3.3. 模型

这里我们研究的是 MVC 中的 M。该模型由以下元素组成:

  1. 业务类
  2. 数据访问类
  3. 数据库

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);
id
唯一标识商品的主键
nom
商品名称
prix
其价格
stockactuel
当前库存
stockminimum
库存低于该数值时需下达补货订单

在以下测试中,使用了数据库 [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 + "]";
  }
}

该类提供:

  1. 一个构造函数,用于设置商品的5项信息
  2. 常被称为 getter/setter 的访问器,用于读取和写入这 5 项信息。 这些方法的命名遵循 JavaBean 标准。在 DAO 层中使用 JavaBean 对象与 SGBD 的数据进行交互是一种常见做法。
  3. 对插入到条目中的数据进行验证。若数据有误,将抛出异常。
  4. 方法 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);
}

该接口中各方法的作用如下:

getAllArticles
将表 ARTICLES 中的所有条目转换为 [Article] 对象列表
clearAllArticles
清空表 ARTICLES
getArticleById
返回由主键标识的对象 [Article]
ajouteArticle
允许向表 ARTICLES 中添加一条记录
modifieArticle
允许修改表 [ARTICLES] 中的条目
supprimerArticle
允许从表 [ARTICLES] 中删除一条记录
changerStockArticle
允许修改表 [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] 类需要一个配置文件,该文件的名称作为参数传递给类的构造函数。该配置文件定义了以下必要信息:

  • 访问包含文章的 SGBD
  • 管理连接池
  • 管理事务

在本示例中,该文件名为 [sqlmap-config-firebird.xml],用于定义对 Firebird 数据库的访问:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
    PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-config-2.dtd">

<sqlMapConfig>
  <transactionManager type="JDBC">
      <dataSource type="SIMPLE">
            <property name="JDBC.Driver" value="org.firebirdsql.jdbc.FBDriver"/>
                <property name="JDBC.ConnectionURL"
                    value="jdbc:firebirdsql:localhost/3050:D:/data/Databases/firebird/dbarticles.gdb"/>
                <property name="JDBC.Username" value="sysdba"/>
                <property name="JDBC.Password" value="masterkey"/>
                <property name="JDBC.DefaultAutoCommit" value="true"/>
        </dataSource>
  </transactionManager>
  <sqlMap resource="articles.xml"/>
</sqlMapConfig>

上述提及的配置文件 [articles.xml] 用于定义如何根据 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+#移动#&gt;=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();
}
List getAllArticles()
返回要呈现给客户端的 [Article] 对象列表
Article getArticleById(int idArticle)
返回由 [idArticle] 标识的 [Article] 对象
void acheter(Panier panier)
通过将已购商品的库存减去购买数量来验证客户的购物车——如果库存不足,可能会失败
ArrayList getErreurs()
返回发生的错误列表——若无错误则为空

在此,接口 [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] 的四个方法。它有两个私有字段:

IArticlesDao articlesDao
由数据访问层提供的数据访问对象
ArrayList erreurs
可能出现的错误列表

要创建该类的实例,必须提供用于访问 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 的子类,具有以下字段和方法:

article
所购商品
qte
购买数量
double getTotal()
显示购买金额
String toString()
对象标识符

类 [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 的派生类,具有以下字段和方法:

achats
客户采购清单 - 类型为 [Achat] 的对象列表
void ajouter(Achat unAchat)
将一项购买添加到购买清单中
void enlever(int idArticle)
从商品列表中删除该商品 idArticle
double getTotal()
计算购物总金额
String toString()
返回购物车的ID字符串
ArrayList getAchats()
返回购物清单

软件包代码 [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 中使用以下配置进行了测试:

Image

注释:

  • 在 [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.xmlarticles.xml
    • 工具 [spring] 的配置文件:spring-config-test-dao.xmlspring-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 保存了一个待测类的实例:
1
2
3
4
5
6
7
8
     // 被测试类的实例
    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号商品的库存减少1

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 的首次调用触发的:

1
2
3
4
         // 获取领域访问实例
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");

此时获取到 Bean [articlesDomain]。在第二次调用 Spring 时:

1
2
3
         // 获取数据访问实例
        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 架构如下:

M=modèle
业务类、数据访问类和数据库
V=vues
页面JSP
C=contrôleur
处理客户端请求的Servlet

3.5.1. 模型

此前已作介绍。它由 Java 归档文件 [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception] 构成。

3.5.2. 视图

视图与文档开头介绍的视图一致:

LISTE
liste.jsp
这些视图被收集在应用程序的 [vues] 文件夹中
INFOS
infos.jsp
PANIER
panier.jsp
PANIERVIDE
paniervide.jsp
ERREURS
erreurs.jsp

3.5.3. 控制器

控制器将由一个名为 [WebArticles] 的单一 Servlet 组成。它将处理来自客户端的各种请求。这些请求将通过客户端请求 HTTP 中包含参数 [action] 来体现:

requête
含义
控制器操作
可能的响应
action=liste
客户想要
商品列表
- 向业务层请求商品列表
业务层
- [LISTE]
- [ERREURS]
action=infos
客户要求
关于视图中显示的某件商品的信息
[LISTE]
- 向业务层请求该商品
- [INFOS]
- [ERREURS]
action=achat
客户购买商品
- 向业务层请求商品,并
将其加入客户购物车
- [INFOS](若数量出错)
- [LISTE] 若无数量错误
action=retirerachat
客户希望删除
商品
- 从会话中获取购物车并进行修改
- [PANIER]
- [PANIERVIDE]
- [ERREURS]
action=panier
客户希望查看其
购物车
- 从会话中获取购物车
- [PANIER]
- [PANIERVIDE]
- [ERREURS]
action=validationpanier
客户已完成购物
并进入支付阶段
- 在数据库中更新
已购商品的库存
- 从客户的购物车中移除
已确认购买的商品
- [LISTE]
- [ERREURS]

3.5.4. 应用程序配置

我们将努力配置应用程序,使其在面对以下变更时尽可能灵活:

  1. 不同视图的 URL 变更
  2. 实现 [IArticlesDao] 和 [IArticlesDomain] 接口的类发生变更
  3. 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 的配置文件来保证。共有两个配置文件:

  1. [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。

  1. 文件 [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+#移动#&gt;=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
les vues

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 请求后显示。控制器请求的参数如下:

actions
Hashtable[] 对象 - 菜单选项数组
listarticles
ArrayList 类型对象 [Article]
message
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

此视图显示商品信息,并支持购买:

Image

当购买数量有误时,该视图会在执行 /main?action=infos&id=ID/main?action=achat&id=ID 请求后显示。控制器请求的参数如下:

actions
对象 Hashtable[] - 菜单选项数组
article
类型为 [Article] 的对象 - 要显示的商品
msg
String 对象 - 数量出错时显示的提示信息
qte
字符串对象 - 在输入字段中显示的值[Qte]

当数量输入出错时,将使用字段 [msg] 和 [qte]:

Image

该页面包含一个表单,通过按钮 [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

此视图显示购物车内容:

Image

该视图在执行 /main?action=panier/main?action=retirerachat&id=ID 请求后显示。控制器请求的参数如下:

actions
Hashtable[] 对象 - 菜单选项数组
panier
类型为 [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

此视图显示购物车为空的信息:

Image

该页面是在执行 /main?action=panier/main?action=retirerachat&id=ID 请求后显示的。控制器请求的参数如下:

actions
Hashtable[] 对象 - 菜单选项数组

代码

1
2
3
4
5
6
7
8
9
<%@ 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

出现错误时显示此视图:

Image

除由视图 [INFOS] 处理的数量错误购买操作外,该视图将在任何导致错误的请求后显示。控制器请求的元素如下:

actions
Hashtable[] 对象 - 菜单选项数组
erreurs
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] 仅将客户端重定向至应用程序的入口点:

1
2
3
4
<%@ 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] 的处理程序的签名如下:
1
2
3
4
5
6
7
8
/**
   * @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. 处理各种操作

处理应用程序中各种可能操作的方法如下:

方法
请求
处理
可能的响应
doListe
GET /main?action=liste
- 查询业务类相关文章列表
业务类
- 显示列表
[LISTE] 或 [ERREURS]
doInfos
GET /main?action=infos&id=ID
- 向
业务类
- 显示
[INFOS] 或 [ERREURS]
doAchat
POST /main?action=achat&id=ID
- 购买数量属于提交的参数之一
- 向
业务类中
- 将其加入购物车,并
客户会话中
[LISTE] 或 [INFOS]
或 [ERREURS]
doRetirerAchat
GET /main?action=退货&id=ID
- 从
购物车中的
客户会话
[PANIER]
doPanier
GET /main?action=panier
- 显示
客户会话
[PANIER] 或 [PANIERVIDE]
doValidationPanier
GET /main?action=validationpanier
- 在数据库中减少
库存
库存
GET
[LISTE] 或 [ERREURS]

3.5.10.4. 代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
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. 应用程序测试

下面展示一些测试截图。首先是应用程序的首页:

Image

实际请求的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());
        }
  • 若发生错误,将返回视图 [ERREURS]:
         // 有错误吗?
        if (erreurs.size() != 0) {
             // 显示错误页面
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // 结束
            return;
        }
  • 否则将返回视图 [LISTE]:
1
2
3
4
5
6
7
         // 显示商品列表
        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:

M=modèle
业务类、数据访问类和数据库
V=vues
页面JSP
C=contrôleur
处理客户端请求的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] 对象中获取客户端请求的参数。这有助于我们初始应用程序的移植。因此,我们应用程序的最终架构如下:

M=modèle
业务类、数据访问类和数据库
V=vues
页面JSP
C=contrôleur
处理客户端请求的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 项目的总体架构如下:

Image

3.6.3.2. 数据访问配置

鉴于数据访问接口保持不变,相关配置文件与上一版本相同。这些配置文件定义在 [WEB-INF/src] 中:

Image

在上图的屏幕截图中,[articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties]文件属于上一版本。

3.6.3.3. 归档目录

在 [WEB-INF/lib] 中,包含与上一版本相同的归档文件,以及 Struts 所需的文件:

Image

3.6.3.4. 应用程序配置

该应用程序通过两个文件进行配置:位于 [WEB-INF] 文件夹中的 [web.xml, struts-config.xml]:

Image

文件 [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>

该配置文件说明了什么?

  • 说明我们的控制器将处理以下 URL:
main.do
用于显示文章列表
liste.do
用于显示文章列表
infos.do
用于显示特定商品的信息
achat.do
购买指定商品
panier.do
查看购物车
retirerachat.do
从购物车中移除商品
validerpanier.do
用于确认购物车
  • 上述操作与上一版本中由Servlet处理的操作一一对应。针对每项操作,均提供了以下信息:
  • 负责处理该操作的类名称
  • 处理该操作后可能产生的响应(即视图)。控制器将从中选择其中一个。
  • 应用程序的消息资源文件名称(message-resources)。 此处该文件虽存在但为空,不会被使用。它应放置在应用程序的 [ClassPath] 中。此处将放置在 [WEB-INF/classes] 中。在 Eclipse 中,将其放置在 [WEB-INF/src] 中可获得以下结果:

Image

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 请求后显示的。控制器请求的元素如下:

actions
对象 Hashtable[] - 菜单选项数组
listarticles
ArrayList 对象(类型为 [Article])
message
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

此视图显示商品信息,并支持购买:

Image

当购买数量有误时,该视图会在执行 /infos.do?id=ID/achat.do?id=ID 请求后显示。 控制器请求的组成部分如下:

actions
对象 Hashtable[] - 菜单选项数组
article
类型为 [Article] 的对象 - 待显示的商品
msg
String 对象 - 数量出错时显示的提示信息
qte
对象 String - 在输入字段 [Qte] 中显示的值

当数量输入出错时,将使用字段 [msg] 和 [qte]:

Image

该页面包含一个表单,通过按钮 [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

此视图显示购物车内容:

Image

该视图是在执行 /panier.do/retirerachat.do?id=ID 请求后显示的。控制器请求的参数如下:

actions
对象 Hashtable[] - 菜单选项数组
panier
类型为 [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

此视图显示购物车为空的信息:

Image

该页面是在执行 /panier.do/retirerachat.do?id=ID 请求后显示的。控制器请求的参数如下:

actions
对象 Hashtable[] - 菜单选项数组

代码

1
2
3
4
5
6
7
8
9
<%@ 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

出现错误时显示此视图:

Image

该视图在任何导致错误的请求后显示,但由视图 [INFOS] 处理的数量错误的购买操作除外。控制器请求的元素如下:

actions
Hashtable[] 对象 - 菜单选项数组
erreurs
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] 中通过以下方式定义:

1
2
3
4
5
6
7
8
        <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] 操作时,将得到以下结果:

Image

类 [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] 发送给客户端:
1
2
3
4
5
6
7
8
     // 初始化错误?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // 显示错误页面
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }

实际发送给客户端的视图由 [struts-config.xml] 提供:

        <action path="/main" type="istia.st.articles.web.struts.ListeArticlesAction">
            <forward name="afficherListeArticles" path="/vues/liste.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>

这是视图 [/vues/erreurs.jsp]。请读者核对该视图的预期内容。这些信息由操作通过 [request] 对象作为属性提供。

  • 同样借助控制器,该操作可以获取应用程序模型(领域层)的访问对象:
     // 域访问对象
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
  • 完成上述操作后,即可查询商品列表:
     // 错误列表
    ArrayList erreurs = new ArrayList();
     // 请求商品列表
    List articles = null;
    try {
      articles = articlesDomain.getAllArticles();
    } catch (UncheckedAccessArticlesException ex) {
       // 记录错误
      erreurs.add("Erreur lors de l'obtention de tous les articles : "
          + ex.toString());
    }
  • 如果发生错误,将返回视图 [ERREURS]:
1
2
3
4
5
6
7
8
     // 是否有错误?
    if (erreurs.size() != 0) {
       // 显示错误页面
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
  • 否则将发送视图 [LISTE]:
1
2
3
4
5
6
     // 显示文章列表
    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> 标签的定义如下:

1
2
3
4
5
6
7
8
9
        <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] 的开头与之前研究过的那些方法完全相同。
  • 回顾一下发送给控制器表单的格式:
1
2
3
4
5
6
7
8
9
        <form method="post" action="achat.do?id=3"/>
            <table>
                <tr>
                    <td><input type="submit" value="Acheter"></td>
                    <td>Qte <input type="text" name="qte" size="3" value=""></td>
                    <td></td>
                </tr>
            </table>
        </form>
  • 请求中包含两个参数:[id]:所购商品编号,[qte]:购买数量。
  • 系统会检测参数 [qte] 是否存在且有效。如果该参数被识别为错误,则向用户返回视图 [INFOS] 并附带一条错误消息:
     // 显示此操作的错误列表
    ArrayList erreurs = new ArrayList();
     // 获取购买数量
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // 数量错误
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherInfosArticle");
    }
  • 已从会话中检索到所购商品。该会话可能已过期。在此情况下,将返回视图 [ERREURS]:
     // 获取客户会话
    HttpSession session = request.getSession();
     // 获取已加入购物车的商品
    Article article = (Article) session.getAttribute("article");
     // 会话已过期?
    if(article==null){
       // 显示错误页面
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
  • 如果会话未过期,商品将被放入购物车,该购物车同样从会话中获取:
     // 获取客户端会话
    HttpSession session = request.getSession();
     // 获取会话中的文章
    Article article = (Article) session.getAttribute("article");
     // 会话已过期?
    if(article==null){
       // 显示错误页面
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
     // 创建新订单
    Achat achat = new Achat(article, qté);
     // 将商品加入客户购物车
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
  • 最后,发送视图 [LISTE]:
     // 返回商品列表
    return mapping.findForward("afficherListeArticles");
  • 实际发送给客户端的视图由 [struts-config.xml] 提供:
        <action 
            path="/achat" type="istia.st.articles.web.struts.AchatArticleAction">
            <forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
            <forward name="afficherListeArticles" path="/main.do"/>
        </action>

这是视图 [/main.do]。该视图并非真正的视图,而是一项操作。因此,之前描述的操作 [/main.do] 将被执行,并显示商品列表。

3.6.6.5. panier.do

此操作用于显示客户的所有购买记录。可通过菜单选项 [Voir le panier] 访问:

与链接 [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:

M=modèle
业务类、数据访问类和数据库
V=vues
页面JSP
C=contrôleur
处理客户端请求的Servlet、对象 [Action]

使用 Spring 时,我们采用相同的架构:

M=modèle
业务类、数据访问类和数据库
V=vues
页面JSP
C=contrôleur
处理客户端请求的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 项目的总体架构如下:

Image

3.7.3.2. 数据访问配置

鉴于数据访问接口保持不变,相关配置文件与上一版本相同。这些配置文件定义在 [WEB-INF/src] 中:

Image

在上图的屏幕截图中,[articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties]文件属于旧版本。

3.7.3.3. 归档目录

在 [WEB-INF/lib] 中,包含与上一版本相同的存档文件,但不再需要的 Struts 相关文件除外:

Image

3.7.3.4. 应用程序配置

该应用程序通过三个文件进行配置:位于 [WEB-INF] 文件夹中的 [web.xml, applicationContext.xml, springwebarticles-servlet.xml]:

Image

文件 [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>

该配置文件说明了什么?

  • 说明我们的控制器将处理以下 URL:
main.do
用于显示文章列表
liste.do
用于显示文章列表
infos.do
用于显示特定商品的信息
achat.do
购买指定商品
panier.do
查看购物车
retirerachat.do
从购物车中移除商品
validerpanier.do
用于确认购物车
  • 上述操作与之前版本中由控制器处理的操作一一对应。每项操作都明确了负责处理它的类名。以操作 [/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] 中即可获得此结果:

Image

3.7.4. 视图 JSP

将使用的 JSP 视图即为 Struts 所使用的视图。所有视图均未修改:

Image

仅创建了一个新的视图: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] 包中:

Image

让我们通过一个示例回顾 Spring 应用的运行机制:

  • 用户请求 URL [http://localhost:8080/springwebarticles/main.do]

Image

发生了什么?

  • 应用程序中的文件 [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>
  • 如果这是对该应用程序的首次请求,则会触发以下操作:
    • 加载了监听器 [ 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>
  • 视图 [index.jsp] 如下所示:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/main.do"/>
  • 因此,客户端被引导重定向至 URL [http://localhost:8080/springwebarticles/main.do]。客户端执行了该操作。
  • Spring控制器因此收到一个新请求。它调用其文件[springwebarticles-servlet.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- 操作处理程序 = 控制器 -->
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
....
    </bean>

     <!-- 应用程序映射-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/liste.do">ListController</prop>
                <prop key="/main.do">ListController</prop>
                <prop key="/infos.do">InfosController</prop>
                <prop key="/achat.do">AchatController</prop>
                <prop key="/panier.do">VoirPanierController</prop>
                <prop key="/retirerachat.do">RetirerAchatController</prop>
                <prop key="/validerpanier.do">ValiderPanierController</prop>
            </props>
        </property>
    </bean>

</beans>
  • 该文件指示其将操作 [/main.do] 交由 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],我们稍后将详细说明。当其中一个操作在浏览器中执行时,会得到以下结果:

Image

类 [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");
        }
  • 如果没有错误,则发送视图 [LISTE]:
1
2
3
4
5
6
7
         // 显示商品列表
        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] 所显示的商品:

Image

如果查看该视图的代码 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");
    }
  • 如果会话未过期,商品将被放入购物车,该购物车同样从会话中获取:
1
2
3
4
5
6
7
8
9
     // 创建新订单
    Achat achat = new Achat(article, qté);
     // 将商品添加至客户购物车
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
  • 最后,发送视图 [LISTE]:
     // 返回商品列表
    return new ModelAndView("index");
  • 如上所述,我们发送视图 [/vues/index.jsp]。我们知道该视图会要求客户端浏览器重定向至 URL [/main.do]。正是通过此重定向,商品列表才会显示出来。

3.7.7.5. panier.do

此操作用于显示客户的所有购买记录。可通过以下菜单访问:

Image

与上述链接关联的代码 HTML 如下:

<a href="panier.do">Voir le panier</a>

该链接返回的页面如下:

Image

该操作在 [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

此操作用于从购物车中移除商品:

Image

如果查看上述链接中的代码 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

此操作用于确认客户的购买行为。具体而言,这体现为一项操作:在数据库中,所购商品的库存数量将减少购买的数量。此操作来自以下菜单:

Image

链接 [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]。