Skip to content

11. 示例 09 - 整数的转换与验证

接下来我们将通过一系列示例,探讨表单参数的转换与验证。具体问题如下:为了处理 [http://machine:port/.../Action] 格式的 URL,[FilterDispatcher] 控制器会实例化实现所请求操作的类,并执行该类的一个方法——默认情况下是名为 execute 的方法。对该 execute 方法的调用会经过一系列拦截器:

拦截器列表定义在 [struts2-core.jar] 归档文件根目录下的 [struts-default.xml] 文件中。其中定义的拦截器列表如下:


             <interceptor-stack name="defaultStack">
                <interceptor-ref name="exception"/>
                <interceptor-ref name="alias"/>
                <interceptor-ref name="servletConfig"/>
                <interceptor-ref name="i18n"/>
                <interceptor-ref name="prepare"/>
                <interceptor-ref name="chain"/>
                <interceptor-ref name="debugging"/>
                <interceptor-ref name="scopedModelDriven"/>
                <interceptor-ref name="modelDriven"/>
                <interceptor-ref name="fileUpload"/>
                <interceptor-ref name="checkbox"/>
                <interceptor-ref name="multiselect"/>
                <interceptor-ref name="staticParams"/>
                <interceptor-ref name="actionMappingParams"/>
                <interceptor-ref name="params">
                  <param name="excludeParams">dojo\..*,^struts\..*</param>
                </interceptor-ref>
                <interceptor-ref name="conversionError"/>
                <interceptor-ref name="validation">
                    <param name="excludeMethods">input,back,cancel,browse</param>
                </interceptor-ref>
                <interceptor-ref name="workflow">
                    <param name="excludeMethods">input,back,cancel,browse</param>
                </interceptor-ref>
</interceptor-stack>

在拦截器中,有一个拦截器负责将请求携带的参数值parami=values 的形式注入到 Action 中。我们知道,如果 Action 存在 setParami 方法,这些值就会通过该方法注入到 Action 的 parami 字段中。否则,不会进行注入,也不会报告错误。

字符串 parami=values 是一个字符串。到目前为止,值的注入都是针对类型为 Stringparami 字段进行的:

private String parami ;

将字符串 valeuri 注入为 *parami* 的值时没有问题。如果 parami 不是 String 类型,则必须将 valeuri 转换为 parami 的类型 Ti。这就是转换问题。例如,我们可能希望年龄是一个整数,并在操作中写道:

private int age ;

此外,我们可能希望将年龄限制在 1 到 150 之间。这便涉及一个有效性验证问题。parami 参数虽然可以转换为正确的类型,但其值未必有效。因此需要完成两个步骤。回到请求处理图:

将分别由两个拦截器处理参数的转换验证。如果任一步骤失败,请求将不会继续执行到动作(如上图红色路径所示)。提交了错误参数的表单将重新显示,并附带错误信息。

参与参数转换和验证的拦截器分别是前面拦截器列表第19行和第20行的conversionErrorvalidation拦截器请注意第20至22行:如果被调用的方法是inputback、cancelbrowse方法之一则不会应用validation拦截器。我们稍后将利用这一特性。

我们将首先探讨整数的转换与验证。由于该验证涉及多个要素,我们将花些时间讲解这个第一个示例。一旦理解了这些内容,后续示例的讲解就会更加迅速。

11.1. 表单

  • 在 [1] 中,输入表单
  • 在[2]中,未输入任何值时的验证结果

11.2. NetBeans 项目

NetBeans 项目如下:

  • 在 [1] 中,应用程序的三个视图
  • 在 [2] 中,源代码、国际化消息文件以及 Struts 配置文件。

11.3. Struts 配置

该应用程序通过 [struts.xml] 和 [example.xml] 文件进行配置。

[struts.xml] 文件内容如下:


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
    "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
    "http://struts.apache.org/dtds/struts-2.0.dtd">
 
<struts>
  <constant name="struts.custom.i18n.resources" value="messages" />
 
  <include file="example/example.xml"/>
 
  <package name="default" namespace="/" extends="struts-default">
    <default-action-ref name="index" />
    <action name="index">
      <result type="redirectAction">
        <param name="actionName">Accueil</param>
        <param name="namespace">/example</param>
      </result>
    </action>
  </package>
</struts>

第 12–18 行将 [/example/Home] 动作定义为用户未指定动作时的默认动作。

[example.xml] 文件内容如下:


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
        "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
        "http://struts.apache.org/dtds/struts-2.0.dtd">
 
<struts>
  <package name="example" namespace="/example" extends="struts-default">
    <action name="Accueil">
      <result name="success">/example/Accueil.jsp</result>
    </action>
    <action name="FormInt" class="example.FormInt">
      <result name="input">/example/FormInt.jsp</result>
      <result name="cancel" type="redirect">/example/Accueil.jsp</result>
      <result name="success">/example/ConfirmationFormInt.jsp</result>
    </action>
  </package>
</struts>
  • 第 8–10 行:[Home] 操作显示 [Home.jsp] 视图
  • 第 11 行:[FormInt] 操作默认会调用 [example.FormInt] 类的 execute 方法。我们将看到另外两个方法也会被调用:input cancel 方法。这些方法随后将在请求参数中指定。
  • 第 12 行:input 键将显示 [FormInt.jsp] 视图(第 5 行)。该视图即表单视图。
  • 第 13 行:cancel 键将由与 [Cancel] 链接关联的 cancel 方法返回。随后渲染的视图将是 [Home.jsp] 视图(通过 type=redirect 进行重定向)。
  • 第 14 行:`success` 键由 [FormInt] 动作的 `execute` 方法返回。如果请求到达了 `execute` 方法,则意味着它已成功通过了所有拦截器,特别是那些验证参数有效性的拦截器。随后,`execute` 方法仅返回 `success` 键,这将显示确认视图 [ConfirmationInt.jsp]。

11.4. 消息文件

[messages.properties] 文件内容如下:


Accueil.titre=Accueil
Accueil.message=Struts 2 - Conversions et validations
Accueil.FormInt=Saisie de nombres entiers
Form.titre=Conversions et validations
FormInt.message=Struts 2 - Conversion et validation de nombres entiers
Form.submitText=Valider
Form.cancelText=Annuler
Form.clearModel=Raz mod\u00e8le
Confirmation.titre=Confirmation
Confirmation.message=Confirmation des valeurs saisies
Confirmation.champ=champ
Confirmation.valeur=valeur
Confirmation.lien=Formulaire de test

除了此文件外,视图还使用以下 [FormInt.properties] 文件:


int1.prompt=1-Nombre entier positif de deux chiffres
int1.error=Tapez un nombre entier positif de deux chiffres
int2.prompt=2-Nombre entier
int2.error=Tapez un nombre entier
int3.prompt=3-Nombre entier >=-1
int3.error=Tapez un nombre entier >=-1
int4.prompt=4-Nombre entier <=10
int4.error=Tapez un nombre entier <=10
int5.prompt=5-Nombre entier dans l''intervalle [1,10]
int5.error=Tapez un nombre entier dans l''intervalle [1,10]
int6.prompt=6-Nombre entier dans l''intervalle [2,20]
int6.error=Tapez un nombre entier dans l''intervalle [2,20]

[FormInt.properties] 文件仅在生成视图的操作为 [FormInt] 操作时使用。这是在消息文件过大时将其拆分的一种方式。Action 操作的消息在 [Action.properties] 文件中进行本地化。

11.5. 视图与操作

接下来我们将介绍该应用程序的视图和操作。根据应用程序配置:


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
        "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
        "http://struts.apache.org/dtds/struts-2.0.dtd">
 
<struts>
  <package name="example" namespace="/example" extends="struts-default">
    <action name="Accueil">
      <result name="success">/example/Accueil.jsp</result>
    </action>
    <action name="FormInt" class="example.FormInt">
      <result name="input">/example/FormInt.jsp</result>
      <result name="cancel" type="redirect">/example/Accueil.jsp</result>
      <result name="success">/example/ConfirmationFormInt.jsp</result>
    </action>
  </package>
</struts>

我们可以看到,这里有三个视图 [Home.jsp、FormInt.jsp、ConfirmationFormInt.jsp] 和两个动作 [Home、FormInt]。

11.5.1. Home.jsp

视图 [Home.jsp] 如下所示:

Image

其代码如下:


<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
  <head>
    <title><s:text name="Accueil.titre"/></title>
    <s:head/>
  </head>
 
  <body background="<s:url value="/ressources/standard.jpg"/>">
    <h2><s:text name="Accueil.message"/></h2>
    <ul>
      <li>
        <s:url id="url" action="FormInt!input"/>
        <s:a href="%{url}"><s:text name="Accueil.FormInt"/></s:a>
      </li>
    </ul>
  </body>
</html>

第 14 行中的链接会生成以下 HTML 代码:

<a href="<a href="view-source:http://localhost:8084/exemple-09/example/FormInt.action">/exemple-09/example/FormInt!input.action</a>">Saisie de nombres entiers</a>

因此,这是一个指向 [FormInt] 操作的链接,该操作在 [example.xml] 中配置如下:


    <action name="FormInt" class="example.FormInt">
      <result name="input">/example/FormInt.jsp</result>
      <result name="cancel" type="redirect">/example/Accueil.jsp</result>
      <result name="success">/example/ConfirmationFormInt.jsp</result>
</action>

点击该链接将实例化 [example.FormInt] 类并执行其 input 方法。由于该方法不存在,因此将执行父类 ActionSupportinput 方法。该方法除了返回 input 键外不执行任何操作。因此,将显示 [/example/FormInt.jsp] 视图。

此外,input 方法是验证拦截器忽略的方法之一:


        <interceptor-ref name="validation">
          <param name="excludeMethods">input,back,cancel,browse</param>
</interceptor-ref>

因此,将不会进行参数验证。这一点很重要,因为这里没有参数,而我们稍后会看到,验证规则要求必须存在六个参数。

11.5.2. [FormInt] 操作

[FormInt] 操作与以下 [FormInt] 类相关联:


package example;
 
import com.opensymphony.xwork2.ActionSupport;
import com.opensymphony.xwork2.ModelDriven;
import java.util.Map;
import org.apache.struts2.interceptor.SessionAware;
import org.apache.struts2.interceptor.validation.SkipValidation;
 
public class FormInt extends ActionSupport implements ModelDriven, SessionAware {
 
  // constructor without parameters
  public FormInt() {
  }
 
  // action model
  public Object getModel() {
    if (session.get("model") == null) {
      session.put("model", new FormIntModel());
    }
    return session.get("model");
  }
 
  public String cancel() {
    // cleaning the model
    ((FormIntModel) getModel()).clearModel();
    // result
    return "cancel";
  }
 
  @SkipValidation
  public String clearModel() {
    // close to the model
    ((FormIntModel) getModel()).clearModel();
    // result
     return INPUT;
  }
 
  // SessionAware
  private Map<String, Object> session;
 
  public void setSession(Map<String, Object> session) {
    this.session = session;
  }
 
  // validation
  @Override
  public void validate() {
    // valid int6 input?
    if (getFieldErrors().get("int6") == null) {
      int int6 = Integer.parseInt(((FormIntModel) getModel()).getInt6());
      if (int6 < 2 || int6 > 20) {
        addFieldError("int6", getText("int6.error"));
      }
    }
  }
}

我们将根据需要对这段代码进行注释。目前:

  • 第 9 行,[FormInt] 类实现了两个接口:
  • ModelDriven 类仅有一个方法,即第 16 行中的 getModel
  • SessionAware,该类仅有一个方法,即第 41 行的 setSession
  • 第 16–21 行:ModelDriven 接口的实现。请注意,该接口允许将视图的模型委托给外部类,在本例中即为下述 [FormIntModel] 类:

package example;
 
public class FormIntModel {
 
  // constructor without parameters
  public FormIntModel() {
  }
 
  // form fields
  private String int1;
  private Integer int2;
  private Integer int3;
  private Integer int4;
  private Integer int5;
  private String int6;
 
  // raz model
  public void clearModel(){
    int1=null;
    int2=null;
    int3=null;
    int4=null;
    int5=null;
    int6=null;
  }
 
  // getters and setters
   ...
}

[FormIntModel] 模型有六个字段,分别对应 [FormInt.jsp] 视图中的六个输入字段。这六个字段将接收提交的值。其中四个字段的类型为 Integer。因此,对于这四个字段,将出现将 String 转换为 Integer 的问题。clearModel 方法允许您重置模型。

让我们回到 [FormInt] 动作的 getModel 方法:


  // action model
  public Object getModel() {
    if (session.get("model") == null) {
      session.put("model", new FormIntModel());
    }
    return session.get("model");
}
  • 第 3–5 行:从会话中检索模型。如果会话中不存在该模型,则创建一个模型实例并将其放入会话中。
  • 第 6 行:虽然每次向该操作发起新请求时都会创建一个操作实例,但其模型仍保留在会话中。

我们可以看到,该类并未定义输入方法,但其父类定义了一个返回输入键的方法。调用该方法将显示 [FormInt.jsp] 视图,我们现在将展示该视图。

11.5.3. [FormInt.jsp] 视图

[FormInt.jsp] 视图如下:

  • [1] 中的空白表单
  • 在 [2] 中,参数验证失败后的表单。

其代码如下:


<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
  <head>
    <title><s:text name="Form.titre"/></title>
    <s:head/>
  </head>
 
  <body background="<s:url value="/ressources/standard.jpg"/>">
    <h2><s:text name="FormInt.message"/></h2>
    <s:form name="formulaire" action="FormInt">
      <s:textfield name="int1" key="int1.prompt"/>
      <s:textfield name="int2" key="int2.prompt"/>
      <s:textfield name="int3" key="int3.prompt"/>
      <s:textfield name="int4" key="int4.prompt"/>
      <s:textfield name="int5" key="int5.prompt"/>
      <s:textfield name="int6" key="int6.prompt"/>
      <s:submit key="Form.submitText" method="execute"/>
    </s:form>
    <br/>
    <s:url id="url" action="FormInt" method="cancel"/>
    <s:a href="%{url}"><s:text name="Form.cancelText"/></s:a>
      <br/>
    <s:url id="url" action="FormInt" method="clearModel"/>
    <s:a href="%{url}"><s:text name="Form.clearModel"/></s:a>
  </body>
</html>

  • 第 12–17 行:六个输入字段对应于 [FormInt] 操作的 [FormIntModel] 模型中的六个字段。当视图显示时,输入字段的 value 属性将用于显示这些字段的值。如果缺少 value 属性,则使用 name 属性。
  • 第 12 行:该输入字段与操作的 int1 字段相关联(name),或者如果该操作实现了 ModelDriven 接口,则与该操作的模型中的 int1 字段相关联。本例中即为这种情况。其他所有字段也是如此。
  • 第 18 行:[Submit] 按钮将表单数据提交至第 11 行定义的 [FormInt] 操作。其 execute 方法将被执行。
  • 第 21–22 行:[Cancel] 链接执行 [FormInt.cancel] 方法。
  • 第 24–25 行:[Clear Model] 链接执行 [FormInt.clearModel] 方法。

11.5.4. [ConfirmationFormInt.jsp] 视图

当 [FormInt.jsp] 表单中的所有输入均有效时,该视图将被显示。

  • 在 [1] 中,提交了有效值
  • 在[2]中,确认页面

[ConfirmationInt.jsp]视图的代码如下:


<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
  <head>
    <title><s:text name="Confirmation.titre"/></title>
    <s:head/>
  </head>
 
  <body background="<s:url value="/ressources/standard.jpg"/>">
    <h2><s:text name="Confirmation.message"/></h2>
    <table border="1">
      <tr>
        <th><s:text name="Confirmation.champ"/></th>
        <th><s:text name="Confirmation.valeur"/></th>
      </tr>
      <tr>
        <td><s:text name="int1.prompt"/></td>
        <td><s:property value="int1"/></td>
      </tr>
      <tr>
        <td><s:text name="int2.prompt"/></td>
        <td><s:property value="int2"/></td>
      </tr>
      <tr>
        <td><s:text name="int3.prompt"/></td>
        <td><s:property value="int3"/></td>
      </tr>
      <tr>
        <td><s:text name="int4.prompt"/></td>
        <td><s:property value="int4"/></td>
      </tr>
      <tr>
        <td><s:text name="int5.prompt"/></td>
        <td><s:property value="int5"/></td>
      </tr>
      <tr>
        <td><s:text name="int6.prompt"/></td>
        <td><s:property value="int6"/></td>
      </tr>
    </table>
    <br/>
    <s:url id="url" action="FormInt" method="input"/>
    <s:a href="%{url}"><s:text name="Confirmation.lien"/></s:a>
  </body>
</html>

要理解这段代码,请记住视图是在 [FormInt] 类实例化之后显示的。因此,该类及其模型 [FormIntModel] 的字段均可被视图访问。

  • 第 16–38 行:显示六个字段的值
  • 第 42–43 行:指向 [FormInt] 操作的链接。为此链接生成的代码如下:


<a href="/exemple-09/example/FormInt!input.action">Formulaire de test</a>

该链接的具体 URL 表明 [FormInt] 操作的输入方法必须处理此请求。回顾 [example.xml] 中 [FormInt] 操作的配置:


    <action name="FormInt" class="example.FormInt">
      <result name="input">/example/FormInt.jsp</result>
      <result name="cancel" type="redirect">/example/Accueil.jsp</result>
      <result name="success">/example/ConfirmationFormInt.jsp</result>
</action>

[FormInt] 类的 input 方法将继承自其父类 ActionSupport。在拦截器执行完毕后,[FormInt] 类的 input 方法才会被执行

我们知道,调用 input 方法会被验证拦截器忽略。因此,将不会进行验证。

显示 [FormInt.jsp] 视图:

在 [2] 中,输入字段恢复为默认值。这看似正常,实则不然。由于调用了 [FormInt] 操作,因此关联的 [FormInt] 类已被实例化。由于该类实现了 ModelDriven 接口,因此其 getModel 方法被调用:


  // action model
  public Object getModel() {
    if (session.get("model") == null) {
      session.put("model", new FormIntModel());
    }
    return session.get("model");
}

我们可以看到,该操作的模型是从会话中检索出来的。在上一步中,该模型已根据提交的值进行了更新。因此,我们检索到了这些值。如果我们没有将会话中的模型存入会话,那么在 [FormInt.jsp] 视图中就会出现六个空字段。

11.5.5. [FormInt!clearModel] 操作

[FormInt!clearModel] 操作由点击 [Clear Model] 链接触发:

  • [1] 中的表单,显示错误输入后的状态
  • [2] 所示为点击 [清除模型] 链接后的表单。

[FormInt.clearModel] 方法如下:


  @SkipValidation
  public String clearModel() {
    // close to the model
    ((FormIntModel) getModel()).clearModel();
    // result
    return INPUT;
}

  • 第 1 行:无需执行任何验证。我们使用 @SkipValidation 注解来表示这一点。因此,验证拦截器将不会执行任何验证。
  • 第 4 行:执行 [FormIntModel].clearModel 方法。我们之前已经遇到过该方法。它将模型的六个字段重置为 null
  • 第 7 行:该方法返回输入键。

回到 [FormInt] 操作的配置:


    <action name="FormInt" class="example.FormInt">
      <result name="input">/example/FormInt.jsp</result>
      <result name="cancel" type="redirect">/example/Accueil.jsp</result>
      <result name="success">/example/ConfirmationFormInt.jsp</result>
</action>

我们可以看到,点击该输入键将显示 [FormInt.jsp] 视图。该视图显示了模型的六个字段。由于这些字段均为空,因此视图中显示了六个空字段 [2]。

11.5.6. [FormInt!cancel] 操作

点击 [Cancel] 链接将触发 [FormInt!cancel] 操作:

  • [1] 中的图示为输入错误后的表单
  • [2] 中,点击 [取消] 链接后的主页。

[FormInt.cancel] 方法如下:


  public String cancel() {
    // cleaning the model
    ((FormIntModel) getModel()).clearModel();
    // result
    return "cancel";
}

  • 第 1 行:请注意,该方法前未添加 SkipValidation 注解。不过,我们确实不想执行验证。cancel 方法是验证拦截器忽略的四个方法(inputbackcancelbrowse)之一,因此无需添加 SkipValidation 注解。
  • 第 3 行:这清空了模型
  • 第 5 行:返回 cancel

回到 [FormInt] 操作的配置:


    <action name="FormInt" class="example.FormInt">
      <result name="input">/example/FormInt.jsp</result>
      <result name="cancel" type="redirect">/example/Accueil.jsp</result>
      <result name="success">/example/ConfirmationFormInt.jsp</result>
</action>

我们可以看到,“cancel”键将在客户端重定向后显示 [Home.jsp] 视图。这在视图 [2] 中有所展示。

11.6. 验证过程

接下来,我们将处理与模型中以下六个字段相关的六个输入字段的验证:


  // champs du formulaire
  private String int1;
  private Integer int2;
  private Integer int3;
  private Integer int4;
  private Integer int5;
private String int6;

每次实例化 [FormInt] 类时,如果执行的方法未被验证拦截器忽略,则会进行此验证。该验证由以下内容控制:

  • [FormInt-validation.xml] 文件(如果它与 [FormInt] 类位于同一文件夹中)
  • [FormInt.validate] 方法(如果存在)。

  • 在 [1] 中:验证过程所需的 [xwork-validator-1.0.2.dtd] 文件
  • 在 [2] 中:位于 [FormInt] 类同一文件夹中的 [FormInt-validation.xml] 文件

[FormInt-validation.xml] 文件内容如下:


<!--
<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//
EN" "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
-->
 
<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//
EN" "http://localhost:8084/exemple-09/example/xwork-validator-1.0.2.dtd">
 
<validators>
 
  <field name="int1" >
    <field-validator type="requiredstring" short-circuit="true">
      <message key="int1.error"/>
    </field-validator>
    <field-validator type="regex" short-circuit="true">
      <param name="expression">^\d{2}$</param>
      <param name="trim">true</param>
      <message key="int1.error"/>
    </field-validator>
  </field>
 
  <field name="int2" >
    ...
  </field>
 
...
</validators>
  • 在 [3] 中,验证文件的 DTD(文档类型定义)URL。该 URL 必须可访问;否则,将不会使用该验证文件。
  • 在 [7] 中,应用程序所用 DTD 的 URL。我们已将 DTD 文件放置在 example-09 项目 [1] 的 [example] 文件夹中,以便即使没有互联网连接也能使用。
  • 第 11–20 行:设置与模型中 int1 字段关联的 int1 参数的验证条件。

表单中名为 int1 的标签如下:


<s:textfield name="int1" key="int1.prompt" />

模型中的 int1 字段声明如下:


private String int1;
  • 第 12–14 行:验证 int1 参数是否存在(不为空)且长度不为零。若不满足此条件,则该输入字段会关联一条错误消息。该字段在 [FormInt.properties] 中定义如下:

int1.error=Tapez un nombre entier positif de deux chiffres

若出现错误,int1 参数的验证过程将终止(short-circuit=true)。

  • 第 15–19 行:使用正则表达式检查 int1 参数的有效性。
  • 第 16 行:正则表达式,在此情况下为两个数字,前后无任何字符。
  • 第 17 行:在与正则表达式进行比较之前,将删除 int1 参数的首尾空格。
  • 第 18 行:错误消息(如有)。它与前一个验证器的错误消息相同。

让我们看看它是如何工作的:

  • 在 [1] 中,int1 字段的输入不正确
  • 在 [2] 中,页面返回:
  • 显示了 int1.error 键的错误信息。该信息以红色显示。
  • 该错误字段的标签也显示为红色。
  • 错误的输入内容再次显示。必须预见到这一点,因为这未必是默认行为。

我们已经看到,如果请求成功通过了所有拦截器(特别是验证拦截器),表单验证会触发 [FormInt].execute 方法的执行

  • 如果请求到达了操作的 execute 方法,它会像我们之前看到的那样,将 success 键返回给控制器。
  • 如果验证拦截器因待验证的参数无效而阻止了请求,则会将 `input` 键返回给控制器。

由于 [FormInt] 操作配置如下:


    <action name="FormInt" class="example.FormInt">
      <result name="input">/example/FormInt.jsp</result>
      <result name="cancel" type="redirect">/example/Accueil.jsp</result>
      <result name="success">/example/ConfirmationFormInt.jsp</result>
</action>

当出现验证错误时,将显示 [FormInt.jsp] 视图,即表单。Struts 标签的设计旨在显示与其相关的任何错误消息。因此,我们将看到 [FormInt.jsp] 视图,其中各个字段都附有错误消息。这在视图 [2] 中有所展示。

现在让我们来检查 int2 字段的验证,该字段在模型中声明如下:


private Integer int2;

[FormInt-validation.xml] 中 int2 字段的验证规则如下:


<field name="int2" >
    <field-validator type="required" short-circuit="true">
      <message key="int2.error"/>
    </field-validator>
    <field-validator type="conversion" short-circuit="true">
      <message key="int2.error"/>
    </field-validator>
  </field>

  • 第 2–4 行:验证 int2 参数是否存在。
  • 第 5–7 行:验证字符串到整数的转换是否可行
  • 第 3、6 行:int2.error 键的错误消息如下:


int2.error=Tapez un nombre entier

[FormInt-validation.xml] 文件中模型的 Integer 字段 int3 的验证规则如下:


<field name="int3" >
    <field-validator type="required" short-circuit="true">
      <message key="int3.error"/>
    </field-validator>
    <field-validator type="conversion" short-circuit="true">
      <message key="int2.error"/>
    </field-validator>
    <field-validator type="int" short-circuit="true">
      <param name="min">-1</param>
      <message key="int3.error"/>
    </field-validator>
  </field>

  • 第 8–11 行:验证 int3 字段是否为大于等于 -1 的整数
  • 第 3、7 行:int3.error 键的错误信息如下:

int3.error=Tapez un nombre entier >=-1

[FormInt-validation.xml] 模型中整数字段 int4 的验证规则如下:


<field name="int4" >
    <field-validator type="required" short-circuit="true">
      <message key="int4.error"/>
    </field-validator>
    <field-validator type="conversion" short-circuit="true">
      <message key="int2.error"/>
    </field-validator>
    <field-validator type="int" short-circuit="true">
      <param name="max">10</param>
      <message key="int4.error"/>
    </field-validator>
  </field>

  • 第 8-11 行:验证该值是否为小于等于 10 的整数
  • 第 3、7 行:int4.error 键的错误消息如下:


int4.error=Tapez un nombre entier <=10

[FormInt-validation.xml] 文件中模型的整数字段 int5 的验证规则如下:


<field name="int5" >
    <field-validator type="required" short-circuit="true">
      <message key="int5.error"/>
    </field-validator>
    <field-validator type="conversion" short-circuit="true">
      <message key="int2.error"/>
    </field-validator>
    <field-validator type="int" short-circuit="true">
      <param name="min">1</param>
      <param name="max">10</param>
      <message key="int5.error"/>
    </field-validator>
  </field>

  • 第 5–9 行:验证该值是否为 [1, 10] 范围内的整数。
  • 第 3、8 行:int5.error 键的错误消息如下:


int5.error=Tapez un nombre entier dans l''intervalle [1,10]

[FormInt-validation.xml] 中模型的 String int6 字段的验证规则如下:


<field name="int6" >
    <field-validator type="requiredstring" short-circuit="true">
      <message key="int6.error"/>
    </field-validator>
    <field-validator type="regex" short-circuit="true">
      <param name="expression">^\d{1,2}$</param>
      <param name="trim">true</param>
      <message key="int6.error"/>
    </field-validator>
  </field>

  • 第 5–9 行:验证 int6 是否为 2 位字符串。
  • 第 3、8 行:int6.error 键的错误消息如下:


int6.error=Tapez un nombre entier dans l''intervalle [2,20]

之前的验证并未检查 int6 参数是否为 [2,20] 范围内的整数。此检查在 [FormInt].validate 方法中进行,该方法在处理完 [FormInt-validation.xml] 文件后执行。该方法如下:


  // validation
  @Override
  public void validate() {
    // valid int6 input?
    if (getFieldErrors().get("int6") == null) {
      int int6 = Integer.parseInt(((FormIntModel) getModel()).getInt6());
      if (int6 < 2 || int6 > 20) {
        addFieldError("int6", getText("int6.error"));
      }
    }
}
  • 第 5 行:我们检查 int6 字段是否存在任何错误。如果存在,则在此处停止。
  • 第 6 行:如果没有错误,则从模型中获取 int6 字符串字段并将其转换为整数。
  • 第 7 行:我们验证检索到的整数是否在 [2,20] 范围内。
  • 第 8 行:若不满足此条件,则向 int6 字段附加一条错误消息。该错误消息通过键值 int6.error 从消息文件中获取。

如果在此验证过程结束时发现错误,则终止对 [FormInt].execute 方法的调用,并将输入键返回给 Struts 控制器。

11.7. 最终细节

我们已经了解了几种输入整数的方法。它们并非完全等同。例如,请看 int5int6 这两个输入字段:

在 [FormInt.jsp] 视图中,它们的声明如下:


      <s:textfield name="int5" key="int5.prompt"/>
<s:textfield name="int6" key="int6.prompt"/>

它们的模型在 [FormIntModel.java] 中声明如下:


  private Integer int5;
private String int6;

int5 字段的类型为 Integer,而 int6 字段的类型为 String。它们的验证规则不同:


<field name="int5" >
    <field-validator type="required" short-circuit="true">
      <message key="int5.error"/>
    </field-validator>
    <field-validator type="conversion" short-circuit="true">
      <message key="int2.error"/>
    </field-validator>
    <field-validator type="int" short-circuit="true">
      <param name="min">1</param>
      <param name="max">10</param>
      <message key="int5.error"/>
    </field-validator>
  </field>
 
  <field name="int6" >
    <field-validator type="requiredstring" short-circuit="true">
      <message key="int6.error"/>
    </field-validator>
    <field-validator type="regex" short-circuit="true">
      <param name="expression">^\d{1,2}$</param>
      <param name="trim">true</param>
      <message key="int6.error"/>
    </field-validator>
  </field>

int6 字段的验证由 [FormInt] 操作的 validate 方法执行:


  public void validate() {
    // valid int6 input?
    if (getFieldErrors().get("int6") == null) {
      int int6 = Integer.parseInt(((FormIntModel) getModel()).getInt6());
      if (int6 < 2 || int6 > 20) {
        addFieldError("int6", getText("int6.error"));
      }
}

尽管表达方式不同,这两条验证规则都旨在验证输入的值是否为指定范围内的整数。然而,如下图所示,int5int6 字段在运行时的行为有所不同:

  • 在 [1] 中,两个字段均输入了相同的错误值
  • 在 [2] 中,返回了错误页面。这两个字段显示了不同的错误信息。
  • 在 [3] 中,int5 字段因使用英文显示而出现不必要的提示信息。这是由于字符串转整数操作失败所致。Apache 日志中也记录了异常:
Avertissement: Error setting expression 'int5' with value '[Ljava.lang.String;@1ad405d8'
ognl.MethodFailedException: Method "setInt5" failed for object example.FormIntModel@21b63266 [java.lang.NoSuchMethodException: example.FormIntModel.setInt5([Ljava.lang.String;)]

奇怪的是,Struts 试图查找方法 FormIntModel.setInt5(String value),但未能找到。

该错误消息的关键在于 xwork.default.invalid.fieldvalue。要将其翻译成法语,只需将法语文本与该键关联即可。因此,我们在 [messages.properties] 文件中添加以下行:


...
xwork.default.invalid.fieldvalue=Valeur invalide pour le champ "{0}".

11.8. 结论

至此,我们对这个首个专门用于参数验证的应用程序的研究告一段落。该应用程序的讲解较为复杂。接下来我们将探讨类似的应用程序。因此,我们将仅就变化之处进行说明。