Skip to content

7. QuiEst 应用程序

在此,我们将介绍一个比之前那些应用程序稍显复杂的 Struts 应用程序。此前那些应用程序为了教学目的而设计得较为简单。

7.1. Users 类

我们有一个 Java 类,用于存储 Unix 机器上用户的相关信息。这些信息存储在三个特定文件中:

  • /etc/passwd:用户列表
  • /etc/group:用户组列表
  • /etc/aliases:电子邮件别名列表

这三个文件的内容如下:

- /etc/passwd

该文件中的行采用以下格式:


    login:pwd:uid:gid:id:dir:shell

其中

登录名
用户登录
密码
其加密密码
用户ID
用户 ID
gid
他的组ID
ID
他的身份
dir
他的登录目录
shell
他的 shell

因此,用户的命令行可能如下所示:

dupond:xg675SDFEkl09:110:57:Guillaume Dupond:/home/iup2-auto/dupond:/bin/bash

上述用户的 ID 为 110,属于组 57。组 57 的定义可在 /etc/group 文件中找到。

- /etc/group

该文件中的行采用以下格式:


    nomGroupe:pwd:gid:membre1,membre2,....

其中

groupName
组名
pwd
加密密码 - 此字段通常为空
组ID
组 ID
成员
用户登录名 - 此字段可以为空

因此,上述第 57 组的配置行可以如下所示:

iup2-auto::57:

这表示第57组被命名为iup2-auto

- /etc/aliases

该文件中的行采用以下格式:

alias:[tab]login

使用

别名
别名
[tab]
一个或多个制表符
登录
该别名所属用户的登录名

因此,该行


    guillaume.dupond:    dupond

表示别名 guillaume.dupond 属于登录名为 dupond 的用户。 请记住,别名用于电子邮件地址中。因此,如果在上例中,Unix 机器名为 shiva.istia.univ-angers.fr,那么发往 guillaume.dupond@shiva.istia.univ-angers.fr 的电子邮件将被投递到该机器上登录名为 dupond 的用户的邮箱中。

这里我们不会详细探讨 users 类的整个接口,而是仅关注其构造函数和几个方法:

import java.io.*;
import java.util.*;

public class users{


   // attributes
  private Hashtable usersByLogin=new Hashtable();       // login --> login, pwd, ..., dir
    private ArrayList erreurs=new ArrayList();             // list of error messages

....

   // manufacturer
  public users(String usersFileName, String groupsFileName, String aliasesFileName) throws Exception {
        // usersFileName: name of the user file with lines of the form
         // login:pwd:uid:gid:id:dir:shell
         // groupsFileName: name of the group file with lines of the form
         // name:pwd:number:member1,member2,...
         // aliasesFileName: name of alias file with lines of the form
         // alias:[tab]login
         // builds the usersByLogin dictionary
....
    }// manufacturer

     // user list
  public Hashtable getUsersByLogin(){
    return usersByLogin;
  }

   // errors
  public ArrayList getErreurs(){
    return erreurs;
  }
按登录用户
一个字典(哈希表),其键为 passwd 文件中的登录名。每个键对应的值是一个字符串数组(String[7]),其元素为与该登录名关联的 passwd 文件行中的 7 个字段。如果该行少于 7 个字段,则某些字段可能为空。
errors
错误消息列表——若无错误则为空

7.2. 该网络应用程序

我们建议构建以下 Web 应用程序(表单页面):

编号
名称
HTML 类型
角色
1
cmbLogins
<select ...>...</select>
显示所有可请求信息的登录项列表
2
btnSearch
<input type="submit" ...>
用于启动搜索

当用户点击 [搜索] 按钮 (2) 时,系统会从类型为 users 的 U 对象中查询登录信息 (1)。如果该登录信息存在,则返回以下响应(信息页面):

如上方的浏览器 URL 所示,表单参数是通过 GET 请求发送至服务器的。因此,我们可以直接向浏览器提供包含这些参数的 URL。这就是我们在此处所做的操作,即输入一个不存在的登录名。我们将获得以下响应(错误页面):

7.3. 应用程序架构

该架构包括以下组件:

  • 视图:
    • logins.jsp,用于显示登录列表(视图 1)
    • infos.jsp,用于显示与登录相关的信息(视图 2)
    • erreurs.jsp,用于显示错误列表(视图 3)
  • 动作使用的 ActionForm 类型表单:
    • formLogins,用于收集来自 logins.jsp 表单的数据
  • 动作:
    • SetupLoginAction,负责准备 form.jsp 的内容并显示该视图
    • InfosLoginAction,在 logins.jsp 的内容发送至服务器后对其进行处理
    • ForwardAction,负责处理 infos.jsp erreurs.jsp 视图中的 [返回表单] 链接
  • 用户业务类,由上述操作用于检索用户数据
  • 由三个平面文件 passwdgroup aliases 提供的模型

7.4. Web 应用程序配置文件

7.4.1. server.xml 文件

应用程序上下文将命名为 /strutsquiest2。因此,我们将向 Tomcat 的 server.xml 文件中添加以下行:

    <Context path="/strutsquiest2" docBase="..." />

完成此操作后,可能需要重启 Tomcat 以便其识别新上下文。可以通过访问 URL http://localhost:8080/strutsquiest2 来验证其有效性。

7.4.2. web.xml 文件

应用程序的 web.xml 配置文件如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN" "http://java.sun.com/j2ee/dtds/web-app_2_2.dtd">
<web-app>
    <servlet>
        <servlet-name>strutsquiest2</servlet-name>
        <servlet-class>istia.st.struts.quiest.Quiest2ActionServlet</servlet-class>
        <init-param>
            <param-name>config</param-name>
            <param-value>/WEB-INF/struts-config.xml</param-value>            
        </init-param>
        <init-param>
            <param-name>passwdFileName</param-name>
            <param-value>data/passwd</param-value>            
        </init-param>
        <init-param>
            <param-name>groupFileName</param-name>
            <param-value>data/group</param-value>            
        </init-param>        
    </servlet>

    <servlet-mapping>
        <servlet-name>strutsquiest2</servlet-name>
        <url-pattern>*.do</url-pattern>        
    </servlet-mapping>
</web-app>

web.xml 文件引入了一项新功能。 Struts 控制器不再是 org.apache.struts.action.ActionServlet,而是一个派生类,我们在此将其命名为 istia.st.struts.quiest.Quiest2ActionServlet。这将允许我们获取两个初始化参数:passwdFileName密码文件的位置)和 groupFileName文件的位置)。此应用程序中不需要别名文件。

7.4.3. struts-config.xml 文件

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>
    <form-beans>
        <form-bean name="formLogins" type="org.apache.struts.action.DynaActionForm">
            <form-property name="cmbLogins" type="java.lang.String" initial=""/>
            <form-property name="tLogins" type="java.lang.String[]"/>            
        </form-bean>            
    </form-beans>

    <action-mappings>

      <action
          path="/init"
            name="formLogins"
            validate="false" 
            scope="session"
          type="istia.st.struts.quiest.SetupLoginsAction"
      >
            <forward name="afficherLogins" path="/vues/logins.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>            
        </action>

      <action
          path="/infosLogin"
            name="formLogins"
            validate="false" 
            scope="session"
          type="istia.st.struts.quiest.InfosLoginAction"
      >
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
            <forward name="afficherInfos" path="/vues/infos.jsp"/>            
        </action>

      <action
          path="/retourLogins"
            parameter="/vues/logins.jsp"
          type="org.apache.struts.actions.ForwardAction"
      />

    </action-mappings>

        <message-resources 
          parameter="istia.st.struts.quiest.ApplicationResources"
    null="false"
  />    

</struts-config>

它包含三个主要部分:

  • <form-beans> 部分中的表单声明
  • <action-mappings> 部分中的操作声明
  • <message-resources> 部分中的资源文件声明

7.4.4. 应用程序的表单对象(Beans)

    <form-beans>
        <form-bean name="formLogins" type="org.apache.struts.action.DynaActionForm">
            <form-property name="cmbLogins" type="java.lang.String" initial=""/>
            <form-property name="tLogins" type="java.lang.String[]"/>            
        </form-bean>            
    </form-beans>    

我们的应用程序中只有一个表单 Bean,名为 formLogins,其类型继承自 DynaActionForm。它将在以下情况下使用:

  • 用于存储显示视图 #1 所需的数据
  • 在用户提交表单时,从视图 #1 中检索表单的值

formLogins Bean 的结构与视图 #1 中的表单相关联。让我们来看看:

编号
名称
HTML 类型
角色
1
cmbLogins
<select ...>...</select>
显示所有可请求信息的登录项列表
2
btnSearch
<input type="submit" ...>
用于启动搜索

让我们区分几种情况:

  • 从客户端到服务器,formLogins 对象用于存储上方 HTML 表单中的值,这些值将通过 [Submit] 按钮提交。因此,它需要一个 cmbLogins 字段来接收 HTML cmbLogins 字段的值,即用户选择的登录名。
  • 从服务器到客户端,formLogins 对象用于为视图 #1 提供初始内容。其 tLogins 字段将作为列表 #1 的内容。其 cmbLogins 字段将用于设置列表 #1 中待选中的项目。

7.4.5. 应用程序操作

操作由 Action 类型或其派生类型的对象处理。操作在 <action-mappings> 标签内进行配置:

    <action-mappings>

      <action
          path="/init"
            name="formLogins"
            validate="false" 
            scope="session"
          type="istia.st.struts.quiest.SetupLoginsAction"
      >
            <forward name="afficherLogins" path="/vues/logins.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>            
        </action>

      <action
          path="/infosLogin"
            name="formLogins"
            validate="false" 
            scope="session"
          type="istia.st.struts.quiest.InfosLoginAction"
      >
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
            <forward name="afficherInfos" path="/vues/infos.jsp"/>            
        </action>

      <action
          path="/retourLogins"
            parameter="/vues/logins.jsp"
          type="org.apache.struts.actions.ForwardAction"
      />

    </action-mappings>

/init 操作

      <action
          path="/init"
            name="formLogins"
            validate="false" 
            scope="session"
          type="istia.st.struts.quiest.SetupLoginsAction"
      >
            <forward name="afficherLogins" path="/vues/logins.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>            
        </action>

下面描述一下 /init 操作的工作原理:

  • /init 操作通常仅在首次请求-响应循环中发生一次,即当用户请求 URL http://localhost:8080/strutsquiest2/init.do 时
  • 此时会创建或回收 formsLogins 对象。该对象会根据 scope 属性的指定,被从会话中检索(回收)或存入会话(创建)。
  • reset 方法会被调用。请注意,在 ActionForm 类及其派生类中,该方法默认不执行任何操作。它在将客户端请求的数据复制到 ActionForm 对象之前被调用,用于在复制前清空该对象。这里的客户端请求是什么? 当请求的 URL 为 http://localhost:8080/strutsquiest2/init.do 时,将触发 /init 操作。该 URL 可通过 GET 或 POST 方式进行请求。只需在请求中包含与 formLogins 字段名称对应的参数,即可初始化这些字段,如下例所示:

Image

  • 该请求包含 cmbLogins 参数(afterpak)。因此,Struts 控制器将该参数的值复制到了 formLoginscmbLogins 字段中。 随后 SetupLoginsAction 动作被执行,并最终显示了 logins.jsp 视图。该视图包含一个表单,其中某些字段的值来自 formLogins。因此,名为 cmbLogins 的 HTML 下拉选择框从 formLogins 中的 cmbLogins 字段(=afterpak)获取了其值。这就是为什么登录列表显示的位置与 afterpak 登录对应。
  • 我们还可以尝试按以下方式传递 tLogins 参数:
http://localhost:8080/strutsquiest2/init.do?cmbLogins=afterpak&tLogins=login1&tLogins=login2

这将使用数组 {"login1","login2"} 初始化 formLoginstLogins 字段。然而,正如我们稍后将看到的,SetupLoginsAction 操作会为 tLogins 字段赋值,并将由此创建的数组替换为一个新数组。最终出现在 logins.jsp 视图中的正是这个新数组。

  • 前面的讨论虽然有些复杂,但说明我们不能假设 /init 操作会在没有客户端参数的情况下被触发。因此,使用 reset 方法来清空 formLogins 可能会很有用。在这种情况下,我们需要扩展 DynaActionForm 类。我们在此处并未这样做。
    • 一旦调用 formLogins 的 reset 方法,控制器会将客户端请求中的数据复制到 formLogins 中同名的字段中。通常,/init 操作是在没有客户端参数的情况下调用的,但我们之前已经说明,没有任何东西能阻止客户端使用任意参数调用 /init 操作。因此,在本阶段结束时,cmbLogins tLogins 字段很可能已经有了值。 我们已经看到,cmbLogins 字段会保留该值,但 tLogins 字段不会。
    • 随后,控制器会检查该操作的 validate 属性。此处的值为“false”。formLoginsvalidate 方法将不会被调用。因此,我们无需编写该方法。
    • 如果 SetupLoginsAction 对象已存在,则将其回收;否则创建新对象,并调用其 execute 方法。该方法的唯一目的是为 formLoginstLogins 字段赋值。该值是登录名数组,将从 users 业务类中获取。此操作可能会失败。这就是为什么 /init 操作之后可能跟两个视图:
      • 如果 users 类无法提供登录名数组,则显示 errors.jsp 视图
      • 否则显示 logins.jsp 视图
    • 控制器将显示上述两个视图中的一个
    • /init 操作的请求-响应循环即告完成。

/infosLogin 操作

      <action
          path="/infosLogin"
            name="formLogins"
            validate="false" 
            scope="session"
          type="istia.st.struts.quiest.InfosLoginAction"
      >
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
            <forward name="afficherInfos" path="/vues/infos.jsp"/>            
        </action>

下面我们来描述一下 /infosLogin 操作的工作原理:

  • 当用户点击 logins.jsp 视图上的 [搜索] 按钮时,通常会触发 /infosLogin 操作。随后,系统会根据视图中 <form> HTML 标签的指定内容向服务器发送请求:
<html:form name="formLogins" method="get" action="/infosLogin" type="org.apache.struts.action.DynaActionForm">
  • 我们可以看到,该请求是通过 GET 方法发送至服务器的。因此,用户可以手动输入该地址:

Image

  • formsLogins 对象会被创建或回收。它会根据 scope 属性的指定,从会话中检索(回收)或存入会话(创建)。
  • 在将客户端请求的数据复制到 ActionForm 对象之前,会调用其 reset 方法。该方法通常采用以下形式:http://localhost:8080/strutsquiest2/infosLogin.do?cmbLogins=xx,其中 xx 是从登录名列表中选定的登录名。但如果用户通过传递任意参数使用了之前的 URL,该参数也可以是任意值。请考虑以下页面序列:

Image

  • 调用 /infosLogin 操作时,传入了参数字符串 cmbLogins=xx&tLogins=login1&tLogins=login2。因此,formLogins 中的 cmbLogins tLogins 字段将分别接收值 "xx" 和 {"login1","login2"}。 /infosLogin 操作将向 users 业务类请求与“xx”登录名关联的信息。users 类会响应称该登录名不存在。因此显示了上方的视图。现在,让我们点击上方的 [返回表单] 链接:

Image

  • [返回表单]链接触发了 /retourLogins 操作。该操作仅直接显示 logins.jsp 视图,不执行任何中间操作。请注意,tLogins 字段用于填充 logins.jsp 视图中的登录名列表。由于用户已将该值修改为 {"login1","login2"},因此这两个登录名现在会出现在列表中。 我们再次强调,在应用程序的运行中,必须充分考虑用户或程序设置的任意参数,这一点至关重要。解决此处问题的方案是让 [返回表单] 链接指向 /init 操作。这样可以确保返回正确的登录名列表。
  • 让我们回到对 /infosLogin 操作的常规请求,例如:

http://localhost:8080/strutsquiest2/infosLogin.do?cmbLogins=afterpak

  • Struts 控制器会为 ActionForm 对象的 cmbLogins 字段赋值。然而,tLogins 字段将不会被赋值(因为发送的请求中没有对应的字段)。这种行为对我们来说是可行的。因此,我们无需为 formLogins 编写自定义的重置方法。
    • 一旦调用 formLogins 的重置方法,控制器会将客户端请求中的数据复制到 formLogins 中同名的字段中。cmbLogins 字段将接收一个值:用户选择的登录名(afterpak)。
    • 随后,控制器会检查该操作的 validate 属性。在此处,其值为“false”。因此不会调用 formLoginsvalidate 方法。
    • 如果 InfosLoginAction 对象已存在,则将其复用;否则创建该对象,并调用其 execute 方法。该方法的作用是检索与 cmbLogins 登录名关联的信息。这些信息将从用户的业务类中获取。此操作可能会失败(例如,登录名不存在)。这就是为什么 /infosLogin 操作之后可以跟两个视图:
      • 如果 users 类无法提供所需的信息,则显示 errors.jsp 视图
      • 否则显示 infos.jsp 视图
    • 控制器将显示这两个视图中的一个
    • /infosLogin 操作的请求-响应循环已完成。

/retourLogins 操作

      <action
          path="/retourLogins"
            parameter="/vues/logins.jsp"
          type="org.apache.struts.actions.ForwardAction"
      />
  • 点击 erreurs.jsp infos.jsp 页面上的 [返回表单] 链接会触发 /retourLogins 操作。
  • 此处该操作未关联任何表单。因此,我们将直接调用 ForwardAction 对象的 execute 方法,该方法将返回一个指向 /vues/logins.jsp 视图的 ActionForward 对象。

7.4.6. 应用程序的消息文件

struts-config.xml 文件的第三部分是消息文件:

        <message-resources 
      parameter="istia.st.struts.quiest.ApplicationResources"
    null="false"
  />        

ApplicationResources.properties 文件位于 WEB-INF/classes/istia/st/struts/quiest 目录下。其内容如下:

errors.header=<ul>
errors.footer=</ul>
parametreManquant=<li>Le paramètre [{0}] n'a pas été initialisé</li>
usersException=<li>Erreur d'initialisation de l'application : {0}</li>
loginInconnu=<li>Le login [{0}] n'existe pas</li>

7.5. 查看代码

若读者无法理解下文展示的视图代码,建议先复习表单处理相关课程。

7.5.1. logins.jsp 视图

请记住,该视图会在以下两种情况下显示:

  • 在首次请求-响应循环中调用 /init 操作时
  • 在后续请求-响应循环中调用 /retourLogins 操作时

logins.jsp 视图的代码如下:

<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>

<html>
    <head>
      <title>Quiest - formulaire</title>
  </head>
  <body background="<html:rewrite page="/images/standard.jpg"/>">
      <center>
        <h2>Application QuiEst</h2>
      <hr>
      <html:form name="formLogins" method="get" action="/infosLogin" type="org.apache.struts.action.DynaActionForm">
          <table>
            <tr>
              <td>Login cherché</td>
            <td>
                <html:select name="formLogins" property="cmbLogins">
                      <html:options name="formLogins" property="tLogins"/>
                  </html:select>
            </td>
            <td>
                <html:submit value="Chercher"/>
            </td>
          </tr>
        </table>
      </html:form>
    </center>
  </body>
</html>

7.5.2. infos.jsp 视图

当成功调用 /infosLogin 操作时,将显示此视图。其代码如下:

<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>

<html>
    <head>
      <title><bean:write name="infosLoginBean" scope="request" property="titre"/></title>
  </head>
  <body background="<html:rewrite page="/images/standard.jpg"/>">
      <h2><bean:write name="infosLoginBean" scope="request" property="titre"/></h2>
    <hr>
    <table border="1">
            <tr>
                <th>login</th><th>pwd</th><th>uid</th><th>gid</th><th>id</th><th>dir</th><th>shell</th>
            </tr>
            <tr>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[0]"/></td>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[1]"/></td>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[2]"/></td>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[3]"/></td>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[4]"/></td>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[5]"/></td>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[6]"/></td>            
            </tr>                                                                                                                                                                                                                
    </table>                                      
    <br>
    <html:link page="/retourLogins.do">
            Retour au formulaire
        </html:link>        
  </body>
</html>

此视图使用了一个名为 infosLoginBean 的对象,该对象由 /infosLogin 操作放入请求中。该对象有两个字段:

String titre;                        // titre à afficher dans la vue
String[] infosLogin;        // tableau des informations à afficher dans la vue

在讲解 InfosLoginAction 类的代码时,我们将更详细地讨论这个类。

7.5.3. errors.jsp 视图

/init/infosLogin 操作发生错误时,将显示此视图。其代码如下:

<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>

<html>
    <head>
      <title>Application QuiEst - erreurs</title>
  </head>
  <body background="<html:rewrite page="/images/standard.jpg"/>">
        <h2 align="center">Application QuiEst - Erreurs</h2>
        <hr>
      <h2>Les erreurs suivantes se sont produites</h2>
        <html:errors/>
    <html:link page="/retourLogins.do">
            Retour au formulaire
        </html:link>    
  </body>
</html>

7.6. Java 类

web.xml 文件引用了一个 Java 类:

<web-app>
    <servlet>
      <servlet-name>strutsquiest2</servlet-name>
    <servlet-class>istia.st.struts.quiest.Quiest2ActionServlet</servlet-class>
....
  </servlet>

...
</web-app>

struts-config.xml 配置文件引用了两个 Java 类:

      <action
          path="/infosLogin"
            name="formLogins"
            validate="false" 
            scope="session"
          type="istia.st.struts.quiest.InfosLoginAction"
      >
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
            <forward name="afficherInfos" path="/vues/infos.jsp"/>            
        </action>

      <action
          path="/init"
            name="formLogins"
            validate="false" 
            scope="session"
          type="istia.st.struts.quiest.SetupLoginsAction"
      >
            <forward name="afficherLogins" path="/vues/logins.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>            
        </action>

7.6.1. Quiest2ActionServlet 类

Quest2ActionServlet 类继承自 ActionServlet 类,即 Struts 的控制器类。我们继承 ActionServlet 类是为了自定义其 init 方法。该方法仅在 Servlet 初始加载时执行一次,允许我们构建一个 users 类型的业务对象。该对象只需构建一次,而 init 方法是执行此构建操作的理想位置。 构建 users 对象需要两个文件:passwdgroup 文件。这两个文件的位置作为参数在应用程序的 web.xml 文件中传递给 Servlet:

    <servlet>
      <servlet-name>strutsquiest2</servlet-name>
    <servlet-class>istia.st.struts.quiest.Quiest2ActionServlet</servlet-class>
    <init-param>
        <param-name>config</param-name>
      <param-value>/WEB-INF/struts-config.xml</param-value>
    </init-param>
    <init-param>
        <param-name>passwdFileName</param-name>
      <param-value>data/passwd</param-value>
    </init-param>
    <init-param>
        <param-name>groupFileName</param-name>
      <param-value>data/group</param-value>
    </init-param>        
  </servlet>

Servlet 代码如下:

package istia.st.struts.quiest;

import java.util.*;
import javax.servlet.*;
import org.apache.struts.action.*;
import istia.st.users.*;

public class Quiest2ActionServlet
  extends ActionServlet {

   // servlet attributes
  private users u = null;
  private ActionErrors erreurs = new ActionErrors();
  private String[] tLogins;

   //init
  public void init() throws ServletException {

    // don't forget to initialize the parent class
    super.init();

     // local variables
    final String[] initParams = {"passwdFileName", "groupFileName"};
    Properties params = new Properties();

    // retrieve servlet initialization parameters
    ServletConfig config = getServletConfig();
    String servletPath = config.getServletContext().getRealPath("/");
    for (int i = 0; i < initParams.length; i++) {
      String valeur = config.getInitParameter(initParams[i]);
      if (valeur == null) {
        erreurs.add(ActionErrors.GLOBAL_ERROR, new ActionError("parametreManquant", initParams[i]));
        valeur = "";
      }
       // the parameter
      params.setProperty(initParams[i], valeur);
    } //for
     // return if there were initialization errors
    if (erreurs.size() != 0) {
      return;
    }
     // create a users object
    try {
      u = new users(servletPath + "/" + params.getProperty("passwdFileName"),
                    servletPath + "/" + params.getProperty("groupFileName"), null);
    }
    catch (Exception ex) {
      erreurs.add(ActionErrors.GLOBAL_ERROR, new ActionError("usersException", ex.getMessage()));
      return;
    } //catch
     // retrieve the list of logins
    tLogins = new String[u.getUsersByLogin().size()];
    Enumeration eLogins = u.getUsersByLogin().keys();
    for (int i = 0; i < tLogins.length; i++) {
      tLogins[i] = (String) eLogins.nextElement();
    }
     // sort logins
    Arrays.sort(tLogins);
  } //init

   // servlet private info access method
  public Object[] getInfos() {
    return new Object[] {erreurs, u, tLogins};
  }
}

简而言之,init 方法的工作原理如下:

  • 首先,调用父类(ActionServlet)的 init 方法,以便其正确初始化
  • 然后读取初始化参数。如果缺少任何参数,则填充私有 ActionErrors 对象的 errors 属性。
  • 如果初始化参数存在,则构建一个 users 对象。此构建过程可能会抛出异常。在这种情况下,ActionErrorserrors 属性会被填充。
  • 如果构造成功,则从创建的对象中检索所有登录记录,将其排序并存入数组,该数组存储在私有属性 `String[] tLogins` 中。
  • 构造好的 users 对象将存储在私有属性 users u 中。
  • 公共方法 getInfos 将三个私有属性(u、errors、tLogins)检索到一个对象数组中。

7.6.2. SetupLoginsAction 类

此操作的目的是初始化 DynaActionForm 对象 formLogins。该对象被放置在会话中,此后不再需要重新初始化。因此,SetupLoginsAction 仅运行一次。其代码如下:

package istia.st.struts.quiest;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.struts.action.*;

public class SetupLoginsAction
  extends Action {

  public ActionForward execute(ActionMapping mapping, ActionForm form,
                               HttpServletRequest request, HttpServletResponse response) throws IOException,ServletException {

     // prepares the form to be displayed
     // retrieve info from the controller servlet
     // infos=(ActionErrors errors, users u, String[] tLogins)
    Object[] infos = ( (Quiest2ActionServlet)this.getServlet()).getInfos();

     // were there any initialization errors?
    ActionErrors erreurs = (ActionErrors) infos[0];
    if (!erreurs.isEmpty()) {
      this.saveErrors(request, erreurs);
      return mapping.findForward("afficherErreurs");
    }

     // put the logins in the form
    DynaActionForm formLogins=(DynaActionForm) form;
    formLogins.set("tLogins",infos[2]);
    return mapping.findForward("afficherLogins");
  }
}

与所有 Struts 动作一样,代码位于 execute 方法中。该方法:

  • 从 Struts 控制器中检索该控制器通过其 init 方法存储的信息。此操作由 Action 类的 getServlet() 方法完成。
  • 这些信息中包含控制器的 ActionErrors 属性。如果该错误列表不为空,则将其放入请求中,并显示 errors.jsp 视图。
  • 如果错误列表为空,则将控制器最初创建的登录列表赋值给 formLogins Bean 的 tLogins 字段。随后请求 logins.jsp 视图,该视图将显示登录列表。

7.6.3. InfosLoginBean 和 InfosLoginAction 类

InfosLoginAction 操作的目的是检索与用户所选登录名相关联的信息,并将其呈现给用户。这些信息将收集在一个类型为 InfosLoginBean 的对象中:

package istia.st.struts.quiest;

public class InfosLoginBean implements java.io.Serializable{

   // bean holding the info needed for the info page
  private String titre;
  private String[] infosLogin;

  // manufacturer
  public InfosLoginBean(String titre, String[] infosLogin){
    this.titre=titre;
    this.infosLogin=infosLogin;
  }

   // getters
  public String getTitre(){
    return this.titre;
  }
  public String[] getInfosLogin(){
    return this.infosLogin;
  }
  public String getInfosLogin(int i){
    return this.infosLogin[i];
  }
}

前面的类是一个 Bean,即一个 Java 类,其中私有属性 T unAttribut 会自动关联两个私有方法:

  • void setUnAttribut(T value) { unAttribut = value; }
  • T getUnAttribut(){ return unAttribut;}

请注意 getset 方法的特殊语法。如果属性是一个数组 T[] unAttribut,我们可以为数组的元素创建 getset 方法:

  • void setUnAttribut(T value, int i) { unAttribut[i] = value; }
  • T getUnAttribut(int i) { return unAttribut[i]; }

为了更好地理解这一点,让我们看看 infos.jsp 视图的代码,该视图必须在 InfosLoginAction 操作之后发送:

<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>

<html>
    <head>
      <title><bean:write name="infosLoginBean" scope="request" property="titre"/></title>
  </head>
  <body background="<html:rewrite page="/images/standard.jpg"/>">
      <h2><bean:write name="infosLoginBean" scope="request" property="titre"/></h2>
    <hr>
    <table border="1">
            <tr>
                <th>login</th><th>pwd</th><th>uid</th><th>gid</th><th>id</th><th>dir</th><th>shell</th>
            </tr>
            <tr>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[0]"/></td>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[1]"/></td>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[2]"/></td>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[3]"/></td>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[4]"/></td>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[5]"/></td>
                <td><bean:write name="infosLoginBean" scope="request" property="infosLogin[6]"/></td>            
            </tr>                                                                                                                                                                                                                
    </table>                                      
    <br>
    <html:link page="/retourLogins.do">
            Retour au formulaire
        </html:link>        
  </body>
</html>

让我们来看以下标签:

<bean:write name="infosLoginBean" scope="request" property="titre"/>

它指示系统写入请求(scope)中包含的 infosLoginBean 对象(name)的 title 字段(property)的值。待写入的值将通过 request.getAttribute("infosLoginBean").getTitre() 获取。因此,InfosLoginBean 类中必须存在 getTitre 方法。确实如此。该标签

<bean:write name="infosLoginBean" scope="request" property="infosLogin[0]"/>

该语句指示系统写入请求中 infosLoginBean 对象的 infosLogin[0] 元素的值。待写入的值将通过 request.getAttribute("infosLoginBean").getInfosLogin(0) 获取。因此,InfosLoginBean 类中必须存在 getInfosLogin(int i) 方法。该类确实包含此方法。

InfosLoginAction 类的目的是根据用户选择的登录信息构建上述 InfosLoginBean 对象。其代码如下:

package istia.st.struts.quiest;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.struts.action.*;
import istia.st.users.*;

public class InfosLoginAction
  extends Action {

  public ActionForward execute(ActionMapping mapping, ActionForm form,
                               HttpServletRequest request, HttpServletResponse response) throws IOException,ServletException {

     // should display login information

     // retrieve info from the controller servlet
     // infos=(ActionErrors errors, users u, LoginBean[] tLogins)
    Object[] infos = ( (Quiest2ActionServlet)this.getServlet()).getInfos();

     // were there any initialization errors?
    ActionErrors erreurs = (ActionErrors) infos[0];
    if (!erreurs.isEmpty()) {
      this.saveErrors(request, erreurs);
      return mapping.findForward("afficherErreurs");
    }

     // first retrieve this login
    String login = (String) ( (DynaActionForm) form).get("cmbLogins");

    // do we have anything?
    if (login == null) {
      // not normal - the login form is returned
            DynaActionForm formLogins=(DynaActionForm) form;
            formLogins.set("tLogins",infos[2]);
            return mapping.findForward("afficherLogins");
    }

     // we have a login - we're looking for it
    String[] infosLogin = (String[]) ( (users) infos[1]).getUsersByLogin().get(login);

     // have we found?
    if (infosLogin == null) {
      // login not found - error page displayed
      ActionErrors erreurs2=new ActionErrors();
      erreurs2.add(ActionErrors.GLOBAL_ERROR, new ActionError("loginInconnu", login));
      this.saveErrors(request, erreurs2);
      return mapping.findForward("afficherErreurs");
    }

     // the login has been found - we put the information found in the query
    String titre="Application QuiEst - login["+login+"]";
    InfosLoginBean infosLoginBean= new InfosLoginBean(titre,infosLogin);
    request.setAttribute("infosLoginBean",infosLoginBean);
    return mapping.findForward("afficherInfos");
  }

}

execute 方法的工作原理如下:

  • 它会检索 Struts 控制器在初始化过程中收集的信息。如果控制器检测到任何错误,执行将在此处停止,并提示用户查看这些错误。
  • 我们验证是否存在登录用户。如果用户通过登录选择表单提交,则存在登录用户。然而,用户也可能直接在浏览器中输入该操作的 URL 而未传递任何参数。如果不存在登录用户,我们将重新显示登录用户列表。
  • 如果存在登录信息,我们将检索与用户业务类相关联的信息。如果该类无法找到正在搜索的登录信息,则显示错误页面。否则,将创建一个 InfosLoginBean 对象来存储 infos.jsp 视图所需的信息。该对象被放入请求中,随后显示 infos.jsp 页面。

7.7. 部署

应用程序的目录结构如下:

 

7.8. 结论

我们在一个实际应用中使用了 Struts 并运用了业务类。我们还演示了必须特别关注客户端发送的请求,且不应对其性质做任何假设。请求可以是任何类型,每个应用程序都必须首先验证其有效性。