Skip to content

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

10.1. 简介

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

第一项改动涉及客户端如何向服务器指示其希望执行的操作。此前,这通常通过客户端 GET 或 POST 请求中的 [action] 参数来指定。在此版本中,操作将由客户端请求的 URL 的最后一个元素指定,如下所示:

Image

1 中,表单提交的目标 URL 是 [/person5/do/validateForm]。正是 URL 的最后一个元素 [validateForm] 使控制器能够识别应执行的操作。 在 2 中,由 [返回表单] 链接触发的 POST 请求被发送到了 URL [/person5/do/returnForm]。同样,URL 的最后一个元素 [returnForm] 告知控制器应执行哪个操作。

我们引入这一变更,是因为这是 StrutsSpring MVC 等最广泛使用的 Web 开发框架所采用的方法。

应用程序中的所有 URL 都将采用 [/person5/do/action] 的格式。[/person5] 应用程序的 [web.xml] 文件将指定其接受 [/do/*] 格式的 URL:


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

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

        // on récupère l'action à exécuter
String action=request.getPathInfo();

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

第二个改动涉及用户输入在请求/响应周期之间的存储方式。目前,这些信息存储在会话中。如果用户众多且每个用户需要存储的大量数据,这种方法可能会存在弊端。 事实上,每个用户都有自己独立的会话。此外,除非提供了注销选项,否则会话在用户注销后仍会保持活跃一段时间。因此,1,000 个各占 100 字节的会话将占用 1 MB 的内存。这仍属于中等需求,且很少有应用程序会同时保持 1,000 个活跃会话。

尽管如此,仍有比会话更节省内存的替代方案,了解这些方案很有必要。在此,我们将采用 Cookie 方法。让我们通过一个示例来说明这一点。


步骤 1:用户提交表单:


此请求/响应循环导致客户端与服务器之间发生以下 HTTP 交互:

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 行浏览器发回给服务器的会话令牌中可以明显看出,该令牌是它之前从服务器接收到的。

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 行,向客户端浏览器发送了 [Set-Cookie] HTTP 头,一个用于名称(第 3 行),一个用于年龄(第 4 行)。这些 Cookie 的值即为上文 POST 1 请求第 14 行中提交的值。


步骤 2:返回表单


Image

这一请求/响应循环导致客户端与服务器之间发生以下 HTTP 交互:

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

这里我们看到点击 [返回表单] 链接触发的 POST 请求。在第 11 行,我们可以看到浏览器使用 HTTP [Cookie] 头将收到的 Cookie [name, age, JSESSIONID] 发回给服务器。这就是 Cookie 的工作原理。客户端将服务器发送给它的 Cookie 发回给服务器。 在此示例中,控制器将接收 [pauline, 18] 这些值,并必须将其填入 2 中显示的 [form] 视图的 [txtName, txtAge] 字段中。

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. 配置 [personne5] Web 应用程序

/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>
    <!--  Mapping ServletPersonne-->
    <servlet-mapping>
        <servlet-name>personne</servlet-name>
        <url-pattern>/do/*</url-pattern>
    </servlet-mapping>
    <!--  welcome files -->
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>
 

除了一些细节外,此文件与上一版本中的文件完全相同:

  • 第 6 行:Web 应用程序的显示名称已更改为 [mvc-personne-05]
  • 第 18 行:应用程序处理的 URL 格式为 [/do/*]。此前仅处理 [/main] 这一 URL。现在,可处理的 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 [/person5/do/form],这相当于要求控制器执行 [form] 操作。

10.4. 视图代码

视图 [表单、响应、错误] 的变化非常小。唯一的改变是,待执行的操作不再像以前那样通过提交表单中名为 [action] 的隐藏字段来指定。现在,它是在提交表单的目标 URL 中定义的,即在 <form> 标签的 [action] 属性中:

[form.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,该 URL 必然采用 [/do/action] 这种形式。最后一个元素将被 <form> 标签中 [action] 属性的相对 URL 替换,从而生成 [/do/validationFormulaire] 作为 POST 目标。
  • 隐藏的 [action] 字段已消失

[response.jsp]:


...
 
<html>
...
  <body>
      ...
    <form name="frmPersonne" action="retourFormulaire" method="post">
    </form>
    <a href="javascript:document.frmPersonne.submit();">
      ${lienRetourFormulaire}
    </a>
  </body>
</html>
 
  • 第 [7] 行:POST 请求的目标将是 [/do/returnForm]
  • 第 7–8 行中,表单中的隐藏字段 [action] 已被移除。

[errors.jsp]:


...
<html>
...
  <body>
...
    <form name="frmPersonne" action="retourFormulaire" method="post">
    </form>
    <a href="javascript:document.frmPersonne.submit();">
      ${lienRetourFormulaire}
    </a>
  </body>
</html>
 
  • 第 [6] 行:POST 目标将为 [/do/returnForm]
  • 第 6–7 行中,表单中的隐藏字段 [action] 已被移除。

欢迎读者参照前几个版本中的方法,测试这些新视图。

10.5. [ServletPersonne] 控制器

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

否。
请求
来源
处理
1
[GET /person5/do/form]
用户输入的 URL
- 发送空的 [form] 视图
2
[POST
/person5/do/formValidation]
并携带参数 [txtName, txtAge]
已提交
点击
[提交] 按钮
[表单]
- 检查参数 [txtName, txtAge] 的值
- 若值不正确,则发送 [errors(errors)] 视图
- 若值正确,则返回 [response(name,age)] 视图
3
[POST
/person5/do/returnForm]
不带提交参数
点击 [返回
表单]
[响应] 中,并查看 [错误]。
- 发送已预先填入最新输入值的 [表单] 视图

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

10.5.1. [doGet] 方法

[doGet] 方法检索待执行操作的方式与以前的版本不同:

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

        // check how the servlet was initialized
        if (erreursInitialisation.size() != 0) {
...
        }
        // retrieve the request sending method
        String méthode=request.getMethod().toLowerCase();
        // retrieve the action to be executed
        String action=request.getPathInfo();
        // action?
        if(action==null){
            action="/formulaire";
        }
        // execution action
        if(méthode.equals("get") && action.equals("/formulaire")){
            // start application
            doInit(request,response);
            return;
        }
        if(méthode.equals("post") && action.equals("/validationFormulaire")){
            // validation of input form
            doValidationFormulaire(request,response);
            return;
        }
        if(méthode.equals("post") && action.equals("/retourFormulaire")){
            // back to input form
            doRetourFormulaire(request,response);
            return;
        }
        // other cases
        doInit(request,response);
    }
  • 第 12 行:获取待执行的操作。其形式为 [/action]。
  • 第 18–22 行:处理由 GET 请求发起的 [/form] 操作
  • 第 23–27 行:处理由 POST 请求发起的 [/validateForm] 操作
  • 第 28–32 行:处理由 POST 请求发起的 [/returnForm] 操作

10.5.2. [doValidationFormulaire] 方法

该方法处理请求 #2 [POST /person5/do/validationFormulaire],其中提交的元素包含 [txtName, txtAge]。其代码如下:

// form validation
    void doValidationFormulaire(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException{
        // parameters are retrieved
        String nom = request.getParameter("txtNom");
        String age = request.getParameter("txtAge");
        // stored in a cookie
        response.addCookie(new Cookie("nom",nom));
        response.addCookie(new Cookie("age",age));
        // parameter verification
        ...
    }

新增内容:

  • [doValidationFormulaire] 方法返回视图 [response, errors] 中的一个。无论返回何种响应,控制器都会在其内部设置两个 Cookie(第 8–9 行)。Cookie 由 [Cookie] 对象表示,其构造函数接受两个参数:Cookie 键及其关联的值。
  • 第 8 行:名称字段输入的值被存入键名为 "name" 的 Cookie 中
  • 第 9 行:年龄字段的输入值被放入键名为 "age" 的 Cookie 中
  • 通过 [response.addCookie] 方法,将 Cookie 添加到发送给客户端的 HTTP 响应中。此处仅对响应进行准备,实际发送将在发送给客户端的视图 JSP 页面执行时才发生。

10.5.3. [doRetourFormulaire] 方法

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

        // display pre-filled form
    void doRetourFormulaire(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
        // retrieve the user's cookies
        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++;
                }
            }
        }
        // prepare the form template
        request.setAttribute("nom",nom);
        request.setAttribute("age",age);
        // the form is displayed
        getServletContext().getRequestDispatcher((String)params.get("urlFormulaire")).forward(
                request, response);
        return;
    }

新增内容:

[doRetourFormulaire] 方法应显示一个预先填入了最新输入内容的表单。在之前的版本中,这些数据存储在会话中。在本版本中,我们不再使用会话,而是使用 Cookie 来存储客户端与服务器之间交互的数据。 当客户端请求表单验证时,会根据情况收到 [response] 或 [errors] 视图,同时还会收到两个标记为“name”和“age”的 Cookie。当点击这两个视图上的 [返回表单] 链接(这会触发对 URL [/do/retourFormulaire] 的 POST 请求)时,浏览器会将收到的这两个 Cookie 发回给服务器。

  • 第 4–18 行:我们提取标记为“name”和“age”的 Cookie 的值。奇怪的是,没有方法可以根据 Cookie 的键来获取其值。因此,我们必须遍历收到的每个 Cookie。
  • 完成此操作后,将获取的两个值放入 [form] 视图模板中(第 20–21 行),以便其进行显示。

10.6. 测试

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