2013年3月

树欲静而风不止

纠结.jpg

今天喝了很多酒,说了特别多的话,也许比整个星期说的话的总和还多。

一起共事的毛童鞋要走了,公司换了新的领导,出台了新的政策,要减少我们外包人员, 要么走要么转到本公司。于是,陆陆续续的几个人走了,那些曾经朝夕相处的人,曾经是满不在乎,爱答不理的人, 他们真的走的时候,我的心里竟然充满了忧伤,也许下一个就是我了。

毛童鞋是和我差不多的人,性格内向,不善言谈,但善良明事理,最重要的是对人真诚,似乎有了奇哥的影子,话说回来,奇哥哪去了,奇哥似乎消失了很长一段时间了,不知道在干什么?

肖童鞋走了,刘童鞋走了,张童鞋走了,现在毛童鞋也要走了,而我呢?何去何从是一个问题!

这段时间,似乎我很清闲,但内心却一直纠结于这个问题之中。留在这里继续写那些丑陋而千篇一律的代码,我可以继续过着稳定泰然的生活;离开这里,也许会找到更好的工作,也许找不到,也许还是写这样的代码,但我都将失去这种生活,将会开始早出晚归之旅,而好处是我将找一份可以写优美代码的工作,我热爱的工作。哪一点更重要呢?

Struts 2 的国际化

本节我们将快速浏览Struts 2的国际化机制,重点在于怎样使用国际化方法满足我们的各种需求,而不深究其内部详细的原理。

1. Struts 2 框架和Java i18n

Java平台早已内置了对i18n的支持,Struts 2提供了一个高层的、极其方便的本地Java对i18n支持的封装。首先我们先简要介绍下Java的基础概念。

1.1 使用ResourceBundle和Locale取得本地化文本

1. 在ResourceBundle中存储资源

Java的ResourceBundle是一个抽象类,其子类的实现管理包中包含了资源。ResourceBundle的子类能按照它们喜欢的任意方式管理它们的资源。Java平台提供了两个方便的子类供你使用,其中最常用的是PropertyResourceBundle,这个类从普通文本属性文件中加载资源,这些资源包括了各种版本内容的文件。

2. 使用本地Java ResourceBundle

如果我们想去的一个本地化文本,必须先获取对包含这个文本的包的引用。以下代码展示了这个功能:

Locale currentLocale = new Locale( "tr");
ResourceBundle myMessages = ResourceBundle.getBundle("EmailClientMessages",currentLocale);
String greetingLabel = myMessages.getString( "greeting");

getBundle()方法获取参数后会在其拥有的资源中进行查找,这个过程包含两个阶段。首先,它查找与接收到的包名参数匹配的属性文件,之后它会查找包的Java类实现。这些类实现由与属性文件命名相似的Java类来实现,例如EmailClientMessages_tr.java,最后她会发现这些属性文件,并使用这些属性文件创建一个ResourceBundle实例。

1.2 Struts 2如何解决本地Java对i18n支持的问题

Struts 2框架为i18n付出了很多努力。框架仍然使用我们刚刚看过的Java类,但事情变得更容易了。首先,不需要实例化ResourceBundle,Struts 2会自动创建它,并处理确定需要哪个包的所有琐碎内容。另一方面,框架也处理确定正确地域的过程,它通过检查来源于浏览器的HTTP头信息自动确定当前的Locale。当然,你也可以覆盖这个行为,并使用其他方式确定地域信息。

Struts 2包创建的过程使用约定和配置混合的方法来定位属性文件,它是一个双向的通道。Struts 2告诉你它在哪里查找属性文件,同时你也可以告诉它在哪里查找属性文件。

1.3 Struts 2的i18n如何工作

框架驱动国际化功能的机制由如下步骤完成:

  1. 让动作扩展ActionSupport,以便它们继承默认的TextProvider实现,
  2. 将一些属性文件放在默认的TextProvider可以发现的位置,
  3. 使用Struts 2的text标签或者通过OGNL直接调用getText()方法将消息取到页面中。

1.4 Struts 2 默认的TextProvider ResourceBundle搜索算法

TextProvider的默认实现会在几个常见的地方搜索开发人员创建的属性文件以创建ResourceBundle,这些位置中有很多都遵循与超类或实现的接口的命名相似的模式。以下展示了Struts 2尝试填充的ResourceBundle的名字和出处:

  1. ActionClass——是否有一个ResourceBundle与当前动作类的名字相同?换句话说就是是否有一系列的属性文件命名为ActionClass.properties、ActionClass*es.properties等?
  2. MyInterface——如果动作实现了任何接口,有没有与这些接口关联的ResourceBundle?也就是说,如果当前类实现了MyInterface接口,是否有一系列的属性文件命名为MyInterface.properties、MyInterface*es.properties等?并且每一个接口的超接口也会被查找,更具体的接口优先于更高层的超接口。
  3. MySuperClass——如果动作扩展了一个父类,那么是否有一个ResourceBundle与这个超类关联?这和接口的查找类似。但在继承链上低级别类的ResourceBundle优先于高级别类的ResourceBundle。也就是说,如果Object.properties存在,那么它是最后一个。
  4. 如果动作实现了ModelDriven接口,那么会使用模型对象的类查找ResourceBundle。也就是说,如果模型对象是User类,那么User.properties文件会被加载。
  5. package.properties——接着,搜索过程会尝试加载当前动作类所在包的ResourceBundle,以及这个链上的每一个父包。注意这些属性文件都叫做package.properties。
  6. 通过关键字引用的ValueStack上的域模型对象,这与第4步的ModelDriven很相似。对于一个给定的关键字,如user.username,如果ValueStack上公开了一个叫做user的属性,那么这个属性的类会用来加载ResourceBundle。
  7. 默认的ResourceBundle——Struts 2允许指定全局包,这个包总是可以访问的。

包搜索顺序中的前6步列出了Struts 2搜索属性文件过程中基于的约定位置,如果基于约定的包都不存在,那么就会使用默认包,也就是第7步所说的全局包。我们可以在struts.properties文件中或者某一个XML配置文件(例如struts.xml或者它包含的一个文件)的constant元素中设置这个属性和所有Struts 2属性。以下是具体的使用方法;

<constant name="struts.custom.i18n.resources" value="global-messages" />

以下是在struts.properties文件中配置的相同内容:

struts.custom.i18n.resources=global-messages

可以指定使用逗号分隔的一系列包,这些包按照给定的顺序被搜索,也可以使用包空间指定包的位置,如下所示:

struts.custom.i18n.resources=global-messages,manning.utils.otherBundle  

注意,如果同时使用这两种方式,struts.properties文件优于XML文件中的constant元素。

2. 从包中取得消息文本

在前面几节的讲解中我们已经捎带着说明了怎样从保重取得消息文本的方法,包括:使用UI组件标签的key属性、验证框架中使用、validate()方法中使用getText()方法、页面中使用text标签等,就剩下本地化类型转换错误消息的国际化了,下面我们就讲解它。

本地化类型转换错误消息的国际化

例如页面中有一个Age字段,当用户输入字母在后台会发生类型转换错误,为了给Age字段追加自定义类型转换错误消息,你只需要在某个可访问的保重追加一个属性,这个属性遵循的命名约定为invalid.fieldvalue.fieldname。以下属性为Age字段定义的一个错误消息:

invalid.fieldvalue.age=Please enter a numerical value for your age.

只需把上述内容追加到Register动作可访问的包(例如Register.properties文件)就可以使用了。此外,你可能想改变本地化默认的类型转换消息。你可以在默认的global-message保重追加以下属性:

xwork.default.invalid.fieldvalue=We can not convert that to a Java type.

Struts 2 的验证框架(一)

之前我们已经学习了如何通过Validateable接口的validate()方法实现动作本地的验证方式。虽然这种方式工作得很好,但是它的某些限制最终会变得难以负担。因此,在本节中我们将引入Struts 2框架的另一个高级机制——验证框架。数据验证框架提供了一个比Validateable接口更通用、更可维护的验证解决方案。验证框架中更强大的一点是Validator(验证器),它是一种可重用的组件,其中实现了特定类型的验证逻辑。

1.熟悉数据验证框架

数据验证框架早已成为了Web应用程序框架的一部分,但是Struts 2将它在优化、模块化、整洁继承方面带到了一个权限的水平。

1.1 验证框架的架构

下图展示了验证框架的主要组件:

Struts 2 验证框架.png

如上图所示,在验证框架中有3个主要的组件:域数据、验证元数据和验证器。在验证工作中每一个组件都扮演了至关重要的角色,我们将一一解释这些内容。

1.域数据

首先,必须有一些验证数据,这些数据以属性的方式驻留在Struts 2的动作中,数据来自对象或者模型驱动对象。

2.验证元数据

验证器和数据属性之间有一个中间组件,这个中间组件就是元数据,这些数据将每一个数据属性与属性运行时数据的合法性验证关联起来。一个属性可以关联多个验证器,也可以不关联任何验证器。元数据层可以使用XML文件或者Java注解将数据属性映射到验证器。

3.验证器

所有这些数据的验证实际上都有验证器完成。验证器是一个包含了执行某种细粒度的验证行动的逻辑的可重用组件。

1.2 Struts 2 工作流中的验证框架

现在看看所有的这些验证工作是如何完成的。验证框架实际上与基本数据验证共享了大部分功能,它使用ValidationAware接口存储错误信息。如果需要,workflow拦截器会将用户带回到input页面。实际上,唯一改变的是验证本身,但是这是一个重大的变化。

不管基本的验证示例还是验证框架都在Struts 2自带的defaultStack环境下工作。下面代码片段是跟当前话题相关的defaultStack部分,来自struts-default.xml文件:

<interceptor-ref name="params"/>
<interceptor-ref name="conversionError"/>
<interceptor-ref name="validation"/>
<interceptor-ref name="workflow"/>

我们需要注意validation拦截器,workflow拦截器调用validate()方法来实施基本验证还没有参与。当validation拦截器触发时,它会通过前一节提到的元数据实施所有已经定义的验证。一下是验证框架的工作流程,数据验证框架在数据转移、类型转换之后,workflow拦截器之前运行:

验证框架的工作流程.png

从图中可以看出整个流程有两个数据验证机制,一种是validation拦截器即数据验证框架的验证,一种是workflow调用validate()方法的验证。我们将会有两种选择,如果能够预测到一个验证逻辑将来还会被重用,那么使用一个自定义验证器来实现会更有意义。然而,如果验证逻辑确实是一个生僻的需求,并且很可能只适用于一次的情况,那么把它放在validate()方法中会更有意义。

2.将动作关联到验证框架

2.1 使用ActionClass-validations.xml声明验证元数据

现在,我们呢使用XML文件声明需要验证的元数据,文件名以:动作类名+-+validations.xml为规范,以下是Register-validation.xml:

<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//
    EN" "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
<validators>
    <field name="password">
        <field-validator type="requiredstring">
        <message>You must enter a value for password.</message>
        </field-validator>
    </field>
    <field name="username">
        <field-validator type="stringlength">
        <param name="maxLength">8</param>
        <param name="minLength">5</param>
        <message>While ${username} is a nice name, a valid username must
                be between ${minLength} and ${maxLength} characters long.
        </message>
        </field-validator>
    </field>
    <field name="portfolioName">
        <field-validator type="requiredstring">
        <message key="portfolioName.required"/>
        </field-validator>
    </field>
    <field name="email">
        <field-validator type="requiredstring">
        <message>You must enter a value for email.</message>
        </field-validator>
        <field-validator type="email">
        <message key="email.invalid"/>
        </field-validator>
    </field>
    <validator type="expression">
        <param name="expression">username != password</param>
        <message>Username and password can't be the same.</message>
    </validator>
</validators>

1.字段验证器

字段验证器是用来操作一个独立字段的验证器。这里的字段(field)与数据属性意思相同,字段验证器中的“字段”的含义是它们来源于请求的HTML表单的字段。

例如第一个字段password字段,一旦为数据声明了一个field元素,我们只需在field元素内部放入field-validator元素来声明哪些验证器用来验证此数据。对于password,我们只声明了一个requiredstring验证器。message元素包含验证失败时显示的消息的文本。一个field元素可以声明任意多个验证器。

2.非字段验证器

你也可以声明验证逻辑不是适用在某个特定字段的验证器,这些验证器适用于整个动作,通常包含对多个字段值的检查。例如:expression验证器就是如此。这个验证器使用OGNL比较其他两个字段是否相等,如果不相等表达式返回true,验证通过,否则,验证失败最终用户会返回输入页面并显示message元素中的内容。

3.消息元素的选择

message元素用来指定验证错误的情况下用户应该看到的消息,我们可以使用OGNL动态的组织消息。例如username字段的声明那样。XML中OGNL使用$作为转义字符,而不是OGNL通常使用的%符号。

message元素的另一个选择是将这些消息抽出到外部的资源文件中以实现验证错误的提示信息的国际化,portfolioName字段的验证方式是一个很好的示例,message 的 key 属性指向的正式国际化文件中的引用。如下:

user.exists=This user ${username} already exists.
portfolioName.required=You must enter a name for your initial portfolio.
email.invalid=Your email address was not a valid email address.

2.2 内建的验证器

框架自带了一系列功能强大的验证器以满足常见的验证需求,以下列出了全部的内建验证器:

数据验证器 参数 功能 类型
required 没有 检验值非空 字段
requiredstring trim(默认为true) 验证值非空,并且不是空字符串 字段
stringlength trim(默认值为true)、minLenth、maxLength 验证字符床的长度在参数指定的范围内。不指定的参数不做检查。 字段
int min、max 验证这个证书值在参数指定的最小值和最大值之间 字段
double minInclusive、maxInclusive、minExclusive、maxExclusive 验证浮点值在参数指定的范围内 字段
date min、max 验证日期值在指定的最小值和最大值之间。日期格式MM/DD/YYYY 字段
email 没有 验证电子邮件地址格式 字段
url 没有 验证URL格式 字段
fieldexpression expression(必须) 根据当前ValueStack解析OGNL表达式。表达式必须返回true或者false以决定验证是否成功 字段
expression expression(必须) 与fieldexpression相同,但用在动作级别 动作
visitor context、appendPrefix 将域对象属性的验证转交给域对象本地的验证声明 字段
regx expression(必须)、caseSensitive、trim 验证String遵循给定的正则表达式 字段

这些数据验证器中的大部分功能都很简单,唯一需要深入讨论的是visitor验证器,这个验证器允许你为每一个域模型(ModelDriven)类定义验证元素据。

3.编写自定义验证器

编写自定义验证器与编写任何其他的Struts 2组件都不同,下面我们将通过编写一个检查密码完整性的自定义验证器。

3.1 检查密码强度的自定义验证器

所有验证器必须实现Validator接口或者FieldValidator接口,前者将实现非字段验证器,后者将实现字段验证器。通常情况下我们会扩展对应的ValidatorSupport或者FieldValidatorSupport这两个类。

我们设计的密码验证器将做一下3项检查:

  • 密码必须包含一个大写字母或者小写字母;
  • 密码必须包含0~9的一个数字;
  • 密码必须至少包含一套特殊字符中的一个字符。

特殊字符有一个默认值,但可以通过一个参数配置,这与之前使用的stringlength参数很类似。以下是自定义验证器类代码:

public class PasswordIntegrityValidator extends FieldValidatorSupport {
    static Pattern digitPattern = Pattern.compile( "[0-9]");
    static Pattern letterPattern = Pattern.compile( "[a-zA-Z]");
    static Pattern specialCharsDefaultPattern = Pattern.compile( "!@#$");

    public void validate(Object object) throws ValidationException {
        String fieldName = getFieldName();
        String fieldValue = (String) getFieldValue(fieldName, object );
        fieldValue = fieldValue.trim();
        Matcher digitMatcher = digitPattern.matcher(fieldValue);
        Matcher letterMatcher = letterPattern.matcher(fieldValue);
        Matcher specialCharacterMatcher;
        if ( getSpecialCharacters() != null ){
            Pattern specialPattern = Pattern.compile("[" + getSpecialCharacters() + "]" );
            specialCharacterMatcher = specialPattern.matcher( fieldValue );
        } else{
            specialCharacterMatcher = specialCharsDefaultPattern.matcher( fieldValue );
        }
        if ( !digitMatcher.find() ) {
            addFieldError( fieldName, object );
        }else if ( !letterMatcher.find() ) {
            addFieldError( fieldName, object );
        }else if ( !specialCharacterMatcher.find() ) {
            addFieldError( fieldName, object );
        }
    }
    private String specialCharacters;
    //省略get和set方法
}

作为一个开发人员,只需要关注验证逻辑的细节,这个逻辑放在validate()方法中,这个方法是Validator接口定义的入口方法并且在扩展类的抽象支持类中没有实现。此外,还需要创建JavaBean属性,这些属性应该与所有公开给用户的参数匹配。以下代码片段展示了一个参数如何从XML文件传递到这个属性:

<field-validator type="passwordintegrity">
    <param name="specialCharacters">$!@#?</param>
    <message>Your password must contain one letter, one number, and one
        of the following "${specialCharacters}".
    </message>
</field-validator>

我们通过两个辅助方法 getFieldName()和getFieldValue()获取字段的值,这里注意,validate()方法接收了需要被验证的对象,由于我们在动作级别通过Register-validation.xml文件定义了验证,所以传入validate()方法的对象是动作本身。获取字段值后,我们进行了各种检查,最终将错误信息添加到存储的错误消息集中,当然还是使用从支持类继承的辅助方法。这就是全部内容了。

3.2 使用自定义数据验证器

我们在应用程序的类路径下的validators.xml文件中声明自定义的验证器,也就是src文件夹下,一下是这个文件的所有代码:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE validators PUBLIC
        "-//OpenSymphony Group//XWork Validator Config 1.0//EN"
        "http://www.opensymphony.com/xwork/xwork-validator-config-1.0.dtd">

<validators>
   <validator name="passwordintegrity" class="manning.utils.PasswordIntegrityValidator"/>
</validators>

然后在Register-validation.xml文件中把这个验证器添加到password字段对应的验证器中,以下是代码片段:

<field name="password">
  <field-validator type="requiredstring">
     <message >Password is required.</message>
  </field-validator>
  <field-validator type="stringlength">
     <param name="maxLength">10</param>
     <param name="minLength">6</param>
     <message>Your password should be 6-10 characters.</message>
  </field-validator>
  <field-validator type="passwordintegrity">
      <param name="specialCharacters">$!@#?</param>
      <message>Your password must contain one letter, one number, and one of the following "${specialCharacters}".</message>
  </field-validator>
</field>

Struts 2 的验证框架(二)

前一节中我们介绍了Struts2的验证框架及怎样自定义验证器,本节将介绍一些使用验证框架的高级主题。一些高级主题只研究了验证机制的细微差别,而另一些高级主题则展示了如何将验证框架应用到一些更特殊的Struts 2开发模式。这些内容将涵盖验证器的继承,将验证映射到域模型对象而不是动作,验证器失败时短路跳出验证等。

1. 在域对象级别验证

在域对象级别是指动作把整个User对象作为JavaBean属性公开出来。在域对象级别验证,首先需要做的是定义元数据,以下是User-validation.xml文件的全部内容:

<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//EN"
       "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
<validators>
  <field name="password">
      <field-validator type="stringlength">
         <param name="maxLength">10</param>
         <param name="minLength">6</param>
         <message>Your password should be 6-10 characters.</message>
      </field-validator>
      <field-validator type="passwordintegrity">
          <param name="specialCharacters">$!@#?</param>
          <message>Your password must contain one letter, one number, and one of the following "${specialCharacters}".</message>
      </field-validator>
  </field>
  <field name="username">
      <field-validator type="stringlength">
         <param name="maxLength">8</param>
         <param name="minLength">5</param>
         <message>While ${username} is a nice name, a valid username must be between ${minLength} and ${maxLength} characters long. </message>
     </field-validator>
  </field>
  <field name="email">
      <field-validator type="requiredstring">
          <message>You must enter a value for email.</message>
      </field-validator>
       <field-validator type="email">
         <message key="email.invalid"/>
      </field-validator>
   </field>
  <validator type="expression">
      <param name="expression">username != password</param>
      <message>Username and password can't be the same.</message>
  </validator>
</validators>

这个文件与之前的Register-validation.xml文件完全相同,但是我们需要把它放在User类同级目录下。User验证数据就绪后,我们需要把使用User的动作和这些验证元数据连接起来。这由visitor验证器完成。以下代码片段展示了UpdateAccount-validation.xml文件的简要内容,此文件将放在与UpdateAccount动作相同目录下:

<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//EN"
       "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
<validators>
   <field name="user">
      <field-validator type="visitor">
         <message>User:  </message>     
      </field-validator>    
  </field>
</validators>

这个文件仅仅使用visitor验证器将验证细节全部转交给在这个指定的字段名的类上定义的验证元数据,即指定了user字段。visitor验证器使用这个信息找到User-validation.xml并使用这个文件描述的验证逻辑验证user属性上所有的数据。而在message元素上我们只定义了验证产生的错误信息的前缀。

ModelDriven动作中使用验证框架

当动作实现ModelDriven接口时,也可以使用上述技术,对于UpdateAccount-validation.xml文件我们只需要做一下轻微的改变:

<validators>
   <field name="model">
      <field-validator type="visitor">
        <param name="appendPrefix">false</param>
        <message>User:  </message>      
      </field-validator>    
  </field>
</validators>

做了两处变更,首先,由于ModelDriven动作的域对象通过getModel()获取方法公开出来,所以现在我们需要改变指向model的字段名。其次,我们需要添加appendPrefix参数告诉visitor验证器不需要在字段前添加user前缀来获取字段值。

2. 使用验证上下文优化验证

有时候可能需要一种更加细粒度的对什么时候运行哪些验证的控制级别,为了控制这些内容,验证框架引入了上下文的概念。数据验证上下文提供了一种简单的方法来识别我们想验证的数据,在使用这些数据的应用程序中的具体位置。

如果一个动作类中包含了多个方法作为动作的路口点时,我们就需要使用验证上下文。具体的做法是为这几个方法分别建立遵循:类名+-+方法名+-+validation.xml这个规则的文件,并在文件中定义验证逻辑。如果你仍然定义了一个类名+-+validation.xml的文件,那么这个文件中定义的验证也会被加载。

与visitor验证器和域对象一起使用验证上下文时,方法与此相同,比如只需在User类所在的目录下定义类名+-+方法名+-+validation.xml的文件分别添加验证规则即可。

显然这种方式很可能会发生冲突而不适合,幸运的是,我们可以在visitor验证器中使用context属性定义一个上下文名称来替换之前文件名中的方法名。使用如下:

<field name="user">
    <field-validator type="visitor">
        <param name="context">admin</param>
        <message>User: </message>
    </field-validator>
</field>

于是我们就可以使用User-admin-validation.xml的名称命名文件而避免冲突。

3. 验证继承

我们已经知道验证可以被声明在不同级别的不同上下文中,现在我们简要的描述一下验证声明的继承。下面展示了当框架开始处理时收集验证文件位置的顺序:

  • SuperClass-validation.xml
  • SuperClass-aliasName-validation.xml
  • Interface-validation.xml
  • Interface-aliasName-validation.xml
  • ActionClass-validation.xml
  • ActionClass-aliasName-validation.xml

在定义验证时,应该基于这个结构在这个搜索列表的更高层定义通用的验证,这样允许你重用这些定义。

4. 验证短路效应

验证框架的一个有用特性是当一个给定的验证失败时,它能够像短路一样停止后续验证。下面是一个用例:

<field name="password">
    <field-validator type="stringlength" short-circuit="true">
        <param name="maxLength">10</param>
        <param name="minLength">6</param>
        <message>Your password should be 6-10 characters.</message>
    </field-validator>
    <field-validator type="passwordintegrity">
        <param name="specialCharacters">$!@#?</param>
        <message>Your password must contain one letter, one number, and one of the following "${specialCharacters}".
        </message>
    </field-validator>
</field> 

这里我们追加的唯一内容是short-circuit属性,把它设置为true。这样做的目的是想在stringlength检查失败的情况下不让passwordintegrity检查运行。注意,虽然这个short-circuit定义在一个字段验证器上,但是这个字段剩余的验证都会成为短路的。如果在动作级别定义短路,那么所有的验证都将成为短路的。

5. 使用注解声明验证

使用注解声明验证和使用XML没有什么不同,下面是一个完整示例:

@Validation

public class RegisterValidationAnnotated extends ActionSupport implements SessionAware {

    @ExpressionValidator(expression = "username != password", message = "Username and password can't be the same.")
    public String execute(){
        /*
         * Create and move the data onto our application domain object, user.
         */
        User user = new User();
        user.setPassword( getPassword() );
        Portfolio newPort = new Portfolio();
        newPort.setName( getPortfolioName() );
        user.getPortfolios().add( newPort );
        user.setUsername( getUsername() );

        /* Login the newly created user */
        getPortfolioService().persistUser( user );
        session.put( Struts2PortfolioConstants.USER, user );

        return SUCCESS;
    }

    /* JavaBeans Properties to Receive Request Parameters */
    private String username;
    private String password;
    private String portfolioName;
    private boolean receiveJunkMail;
    private String email;

    @RequiredStringValidator(type = ValidatorType.FIELD, message="Email is required.")
    @EmailValidator(type = ValidatorType.FIELD, key="email.invalid", message="Email no good.")
    public void setEmail(String email) {
        this.email = email;
    }
    public String getEmail() {
        return email;
    }

    @RequiredStringValidator(type = ValidatorType.FIELD, message = "Portfolio name is required.")
    public String getPortfolioName() {
        return portfolioName;
    }
    public void setPortfolioName(String portfolioName) {
        this.portfolioName = portfolioName;
    }

    @StringLengthFieldValidator(type = ValidatorType.FIELD, minLength="5" , maxLength = "8",  message = "Password must be between ${minLength} and ${maxLength} characters.")
    @RequiredStringValidator(type = ValidatorType.FIELD, message = "Password is required.")
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }

    @RequiredStringValidator(type = ValidatorType.FIELD, message = "Username is required.")
    @StringLengthFieldValidator(type = ValidatorType.FIELD, minLength="5" , maxLength = "8",  message = "Username must be between ${minLength} and ${maxLength} characters.")
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }

    public boolean isReceiveJunkMail() {
        return receiveJunkMail;
    }
    public void setReceiveJunkMail(boolean receiveJunkMail) {
        this.receiveJunkMail = receiveJunkMail;
    }

    private PortfolioServiceInterface portfolioService;

    public PortfolioServiceInterface getPortfolioService( )     {

        return portfolioService;

    }

    public void setPortfolioService( PortfolioServiceInterface portService){
        portfolioService = portService;
    }

    private Map session;

    public void setSession(Map session) {

        this.session = session;
    }

}

一个细微的不同是消息处理方式。message属性是这些注解必须的属性。如果想使用来自于属性文件资源包的消息,你不需要把关键字作为message属性的值。相反,向注解添加key属性即可,但你仍然需要在message中指定消息的值,当key查找失败时,将使用它作为默认消息。

Struts 2 标签(四)

我们已经从宏观上介绍了UI组件架构,现在开始学习使用UI组件标签。首先从总结所有UI组件都使用的属性和方法开始。

1.通用属性

所有Struts 2 UI组件标签通用的属性有很多,大部分是底层HTML元素公开的很多属性,这里我们重点关注Struts 2标签最核心的使用。通常情况下,你可以假定Struts 2标签支持底层HTML元素的所有属性。

下面是这些常用通用属性的一个列表,如果属性的数据类型是String,那么属性值会被当作字符串字面值解析,除非使用%{expression}标记;所有非String类型都会被作为OGNL表达式自动求值。

属性 主题 数据类型 描述
name simple String 设置表单输入元素的name属性。如果没有手工设置value属性,那么也会被传播到组件的value属性。组件自己使用name属性指向ValueStack上的属性作为提交的请求参数值的目标
value simple Object 指向ValueStack上的属性的OGNL表达式,用来为预填充设置表单输入元素的值。默认为name属性设置的值
key simple String 从ResourceBundle取得本地化的标签(label),可以传播到name属性,也可以传播到value属性
label XHTML String 为组件创建一个HTML标签(label),如果设定使用key属性和本地化的文本就不需要指定这个属性
labelPosition XHTML String 元素标签(label)的位置,可选的值是left或者top
required XHTML Boolean 如果设定为true,那么标签(label)旁边会出现一个星号来暗示这是一个必须的字段。默认情况下,如果一个字段级别的数据验证器通过name属性与这个输入字段关联时,这个值为true
id simple String HTML的id属性。如果id属性没有指定,组件会创建一个唯一标识。
cssClass simple String HTML的class属性
cssStyle simple String HTML的style属性
disabled simple String HTML的disablled属性
tabindex simple String HTML的tabindex属性
theme N/A String 在哪个主题下呈现这个组件,默认是xhtml
templateDir N/A String 用来覆盖从中取出模板的默认目录名
template N/A String 用来呈现UI标签的模板。

除了上述属性,组件也支持常用的JavaScript事件处理属性,例如onclick和onchange。

2.简单组件

1.head组件

这个标签什么事也做不了,但是在支持其他标签方面它起了重要作用。如果某个标签依赖head标签引入的资源,那么省去head标签,这个标签就不能正常工作。下面是使用方法:

<head>
    <title>Portfolio Registration</title>
    <s:head/>
</head>

下面是它生成的内容:

<link rel="stylesheet" href=" . . . styles.css" type="text/css"/>
<script language="JavaScript" type="text/javascript" src=". . .dojo.js"/>
<script language="JavaScript" type="text/javascript" src="dojoRequire.js"/>

2.form组件

form组件也许是这些组件中最重要的组件,它提供了重要的切入点,form组件是指向Struts 2 动作的表单。除了常用属性外,form组件还使用下面的一些属性:

属性 类型 描述
action String 表单提交的目标,可以是Struts2动作的名字或者URL
namespace String Struts2命名空间,在这个命名空间中查找命名动作(action属性),或者从这里开始构建URL。默认为当前命名空间
method String 与HTML form属性相同,默认为POST
target String 与HTML form属性相同
enctype String 上传文件时设置为multipart/form-data
validate Boolean 与验证框架一起使用,打开客户端JavaScript验证

下面是使用的示例:

<s:form action="Login">
    ......
</s:form> 

会生成下面的代码:

<form id="Login" name="Login" onsubmit="return true;"
    action="/manningSampleApp/chapterSeven/Login.action" method="POST">
......
</form>

决定如何创建URL时需要遵循的步骤如下所述

  • 如果没有指定action属性,则会重新指向生成当前页面的动作
  • 如果指定了action属性,它首先被当作Struts 2动作解析。如果没有指定namespace属性,那么会在当前请求的命名空间中查找这个动作。如果指定了namespace属性,那么会在指定的命名空间中查找这个动作。注意,指定的动作没有.action的扩展名。
  • 如果action属性中指定的值不是声明性架构中的Struts 2动作,那么这个值会被用来直接构建URL。如果指定的字符串以一个斜线(/)开头,那么它被假定为ServletContext相关,框架会在它之前加上ServletContext的庐江构建一个URL。如果指定的字符串没有已斜线开头,那么这个值会被直接当作URL。注意这种情况下,即使指定了namespace属性也不会使用。

下面是使用namespace的示例:

<s:form action="Login" namespace="/chapterFour">

会生成下面的代码:

<form id="Login" name="Login" onsubmit="return true;"
action="/manningSampleApp/chapterFour/Login.action" >

下面是一个使用不映射动作的例子:

<s:form action="/chapterSeven/PortfolioHomePage.jsp">

生成代码如下:

<form id="PortfolioHomePage" onsubmit="return true;"
action="/manningSampleApp/chapterSeven/PortfolioHomePage.jsp">

最后,如果你指定一个不是以斜线开始的值,并且这个值也不是一个动作,也不是一个完整的URL,那么这个值会被原封不动的输出到action属性中。下面是示例:

<s:form action="MyResource">    

这是输出:

<form id="MyResource" onsubmit="return true;" action="MyResource">

3.textfield组件

这个组件生成了无处不在的文本输入字段。除了通用属性外,textfield还经常使用一些自己的属性,如下:

属性 类型 描述
maxlength String 字段数据的最大长度
readonly Boolean 如果为true,字段不可编辑
size String 可视长度

4.password组件

password标签实质上很像textfield标签,除通用属性外,它还经常使用如下属性:

属性 类型 描述
maxlength String 字段数据的最大长度
readonly Boolean 如果为true字段不可编辑
size String 文本字段的可视长度
showPassword Boolean 如果设置为true,并且ValueStack中对应的属性有值,那么密码会预填充。默认为false。被填充的值会被伪装。

5.textarea组件

textarea标签生成一个基于HTML的textarea元素的组件。除了通用属性外,它还有一些其他属性:

属性 类型 描述
cols Integer 列数
rows Integer 行数
readonly Boolean 如果为true字段不可编辑
wrap String 指定textarea中的内容是否应该被包装起来

6.checkbox组件

checkbox组件使用单个HTML的checkbox元素创建一个Boolean型的组件。注意,这个组件与HTML的checkbox组件不是完全相同,它只能指定Boolean类型的值,与这个标签绑定的Java端的属性必须是Boolean类型的。除了之前的通用属性,checkbox组件也使用下列属性:

属性 类型 描述
fieldValue String 被checkbox提交的哦真实值,可能是false或者true,默认是true
value String 与fieldValue一起使用,决定checkbox是否被选中。如果fieldValue=true,并且value=true,那么组件被选中

预填充通常使用UI组件的value属性完成,对于checkbox组件来说要复杂一些。我们必须指定字段要提交的值以及Java端属性的当前值。在checkbox组件的上下文中他们是不同的值,而在textfield标签中是相同的。

3.基于集合的组件

Struts 2 标签支持数组、Map或者List集合类型。集合支持的组件的金币嗯逻辑是Java端的数据以一系列选项的方式呈现给用户,之后用户选择一个选项,这个值随着请求被提交。

1.select组件

select组件可能是最常见的基于几何的UI组件,在Java Web应用程序中,通常利用Collection、Map或者数组中的数据构建这一系列选项。

以下代码展示了select UI组件最简单的用例:

<s:select name="user.name" list="{'Mike','Payal','Silas'}" />

select组件的list属性指向了为这个组件提供数据的数据集。通常情况下会使用一个OGNL表达式指向ValueStack上的一系列的数据,而不是用字面值生成的列表。但这里在介绍这个标签时我们尽量简化它的使用。以下代码是上述标签呈现的HTML标记:

<select name="user.name" id="ViewPortfolio_user_name">
    <option value="Mike">Mike</option>
    <option value="Payal">Payal</option>
    <option value="Silas">Silas</option>
</select>

列表中的每一个值都用来创建一个对应的option元素。但是如何预填充呢?由于没有指定value属性,所以会从name属性推断出value属性。如果标签呈现的过程中ValueStack的user.name属性包含了一个值,那么这个值会与所有option元素中的value属性逐一匹配,知道找到相同的值,并将其预选中。上面的情况没有一个选项被预选中,因此user.name属性是没有包含值的。

以下是select UI组件特有的属性:

属性 类型 描述
lisst Collection\ Map\Array\Iterrator 用来生成下拉列表选项的数据集合
listKey String 当list中的元素是复杂类型时,用来指定用作被提交的值的list元素的属性,默认是key
listValue String 当列表中的元素是复杂类型时,用来指定用作选项内容的list元素的属性。换句话说就是用户看到的字符串。默认是value
headerKey String 与标题一起使用,如果用户选择了标题,那么指定提交的值
headerValue String 作为list的标题呈现给用户,例如“States”、“Countries”
emptyOption Boolean 与标题一起使用,在标题和真实选项之间放置一个空的间隔选项
multiple Boolean 用户可以选择多个值
size String 一次显示的选项个数

下面是select标签的一个比较复杂的用例:

<h5>Browse an Artist's Portfolios ( Demo of select component. )</h5>
<s:form action="SelectPortfolio" >
    <s:select name="username" list='users' listKey="username"
        listValue="username" label="Select an artist" />
    <s:submit value="Browse"/>
</s:form>

两个新的属性listKey和listValue是必须的,因为与我们展示的第一个例子不同,支持这个select组件的集合中的数据是复杂的User类型而不是简单的String。listKey属性将对象的一个属性与生成的option元素的value属性绑定,这个属性决定了选项被选中时提交的请求参数值。这里只需一个唯一的标识符即可,显然使用username属性非常适合(已假定username是唯一的)。listValue属性决定了UI下拉列表中用户看到的内容。它相当于生成的option元素包含的字符串。

下面是上述标签生成的HTML代码:

<form id="SelectPortfolio" name="SelectPortfolio"
        action="/manningSampleApp/chapterSeven/SelectPortfolio.action" >
    <select name="username" id="SelectPortfolio_username">
        <option value="Jimmy">Jimmy</option>
        <option value="Chad">Chad</option>
        <option value="Mary">Mary</option>
    </select>
    <input type="submit" id="SelectPortfolio_0" value="Browse"/>
</form>

下面展示了用Map类型的数据实现同样功能的代码:

<h5>Browse an Artist's Portfolios ( Demo of select component. )</h5>
<s:form action="SelectPortfolio" >
    <s:select name="username" list='users' listValue="value.username"
        label="Select an artist" />
    <s:submit value="Browse"/>
</s:form>

需要注意的是遍历Map数据时,遍历的是类型为Entry的对象,而Entry有两个属性key和value,这正好是listKey和listValue的默认值,所以在这里无需指定listKey属性的值,而Entry的value属性对应的是User对象,因此listValue使用value.username的方式从中取的username属性的值。

2.radio组件

radio组件提供了很多与select组件相似的功能,但呈现方式不同。下面是除通用属性外,这个组件的一些常用属性列表:

属性 类型 描述
list Collection\ Map\Array\Iterrator 用来生成单选项的数据集合
listKey String 集合的元素的属性,用来作为提交的值,默认是key
listValue String 集合的元素的属性,用来作为选项的内容,换句话说是用户看到的字符串。默认是value

下面是这个组件在页面上的显示:

radio tag.png

这个标签的使用几乎与select组件完全相同,因此我们不会讲解这个组件的详细内容。

3.checkboxlist组件

checkboxlist组件与select组件也很相似。如下图所示,它呈现了相同的选项,但使用了多选框,因此它允许选择多个选项。

checkboxlist tag.png

此外,checkboxlist组件还经常使用下面的属性:

属性 类型 描述
list Collection\ Map\Array\Iterrator 用来生成复选项的数据集合
listKey String 集合的元素的属性,用来作为提交的值,默认是key
listValue String 集合的元素的属性,用来作为选项的内容,换句话说是用户看到的字符串。默认是value

4.预选中基于集合的组件

value属性指向了ValueStack上的一个属性,这个属性会用作组件的当前值预先选择一个选项。通常不设置value属性,而是让框架根据name属性推断出value属性。每一个HTML的选项都有一个value属性,这个属性表示这个选项被选中时整个组件的值。巧妙的地方是框架使用Struts2标签的value属性的值来匹配某一个选项的值,在匹配时,这个选项被选中。

例如下面的代码,其中defaultUsername中包含了后台传入的默认的用户名chad:

<s:form action="SelectPortfolio" >
    <s:radio name="username" list='users' value="defaultUsername"
            listKey="username" listValue="username" label="Select an artist" />
    <s:submit value="Browse"/>
</s:form>

以上代码最终会输出下面的HTML代码:

<input type="radio" name="username" value="Jimmy"/>
<input type="radio" name="username" value="Charlie Joe"/>
<input type="radio" name="username" checked="checked" value="Chad"/>
<input type="radio" name="username" value="Mary"/>

相同的预选中的过程同样适用于所有基于集合的组件,如果是多重选中的情况,value属性可以指向一个包含多个选择的属性,例如一个包含用户名的数组,那么这个数组会选中这些值中的每一个。

4.额外的组件

1.label组件

label(标签)组件不应该与前面讲解的UI组件生成的标签(label)相混淆。label组件在HTML只输出只读文本,很像一个只读的textfield组件。下面是一个用例:

<s:label name="username" label="Username" />

2.hidden组件

hidden组件用来是实现隐藏域,以下是使用的标记及输出的HTML:

<s:hidden name="username" />

<input type="hidden" name="username" value="Chad"
          id="UpdateAccount_username"/>

3.doubleselect组件

doubleselect(关联下拉列表)组件可实现下拉列表的联动效果,下面是常见的使用方法,其中protfolios即使User对象的属性,而它本神又是一个集合;

<h4>Select a portfolio to view.</h4>
<s:form action="ViewPortfolio">
    <s:doubleselect name="username" list='users' listKey="username"
            listValue="username" doubleName="portfolioName"
            doubleList="portfolios" doubleListValue="name" />
    <s:submit value="View"/>
</s:form>