Skip to content

10. Web应用程序 MVC [personne] – 版本 5

10.1. 简介

在此版本中,我们进行了两项修改:

第一项修改涉及客户端向服务器指示所需操作的方式。 此前,该操作是通过客户端的 GET 或 POST 请求中名为 [action] 的参数来指定的。 在此,操作将由客户端请求的URL的最后一个元素指定,如下所示:

Image

在 [1] 中,表单提交的 URL 为 [/personne5/do/validationFormulaire]。正是 URL 的最后一个元素 [validationFormulaire] 使控制器能够识别应执行的操作。 在 [2] 中,由链接 [Retour au formulaire] 触发的 POST 操作是在 URL [/personne5/do/retourFormulaire] 上执行的。 同样,URL中的最后一个元素[retourFormulaire]向控制器指示了应执行的操作。

我们引入这一修改,是因为这是 StrutsSpring MVC 等最流行的 Web 开发框架所采用的方法。

应用程序中的所有 URL 都将采用 [/personne5/do/action] 的形式。应用程序 [/personne5] 中的 [web.xml] 文件将表明该应用程序接受 [/do/*] 形式的 URL:


    <servlet-mapping>
        <servlet-name>personne</servlet-name>
        <url-pattern>/do/*</url-pattern>
</servlet-mapping>

控制器将按以下方式获取要执行的操作名称:

        // 获取待执行的操作
String action=request.getPathInfo();

[request] 对象的 [getPathInfo] 方法返回请求 URL 的最后一个元素。

第二项修改涉及在两个请求/响应周期之间如何保存用户输入。目前,这些信息存储在会话中。如果用户数量众多且每个用户需要存储的数据量很大,这种方法可能会带来不便。实际上,每个用户都有自己的个人会话。 此外,除非特意提供注销选项,否则会话在用户离开后仍会保持活跃一段时间。因此,1000个各占100字节的会话将占用1MB内存。这仍属于中等需求,且很少有应用程序会同时保持1000个会话处于活动状态。

不过,除了会话之外,还有其他更节省内存的替代方案,了解这些方案很有必要。这里我们将使用Cookie的方法。让我们通过一个例子来说明。


步骤1:用户提交表单:


这一请求/响应循环会在客户端与服务器之间产生以下数据交换:

[1] : [demande du client]

POST /personne5/do/validationFormulaire HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.8.0.3) Gecko/20060426 Firefox/1.5.0.3
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: fr-fr,fr;q=0.8,en;q=0.6,en-us;q=0.4,de;q=0.2
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Referer: http://localhost:8080/personne5/do/formulaire
Cookie: JSESSIONID=6C6F4D112803A7E3696D41F5750CEDE7
Content-Type: application/x-www-form-urlencoded
Content-Length: 24

txtNom=pauline&txtAge=18

这是一个典型的 POST 请求。此处没有特别需要说明的地方,只是虽然我们不会使用会话,但 Web 服务器还是创建了一个会话。这可以从第 11 行浏览器发回给服务器的会话令牌中看出,该令牌是它之前从服务器接收到的。

[2]:[réponse du serveur]

1
2
3
4
5
6
7
HTTP/1.x 200 OK
Server: Apache-Coyote/1.1
Set-Cookie: nom=pauline
Set-Cookie: age=18
Content-Type: text/html;charset=ISO-8859-1
Content-Length: 547
Date: Mon, 22 May 2006 08:03:51 GMT

可以看到,第3行和第4行中,HTTP和[Set-Cookie]这两个头部字段已被发送至客户端浏览器,其中一个用于存储姓名(第3行),另一个用于存储年龄(第4行)。 这些 Cookie 的值即为上文 POST 和 [1] 第 14 行提交的值。


步骤 2:返回表单


Image

此请求/响应循环导致客户端与服务器之间进行以下 HTTP 数据交换:

[1]:[demande du client]

POST /personne5/do/retourFormulaire HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.8.0.3) Gecko/20060426 Firefox/1.5.0.3
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: fr-fr,fr;q=0.8,en;q=0.6,en-us;q=0.4,de;q=0.2
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Referer: http://localhost:8080/personne5/do/validationFormulaire
Cookie: nom=pauline; age=18; JSESSIONID=6C6F4D112803A7E3696D41F5750CEDE7
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

这里我们看到,点击链接 [Retour au formulaire] 触发了 POST。 第11行,我们可以看到浏览器通过Http头[Cookie]将收到的Cookie [nom, age, JSESSIONID]发回给服务器。这就是Cookie的工作原理:客户端将浏览器发送给它的Cookie发回给服务器。 在此示例中,控制器将接收 [pauline, 18] 值,并需将其填入 [txtNom, txtAge] 字段中,该字段位于 [formulaire] 视图中,该视图在 [2] 页面上显示。

[2] : [réponse du serveur]

1
2
3
4
5
HTTP/1.x 200 OK
Server: Apache-Coyote/1.1
Content-Type: text/html;charset=ISO-8859-1
Content-Length: 2341
Date: Mon, 22 May 2006 08:16:47 GMT

这里没有什么特别需要指出的,除了在该响应中,服务器没有发送任何cookie。这并不会阻止浏览器在下次交互时将从服务器接收到的所有cookie发回,即使这毫无意义。因此,我们以增加客户端/服务器交互中的字符流量为代价,减轻了服务器可用内存的压力。

10.2. Eclipse 项目

要创建Web应用程序[/personne5]的Eclipse项目[mvc-personne-05],需按照第6.2节所述的步骤复制项目[mvc-personne-04]。

10.3. Web 应用程序 [personne5] 的配置

应用程序 /personne5 的文件 web.xml 如下:


<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4"
    xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <display-name>mvc-personne-05</display-name>
    <!--  ServletPersonne -->
    <servlet>
        <servlet-name>personne</servlet-name>
        <servlet-class>
            istia.st.servlets.personne.ServletPersonne
        </servlet-class>
...
    </servlet>
    <!--  映射 ServletPersonne-->
    <servlet-mapping>
        <servlet-name>personne</servlet-name>
        <url-pattern>/do/*</url-pattern>
    </servlet-mapping>
    <!--  欢迎页面 -->
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

该文件与上一版本完全相同,仅存在以下细微差异:

  • 第 6 行:Web 应用程序的显示名称已更改为 [mvc-personne-05]
  • 第 18 行:应用程序处理的 URL 格式为 [/do/*]。此前仅处理 URL [/main]。现在,URL 的数量与待处理的操作数量相同。

首页 [index.jsp] 发生变化:


<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/do/formulaire"/>
  • 第 5 行:页面 [index.jsp] 将客户端重定向至 URL [/personne5/do/formulaire],这相当于要求控制器执行操作 [formulaire]。

10.4. 视图代码

[formulaire, réponse, erreurs]视图的变化不大。唯一的改动在于,待执行的操作不再像以前那样通过提交表单中名为[action]的隐藏字段来指定。 现在,该操作是在表单提交的目标 URL(c.a.d)中定义的,位于 <form> 标签的 [action] 属性中:

[formulaire.jsp]:


...
<html>
  <head>
    <title>Personne - formulaire</title>
    <script language="javascript">
...
    </script>
  </head>
  <body>
    <center>
      <h2>Personne - formulaire</h2>
      <hr>
      <form name="frmPersonne" action="validationFormulaire" method="post">
...
      </form>
    </center>
  </body>
</html>
  • [13] 行:表单的 [action] 参数在先前版本中消失了一段时间后,现已重新出现。要理解此处的属性值,需注意应用程序处理的所有 URL 均采用 [/do/action] 这种形式。 在 [13] 行中,[action] 属性的值为一个相对 URL(不以 / 开头)。 因此,浏览器会将其与当前显示页面的 URL 进行拼接,最终必然形成 [/do/action] 格式的 URL。 最后一个元素将被 <form> 标签中 [action] 属性的相对 URL 替换,从而使 [/do/validationFormulaire] 成为 POST 的目标。
  • 隐藏字段 [action] 已消失

[réponse.jsp]:


...

<html>
...
  <body>
      ...
    <form name="frmPersonne" action="retourFormulaire" method="post">
    </form>
    <a href="javascript:document.frmPersonne.submit();">
      ${lienRetourFormulaire}
    </a>
  </body>
</html>

  • 行 [7]:POST 的目标将变为 [/do/retourFormulaire]
  • 隐藏字段 [action] 在第 7-8 行表单中已消失。

[erreurs.jsp]:


...
<html>
...
  <body>
...
    <form name="frmPersonne" action="retourFormulaire" method="post">
    </form>
    <a href="javascript:document.frmPersonne.submit();">
      ${lienRetourFormulaire}
    </a>
  </body>
</html>

  • 第 [6] 行:POST 的目标将变为 [/do/retourFormulaire]
  • 隐藏字段 [action] 在第 6-7 行表单中已消失。

请读者按照前几个版本中的原则测试这些新视图。

10.5. 控制器 [ServletPersonne]

Web 应用程序 [/personne5] 中的控制器 [ServletPersonne] 将处理以下操作:

请求编号
请求
来源
处理
1
[GET /personne5/do/formulaire]
用户输入的URL
- 发送空的视图 [formulaire]
2
[POST
/personne5/do/validationFormulaire]
并附带参数 [txtNom, txtAge]
已发布
点击按钮
[Envoyer] 视图中的
[formulaire]
- 检查参数值 [txtNom, txtAge]
- 若参数值不正确,则发送视图 [erreurs(erreurs)]
- 若参数正确,则发送视图 [reponse(nom,age)]
3
[POST
/personne5/do/retourFormulaire]
不带任何参数
点击链接 [返回
表单] 链接
[réponse] 和 [erreurs]。
- 发送已预填入最新输入值的视图 [formulaire]

控制器 [ServletPersonne] 的框架与上一版本完全相同。我们将回顾对方法 [doValidationFormulaire, doRetourFormulaire, doGet] 所做的修改,方法 [init, doInit, doPost] 保持不变。

10.5.1. 方法 [doGet]

方法 [doGet] 获取待执行操作的方式与前几个版本不同:

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

        // 检查 Servlet 的初始化情况
        if (erreursInitialisation.size() != 0) {
...
        }
        // 获取请求的发送方法
        String méthode=request.getMethod().toLowerCase();
        // 获取待执行的操作
        String action=request.getPathInfo();
        // 操作?
        if(action==null){
            action="/formulaire";
        }
        // 执行操作
        if(méthode.equals("get") && action.equals("/formulaire")){
            // 启动应用程序
            doInit(request,response);
            return;
        }
        if(méthode.equals("post") && action.equals("/validationFormulaire")){
            // 验证输入表单
            doValidationFormulaire(request,response);
            return;
        }
        if(méthode.equals("post") && action.equals("/retourFormulaire")){
            // 返回输入表单
            doRetourFormulaire(request,response);
            return;
        }
        // 其他情况
        doInit(request,response);
    }
  • 第 12 行:获取待执行的操作。其格式为 [/action]。
  • 第 18-22 行:处理由请求 GET 调用的操作 [/formulaire]
  • 第23-27行:处理由请求POST调用的操作[/validationFormulaire]
  • 第 28-32 行:处理由请求 POST 调用的操作 [/retourFormulaire]

10.5.2. 方法 [doValidationFormulaire]

该方法处理第 2 个请求 [POST /personne5/do/validationFormulaire],其发送元素中包含 [txtNom, txtAge]。其代码如下:

// 表单验证
    void doValidationFormulaire(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException{
        // 获取参数
        String nom = request.getParameter("txtNom");
        String age = request.getParameter("txtAge");
        // 并将参数存储在 Cookie 中
        response.addCookie(new Cookie("nom",nom));
        response.addCookie(new Cookie("age",age));
        // 参数验证
        ...
    }

新功能:

  • 方法 [doValidationFormulaire] 返回其中一个视图 [réponse, erreurs]。 无论响应内容为何,控制器都会在其中设置两个 Cookie(第 8-9 行)。一个 Cookie 由 [Cookie] 对象表示,其构造函数接受两个参数:Cookie 的键及其关联的值。
  • 第 8 行:输入的姓名值被放入键名为 "nom" 的 Cookie 中
  • 第9行:年龄字段的输入值被存入键名为“age”的Cookie中
  • 在使用 [response.addCookie] 方法向客户端返回 HTTP 响应时,会添加一个 Cookie。此响应目前仅处于准备状态。只有当客户端收到的视图中的 JSP 页面被执行时,该响应才会真正发送出去。

10.5.3. 方法 [doRetourFormulaire]

该方法处理第 2 号请求 [POST /personne5/do/retourFormulaire],该请求不包含任何提交的数据。其代码如下:

        // 显示预填充表单
    void doRetourFormulaire(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
        // 获取用户的 Cookie
        Cookie[] cookies=request.getCookies();
        String nom=null;
        String age=null;
        int nbCookies=0;
        for(int i=0;i<cookies.length && nbCookies<2;i++){
            if(cookies[i].getName().equals("nom")){
                nom=cookies[i].getValue();
                nbCookies++;
            }else{
                if(cookies[i].getName().equals("age")){
                    age=cookies[i].getValue();
                    nbCookies++;
                }
            }
        }
        // 准备表单模板
        request.setAttribute("nom",nom);
        request.setAttribute("age",age);
        // 显示表单
        getServletContext().getRequestDispatcher((String)params.get("urlFormulaire")).forward(
                request, response);
        return;
    }

新功能:

方法 [doRetourFormulaire] 需显示一个预先填入最新输入内容的表单。在上一版本中,这些数据存储在会话中。而在本版本中,不再使用会话,而是使用 Cookie 来在两次客户端-服务器交互之间保存数据。 当客户端请求表单提交时,会根据具体情况收到 [réponse] 或 [erreurs] 视图作为响应,同时附带两个名为“nom”和“age”的 Cookie。 当点击这两个视图中的 [Retour au formulaire] 链接(该操作会在 URL [/do/retourFormulaire] 上触发 POST 页面)时,浏览器会将收到的两个 Cookie 发回给服务器。

  • 第4-18行:获取名为“name”和“age”的Cookie的值。颇为奇怪的是,目前尚无方法能根据Cookie的键值直接获取其值。因此,我们不得不遍历所有接收到的Cookie。
  • 完成上述操作后,将获取的两个值放入视图模板 [formulaire] 中(第 20-21 行),以便该视图将其显示出来。

10.6. 测试

将Eclipse项目[personne-mvc-05]集成到Tomcat后,启动或重启Tomcat,然后访问URL [http://localhost:8080/personne5]。