2013年3月

Struts 2 标签(三)

前两节我们复习了Struts2的数据标签、流程管理标签以及其他标签,本节将学习Struts2的UI组件标签。每一个UI组件都是一个功能单元,用户通过UI组件与应用程序交互、向应用程序输入数据。每一个Struts 2 UI组件的核心都是一个HTML表单控件。但是不要误解,这些组件不仅仅是一个输出HTML输入元素的标签,它们是高层组件,其中的HTML元素只是在浏览器中的表现。UI组件构建在一个分层的、迷你的MVC架构上,这个架构把某个组件的HTML标签的输出分离到了底层的FreeMarker模板中,开发人员可以通过修改这些底层的模板来改变这些组件。

1.为什么需要UI组件标签

新一波的前端界面复杂化的浪潮已然到来,随着时间的流逝,由AJAX和其他富客户端技术支持的动态的前端界面慢慢变成了普通的需求。一个新的web应用程序框架如果不能从框架上考虑管理前端界面日益增加的复杂度,那么它必将走向失败。Struts2 UI组件很好的实现了这个要求。

不仅仅是表单元素

每一个常用的HTML表单元素都有一个对应的Struts 2 UI组件,除了这个一对一的关系,更复杂的UI组件将一对HTML元素包装成一个单元来创建一个新的表单组件。你可以把呈现对应的HTML元素当作是UI组件的基本功能,但最终你会发现,UI组件不仅仅是底层HTML元素的总和。下面是UI组件能够做的一些事情。

  • 生成HTML标记
  • 绑定HTML表单字段和Java端属性
  • 与框架提供的类型转换关联起来
  • 与框架提供的验证关联起来
  • 与框架提供的国际化功能关联起来

1.生成HTML标记

每一个UI组件标签确实生成了一组HTML标记,这些标记定义了对应的HTML元素,并且经常包含一些其他的布局标记,这是组件标签最简单的一个方面。下面是一个简单的示例。:

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

上述标签将会在页面输出如下HTML代码:

<td class="tdLabel">
    <label for="Register_username" class="label">Username:</label>
</td>
<td>
    <input type="text" name="username" value="" id="Register_username"/>
</td>

首先不要被表格标记迷惑,这些是XHTML主题生成的,主题也追加了一些CSS控制用的类属性。如果你喜欢,它甚至能够为这些样式生成一个简单的默认样式表。id属性由包含它的表单的name属性与输入字段的name属性拼接而成。

2.将表单字段绑定到ValueStack的属性

创建HTML元素只是开始。一旦HTML元素生成后,UI组件会将这些生成的元素与Struts2框架提供的丰富的功能连接起来。首先,UI组件会把表单字段绑定到ValueStack的属性,这为数据转移奠定了基础。当你查看这些UI组件标签生成的表单时,它在两个方向上与后台的Java属性关联。首先,ValueStack上的数据能够流向表单来预填充表单。其次,当表单提交时,来源于表单字段的数据会流向框架并自动被转移到ValueStack上。UI组件通过将name属性指定为OGNL表达式将自己同时绑定到ValueStack上的传出和传入属性,以实现表单的预先填充和表单提交时的自动数据转移。

3.与类型转换、数据验证以及国际化集成

对于类型转换和数据验证,在问题发生时UI组件能够自动处理错误消息。类型转换或者数据验证出现问题时会导致请求将用户带回数据表单页面。这时UI组件会自动检测与自己相关的错误信息是否存在,并显示相应的错误信息,当然你也可以自定义和国际化这些错误信息。

UI组件也能够接入国际化机制为UI组件提供本地化的标签名字。这个特性需要使用所有UI组件都有的key属性,只需设置这个属性,UI组件就能够从框架的ResourceBundle中取得本地化的标签,并能智能的推算出name属性和value属性来完成组件的数据绑定。

2. 标签、模板和主题

现在我们来学习UI组件标签独一无二的架构。UI组件的迷你MVC架构最大的好处是可以重用自定义的内容。

UI组件架构.png

UI组件构建在分层的架构上。组件API被公开为页面中的JSP标签,它会被作为JSP标签处理。本地标签仅仅是框架对应的UI组件API的包装,如JSP类型的Struts 2 textfield标签就是对org.apache.struts2.components.TextField组件类的包装。如图,本地标签很快将控制转交给对应的组件API做实际处理。UI组件层处理标签的逻辑并准备数据,之后组件将呈现的任务交给底层的FreeMarker模板。

以这种方式将标签API输出的标记与实现逻辑的Java类分层,这样我们就有了迷你MVC架构。像所有的MVC架构一样,最主要的好处就是能够很容易的修改标签的视图而不需要修改嵌入在标签组件类中的业务逻辑。

2.1 标签

标签非常简单,Struts2标签API是一个高层的API,也就是说,标签的功能和使用定义在给定的视图层技术细节之外,框架提供了标签API在JSP、Velocity、FreeMarker等视图层技术中的实现。不管你使用哪种技术,本地标签都将处理过程委托给UI组件框架,框架管理标签的逻辑和数据模型,数据可能来源于ActionContext或者ValueStack,也可能来源于标签的属性或参数。那些呈现UI组件的HTML标记的模板可以访问这些数据。框架使用FreeMarker模板呈现UI组件。

2.2 模板

每一个标签都有一个与之对应的FreeMarker模板来呈现它的标记。FreeMarker模板看起来很像普通的文本文件,在这些普通的文本内部有一些特殊的FreeMarker指令,它们可以动态地取得数据并能呈现结果输出。对于UI组件标签,模板获得标签逻辑收集的数据模型,将这些数据和模板中的静态部分混合起来创建最终呈现在HTML页面中的标记。

2.3 主题

底层模板为给定标签生成标记是很容易理解的,然而,实际上情况更复杂。实际上,每个标签都准备了几个版本的底层模板。每一个版本属于一个主题。一个主题是一组模板,每一个模板至少属于一个标签,这些模板以某种一致的方式或者习惯呈现这些标签。

有几种方法可以指定使用的主题,默认情况下,所有的标签都使用xhtml主题呈现。可以在整个应用的范围内改变默认主题,也可以在每个页面或者每个标签指定不同的主题。

Struts 2 自带了4个主题来呈现UI组件,它们如下:

  • simple:呈现基本的HTML元素
  • xhtml:使用表格提供的布局呈现UI组件
  • css_xhtml:使用纯CSS提供的布局呈现UI组件
  • ajax:扩展xhtml主题并且提供丰富的ajax组件。

改变主题

所有框架的默认属性都定义在default.properties文件中,这个文件在Struts 2核心的JAR文件的org.apache.struts2包内。覆盖框架的默认属性,只需创建自己的属性文件,命名为struts.properties,并把它放在类路径中。要改变默认主题,只需添加如下内容,UI组件就会使用css_xhtml来呈现。

struts.ui.theme=css_xhtml

Struts 2 标签(二)

本节将学习Struts2标签库的详细内容,包括数据标签,流程控制标签和其他标签。

1.数据标签

数据标签能从ValueStack上取得数据,或者将变量、对象放在ValueStack上。本小节将讨论property、set、push、bean和action标签,并演示这些标签的常见用例。

1.1 property标签

property标签提供了一种将属性写入呈现的HTML页面的快速、方便的方法。下面是这个标签的重要属性:

属性 是否必须 默认值 类型 描述
value 栈最顶端 Object 被显示的值
default 空(null) String 值为空时使用的默认值
escape true Boolean 是否转义HTML

实际的使用很简单,代码如下:

<h4>Property Tag</h4>
The current user is <s:property value="user.username"/>.

1.2 set标签

在标签的上下文中,set意味着给一个属性指定一个别名。有时候一个属性需要使用一个很长的、复杂的OGNL表达式才能引用到它,通过为这个属性重新指定一个顶层的名字可以更容易、更快捷的访问它。默认情况下,这个属性会变成ActionContext中的一个命名对象,与ValueStack、会话映射齐名。所以你可以使用如#myObjec这样的OGNL表达式把它作为一个顶层的命名对象引用。当然,你也可以指定这个新引用被放置在ActionContext中的某个作用域中。下面是set标签的属性:

属性 是否必须 类型 描述
name String 被设置在指定作用域内的变量的引用名
scope String application、session、request、page或者action,默认是action
value Object 设置值的表达式

下面是实际的使用:

<s:set name="username" value="user.username"/>
Hello, <s:property value="#username"/>. How are you?    

以下代码经把新引用设置为application作用域映射内的一个实体:

<s:set name="username" scope="application" value="user.username"/>
Hello, <s:property value="#application['username']"/>. How are you?

1.3 push标签

set标签允许你创建指向值的新引用,而push标签允许你把属性放到ValueStack上。当你需要对一个对象进行很多操作时这个标签会很有用。将对象放在ValueStack顶端之后,它的所有属性可以通过第一级别的OGNL表达式访问。

属性 是否必须 类型 描述
value Object 放到ValueStack中的值

以下是使用这个标签的示例:

<s:push value="user">
    This is the "<s:property value="portfolioName"/>" portfolio,
    created by none other than <s:property value="username"/>
</s:push>

push标签把user属性放到ValueStack的顶端,这是我们就可以把user的属性当作ValueStack这个虚对象上的顶级属性访问,这样OGNL表达式就变简单了。最后push的结束标签把user从ValueStack顶端删除。

1.4 bean标签

bean标签像是set标签和push标签的混合标签。主要的不同是不需要使用一个已经存在的对象,bean标签会自己new这个对象,并把它放到ValueStack上或者设置为ActionContext的顶级引用。默认是将对象放在ValueStack上,并在标签存在期间一直保留在ValueStack上。以下是bean标签的属性:

属性 是否必须 类型 描述
name String 被传入bean的包名和类名
var String 在结束标签作用域外引用这个bean时使用的变量名

下面示例展示了如何创建和存储一个bean作为ActionContext中的命名参数:

<s:bean name="org.apache.struts2.util.Counter" var="counter">
    <s:param name="last" value="7"/>
</s:bean>
<s:iterator value="#counter">
    <li><s:property/></li>
</s:iterator>

默认情况下创建的对象会存储在ValueStack中,使用var后会在ActionContext中存储这个对象的引用,正是因为如此,在iterator标签中,我们使用它的方式需要用#操作符,即OGNL表达式#counter。在许多Struts2标签中,都存在可选的var属性,它的作用与此类似。

bean标签是我们研究的第一个参数化标签。现在看看如何使用bean标签将新创建的bean放在ValueStack上而不是存储在ActionContext上。而且我们我们还是用param标签演示怎样向自定义对象提供参数。

<s:bean name="manning.utils.JokeBean" >
    <s:param name="jokeType">knockknock</s:param>
    <s:property value="startAJoke()"/>
</s:bean>

上面我们使用了OGNL方法调用语法starAJoke(),bean标签不需要完全遵守JavaBean标准,但是OGNL能够使用这个方法,我们将在OGNL详解中讲解它的机制。

最后,注意我们向JokenBean传递了一个参数,这个参数用来控制这个bean讲述的笑话的类型。只要这个对象实现了一个与这个参数同名的属性,它就能自动接收传入的参数。

1.5 action标签

这个标签允许我们从当前的视图层调用其他的动作。action标签的实际使用很简单,只需指定其他需要调用的动作。

属性 是否必须 类型 描述
name String 动作名
namespace String 动作的命名空间,默认使用当前页面的命名空间
var String 在页面后续代码中使用的动作对象的引用名
executeResult Boolean 设置为true时排除动作的结果(默认值为false)
flush Boolean 设置为true时在action标签的结尾会刷新写出缓冲(默认为true)
ignoreConte xtParams Boolean 设置为true时动作被调用时不包含请求参数(默认值为false)

以下是一个选择包含辅助动作结果的示例:

<h3>Action Tag</h3>
<h4>This line is from the ActionTag action's result.</h4>
<s:action name="TargetAction" executeResult="true"/>

通常情况下,你希望辅助动作被触发,但不写结果,而是提供一个在ActionContext上某个地方存储域数据,在控制返回后,主动作可以访问这些数据,下面示例正是如此:

<h4>This line is before the ActionTag invokes the secondary action.</h4>
<s:action name="TargetAction"/>
<h4>Secondary action has fired now.</h4>
<h5>Request attribute set by secondary action = </h5>
<pre> <s:property value="#request.dataFromSecondAction"/></pre>

2.控制标签

Struts2中有一系列的标签可以容易的控制页面执行的流程,包括iterator标签和if/else标签等。

2.1 iterator标签

使用iterator表亲啊可以很容易的便利集合对象。下面是这个标签的属性:

属性 是否必须 类型 描述
value Object 被遍历的对象
status String 如果被指定,IteratorStatus对象会使用这个属性指定的名字被放在ActionContext中

下面是一个使用示例:

<s:iterator value="users" status="itStatus">
    <li>
    <s:property value="#itStatus.count" />
    <s:property value="portfolioName"/>
    </li>
</s:iterator>

上面的代码很直观,在标签内部,每一个对象会轮流被放在ValueStack顶端,我们可以很容易的访问用户属性。注意,iterator标签通过指定status属性,也声明了IteratorStatus对象。你为status属性指定的名字会用来作为关键字从ActionContext中取得遍历状态对象。

使用IteratorStatus

有时候我们非常想知道当前发生的循环的状态信息。如果定义了status属性,那么会在ActionContext中提供一个IteratorStatus对象,这个对象可以提供诸如集合大小、当前索引、当前对象的索引是奇数还是偶数的简单信息。下面是IteratorStatus的共有方法:

方法名 返回值
getCount int
getIndex int
isEven boolean
isFirst boolean
isLast boolean
isOdd boolean
modulus(int operand) boolean

2.2 if和else标签

很多语言都提供了熟悉的if和else控制逻辑,Struts2标签亦是如此。使用这些标签很容易,它们都只有一个属性,即布尔型test:

属性 是否必须 类型 描述
test Boolean 被求值和测试的布尔表达式

以下是如何使用这个标签的示例:

<s:if test="user.age > 35">This user is too old.</s:if>
<s:elseif test="user.age < 35">This user is too young</s:elseif>
<s:else>This user is just right</s:else>

3.其他标签

这一节将讨论Struts2的inclued标签、URL标签、i18n标签和text标签,最后了解下详细的param标签的使用。这些标签很有用,但是不太容易分类。

3.1 inclued标签

虽然JSP有自己的包含标签<jsp:include>,但是Struts2提供了一个与Struts2集成的更好、有更多高级特性的include标签。简而言之,这个标签可以执行Servlet API样式的包含。下面是其唯一的属性:

属性 是否必须 类型 描述
value String 页面、动作、Servlet以及其他可以被引用的URL的名字

这个标签很简单,无须展示例子了吧。include标签的行为很想JSP的include标签,但是它与框架集成的更好并且提供了对ValueStack(使用%{...}从ValueStack取值)和可扩展参数模型的本地访问(使用<s:param>标签传递查询字符串参数)。

3.2 URL标签

Struts2提供了URL标签来帮助管理URL,这个标签支持与URL相关的所有操作,从控制参数到Cookie缺失时自动持久化会话。下面是它的属性:

属性 是否必须 类型 描述
value String 基础URL,默认为呈现当前页面的URL
action String 生成的URL指向的动作名,使用声明性架构中配置的动作名(不带.action扩展名)
var String 如果指定,这个URL不会被重写,而会存储在ActionContext留待后用
includeParams String 从all、get、none中选择参数,默认值为get
includeContext Boolean 如果为true,那么生成的URL会使用应用程序的Context作为前缀,默认值为true
encode Boolean 如果用户浏览器不支持Cookie,会将session ID追加到生成的URL中
scheme String 指定协议,默认使用当前协议(HTTP或HTTPS)

下面是两个示例:

URL = <s:url value="IteratorTag.action"/>
<a href='<s:url value="IteratorTag.action" />'> Click Me </a>

URL标签只是把生成URL输出为字符串,下面是页面输出:

URL = IteratorTag.action
<a href='IteratorTag.action'> Click Me </a>

如果我们想指向一个动作,那么应该使用action属性:

URL = <s:url action="IteratorTag" var="myUrl">
                <s:param name="id" value="2"/>
            </s:url>
<a href='<s:property value="#myUrl" />'> Click Me </a>

这样页面输出会是这样:

URL =
<a href='/manningHelloWorld/chapterSix/IteratorTag.action?id=2'>
    Click Me
</a>

3.3 i18n和text标签

这两个标签被用在国际化功能中,它们使用name属性指定关键字,通过这个关键字取得对应的文本消息。

下面是text标签支持的属性:

属性 是否必须 类型 描述
name String 在ResourceBundle中查找用的关键字
var String 如果找到,文本会使用这个名字保存在ActionContext中

如果你想手动指定应该使用的ResourceBundle,可以使用i18n标签,下面是i18n的属性:

属性 是否必须 类型 描述
name String ResourceBundle的名字

下面是一个示例,它展示了如何使用i18n标签设置ResourceBundle,以及如何使用text标签从中取出文本消息:

<s:i18n name="manning.chapterSix.myResourceBundle_tr">
    In <s:text name="language"/>,
    <s:text name="girl" var="foreignWord"/>
</s:i18n>
"<s:property value="#foreignWord"/>" means girl.

3.4 param标签

param标签什么也不做,但它是最重要的标签之一。它不仅在上述标签中起重要作用,而且在许多UI组件标签中也起重要作用。下面是它的属性:

属性 是否必须 类型 描述
name String 参数名
value Object 参数值

在之前的例子中已经展示了param标签的作用,包括作为一种自定义的工具对象传递参数的方法。只要有了这个整体思路,剩下的就是细读标签API看看哪个标签可以接受参数。

Struts 2 的数据转移和类型转换机制(二)

本节将讲述Struts2内建的类型转换器的具体细节。通过配置,Struts2框架能够处理几乎所有你可能需要的类型转换。

1.内建的类型转换器

1.1 立即可用的类型转换器

Struts2框架自带了对HTTP本地字符串和以下列出的Java类型之间转换的内建支持。

■ String—有时候字符串就是字符串。
■ boolean/Boolean—true和false字符串可以被转换为Boolean的原始类型和对象类型。
■ char/Character—原始类型或者对象类型。
■ int/Integer, float/Float, long/Long, double/Double—原始类型或者对象类型
■ Date—当前Locale的SHORT格式的字符串版本 (例如,12/10/97)。
■ array—每一个字符串元素必须能够转换为数组的类型。
■ List—默认情况下使用String填充。
■ Map—默认情况下使用String填充。

当框架定位到一个给定的OGNL表达式指向的Java属性时,它会查找这个类型的转换器。如果这个类型在前面的列表中,你不需要任何事情,等着接收数据即可。

1.2 使用OGNL表达式从表单字段名映射到属性

下面将按照上述类型转换器列表讲述每一个内建的类型转换器如何在两端建立对等的内容。

1.原始类型和包装类

指向ValueStack上特定属性的OGNL表达式例子:

<h4>Complete and submit the form to create your own portfolio.</h4>
<s:form action="Register">
    <s:textfield name="user.username" label="Username"/>
    <s:password name="user.password" label="Password"/>
    <s:textfield name="user.portfolioName" label="Enter a name "/>
    <s:textfield name="user.age" label="Enter your age as a double "/>
    <s:textfield name="user.birthday" label="Enter birthday. (mm/dd/yy)"/>
    <s:submit/>
</s:form>

以下是公开的User对象的JavaBean属性:

private User user;
public User getUser() {
    return user;
}
public void setUser(User user) {
    this.user = user;
}

而User对象也公开了相应的属性:

public class User {
    private String username;
    private String password;
    private String portfolioName;
    private Double age;
    private Date birthday;
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    ......
}

注意类型转换和验证之间的区别,以及类型转换错误和验证错误之间的区别。验证代码用来从动作的业务逻辑的视角验证数据是否是数据类型的一个合法实例,它通过validation拦截器或者workflow拦截器对validate()方法的调用而触发。类型转换在将HTTP字符串值绑定到Java类型时发生,如在param拦截器转移请求数据时发生。

类型转换错误导致用户会被返回到输入页面,并展示一个默认的错误信息告知用户,你可以自定义类型转换问题的错误报告,将在以后章节学习。

2.处理多值请求参数

多个参数值可以被映射到传入请求中的一个参数名,一个表单有多种方式在一个参数名下提交多个值,也有多种方法将它们映射到Java端的类型,这意味着使用OGNL有很多种方式。Struts2提供了丰富的支持以便将多值的请求参数转移到Java端各种面向集合的数据类型,从数组到真正的Collection。下面将通过展示两端的示例说明怎样使用它。

3.数组

为数据转移指向数组属性(使用索引和不使用索引):

<s:form action="ArraysDataTransferTest">
    <s:textfield name="ages" label="Ages"/>
    <s:textfield name="ages" label="Ages"/>
    <s:textfield name="ages" label="Ages"/>

    <s:textfield name="names[0]" label="names"/>
    <s:textfield name="names[1]" label="names"/>
    <s:textfield name="names[2]" label="names"/>
    <s:submit/>
</s:form>

OGNL输入字段名指向的数组属性:

private Double[] ages ;
public Double[] getAges() {
    return ages;
}
public void setAges(Double[] ages) {
    this.ages = ages;
}
private String[] names = new String[10];
public String[] getNames() {
    return names;
}
public void setNames(String[] names) {
    this.names = names;
}

注意这些属性不需要带索引参数的获取方法和设置方法,OGNL处理所有与索引相关的细节,我们只需通过一对获取方法和设置方法公开数组。

请求转发的结果页面代码:

<h5>Congratulations! You have transferred and converted data to and from Arrays.</h5>
<h3>Age number 3 = <s:property value="ages[2]" /> </h3>
<h3>Name number 3 = <s:property value="names[2]" /> </h3>

3.List

使用List与使用数组几乎完全相同,仅有的不同是在Java 5之前List不支持类型指定,List没有类型的特性对Struts 2的类型转换机制有着重要的影响。对List来说,没有方法能够自动发现内部元素的类型。使用List时用两种选择,为元素指定类型或者接受默认类型(String)。接受默认行为时页面代码和使用数组相同,不同的是在Java端,没有类型说明,List中元素都会是String类型。

有些时候,除了使用String,你会想为List中的元素指定类型。这种情况下,我们只需告知OGNL某个给定属性我们希望的元素类型,这使用一个简单的属性文件实现。

为了给动作对象上的List属性指定元素类型,我们根据下图中的命名约定创建一个文件。

List指定类型命名约定.png

文件名约定是:动作名+-+conversion+属性文件扩展名,其中conversion是类型转换用的文件名后缀。之后把这个文件放在Java包中这个类的旁边。文件的内容约定如下图:

List指定类型内容约定.png

文件内容约定是:Element+_+List类型属性的名字=元素类型(如:Element-weights=java.lang.Double),这样List属性会像数组属性一样工作,每一个独立元素都会被转换为指定的元素类型。例如页面可以这样写:

<s:textfield name="weights[0]" label="weights"/>
<s:textfield name="weights[1]" label="weights"/>
<s:textfield name="weights[2]" label="weights"/>

当然也可以不使用索引,这取决于你的偏好,在Java端,动作对象上的属性没有变化。需要记住的一点就是在给List或者其他Collection指定类型时注意不要预先初始化List。

private List weights;
public List getWeights() {
    return weights;
}
public void setWeights(List weight) {
    this.weights = weight;
}

下面是一个更全的功能的示例

页面代码:
<s:textfield name="users[0].username" label="Usernames"/>
<s:textfield name="users[1].username" label="Usernames"/>
<s:textfield name="users[2].username" label="Usernames"/>

动作属性代码:
private List users ;
public List getUsers(){
    return users;
}
public void setUsers ( List users ) {
    this.users=users;
}

指定类型的属性文件内容:
Element_users=manning.utils.User

5.Map

Map使用关键字而非索引来关联到它的值,使用关键字的特性对Struts2类型转换过程有两点含义。一是引用它们的OGNL表达式语法与List不同;而是与为Map属性指定类型相关。对Map来说,可以为关键字对象指定类型,如果不指定默认是String。

首先看一下页面代码,Java端代码和List的相同:

<s:textfield name="maidenNames.mary" label="Maiden Name"/>
<s:textfield name="maidenNames.jane" label="Maiden Name"/>
<s:textfield name="maidenNames.hellen" label="Maiden Name"/>
<s:textfield name="maidenNames['beth']" label="Maiden Name"/>
<s:textfield name="maidenNames['sharon']" label="Maiden Name"/>
<s:textfield name="maidenNames['martha']" label="Maiden Name"/>

现在在看一下指定类型的示例:

指定类型属性文件内容
Element_myUsers=manning.utils.User

页面代码:
<s:textfield name="myUsers['chad'].username" label="Usernames"/>
<s:textfield name="myUsers['jimmy'].username" label="Usernames"/>
<s:textfield name="myUsers['elephant'].username" label="Usernames"/>
<s:textfield name="myUsers.chad.birthday" label="birthday"/>
<s:textfield name="myUsers.jimmy.birthday" label="birthday"/>
<s:textfield name="myUsers.elephant.birthday" label="birthday"/>

Java端代码:
private Map myUsers ;
public Map getMyUsers(){
    return myUsers;
}
public void setMyUsers ( Map myUsers ) {
    this.myUsers=myUsers;
}

前面已经说了,除了为元素指定类型,使用Map属性时也可以为关键字对象指定类型。与值一样,OGNL也会把参数的名字当作应该尝试转换为指定类型的字符串。假如我们想使用Integer作为关键字的myUsers版本。应该在属性文件内这样写:

Key_myOrderedUsers=java.lang.Integer
Element_myOrderedUsers=manning.utils.User

以下是提交表单页面:

<s:textfield name="myOrderedUsers['1'].birthday" label="birthday"/>
<s:textfield name="myOrderedUsers['2'].birthday" label="birthday"/>
<s:textfield name="myOrderedUsers['3'].birthday" label="birthday"/>

最后,如果使用Java5或更高版本,可以使用泛型来类型化Collection和Map,在类型转换时Struts2可以使用这些信息,使得属性文件配置不再必要。

2.自定义类型转换

虽然内建的类型转换器功能强大且涵盖面广,但有时我们还是需要构建自定义的类型转化器。你可以指定一个转换逻辑将任何字符串翻译为任何Java类型。唯一需要做的就是创建字符串语法和对应的Java类,之后是使用一个类型转换器把他们连接起来。

2.1 实现类型转换器

类型转换器是OGNL的一部分,因此类型转换器必须实现ognl.TypeConverter接口。一般情况下,OGNL的类型转换器能实现任何两种数据类型之间的转换。在Web应用程序领域,我们只需实现所有Java类型和HTTP字符串之间的转换即可。

Struts2提供了一个方便的基类:org.apache.struts2.util.StrutsTypeConverter作为自定义类型转换器的扩展点。下面代码是这个类所定义的抽象方法:

public abstract Object convertFromString(Map context, String[] values,Class toClass);
public abstract String convertToString(Map context, Object o);

第一个方法经请求转换为Java对象,第二个方法将Java对象转换为请求字符串。当你编写一个自定义的转换器时,你只需继承这个类并将自己的逻辑填充到这两个方法中即可。

2.2 在字符串和Java对象间转换

自定义类型转换逻辑.png

以下代码实现了上图所示的转换规则:

public class CircleTypeConverter extends StrutsTypeConverter {
    public Object convertFromString(Map context, String[] values,Class toClass) {
        String userString = values[0];
        Circle newCircle = parseCircle ( userString );
        return newCircle;
    }
    public String convertToString(Map context, Object o) {
        Circle circle = (Circle) o;
        String userString = "C:r" + circle.getRadius();
        return userString;
    }
    private Circle parseCircle( String userString ) throws TypeConversionException {
        Circle circle = null;
        int radiusIndex = userString.indexOf('r') + 1;
        if (!userString.startsWith( "C:r") )
            throw new TypeConversionException ( "Invalid Syntax");
        int radius;
        try {
            radius = Integer.parseInt( userString.substring( radiusIndex ) );
        } catch ( NumberFormatException e ) {
            throw new TypeConversionException ( "Invalid Value for Radius"); 
        }
        circle = new Circle();
        circle.setRadius( radius );
        return circle;
    }
}

2.3 配置框架使用自定义转换器

构造了自定义转换器之后,必须让框架知道什么时候、在哪里使用它。这里有两种选择,可以配置转换器用在一个给定动作的局部或者全局。前者将只告诉框架在处理特定动作的特定属性时才使用转换器,而后者在应用程序的任何地方,每次通过OGNL设置或者取得一个特定属性时都会使用它。

1.属性专用

与前面Map和List指定元素类型相同,在同样的文件中再添加以下内容:

circle=manning.utils.CircleTypeConverter

这一行简单的内容将属性名circle和类型转换器关联起来。现在当OGNL想给Circle属性设置值时,它会自动使用我们定义的类型转换器。

2.全局类型转化

我们可以指定所有的Circle类型的属性都使用我们的转换器,这个过程和之前局部指定仅有一点不同:在定义全局类型转换器时,不使用动作名+-+conversion+属性文件扩展名的约定文件,而是使用xwork-conversion.properties,并且文件内容修改为如下:

manning.utils.Circle=manning.utils.CircleTypeConverter

此外,还需将xwork-conversion.properties文件放在类路径下,例如在WEB-INF/classess/中。

Struts 2 标签(一)

在讲解Struts2标签库之前,本节将首先关注数据通过Struts2标签API离开框架的环境下的OGNL表达式语言。我们会浏览OGNL表达式的语法,并研究它可以取出数据的位置,尤其要深入研究ValueStack和ActionContext。

1.入门

当一个请求到达框架时,Struts2首先要做的事情是创建存储请求的所有重要数据的对象。应用程序的特定领域数据(使用标签最常访问的数据)会存储在ValueStack中,而另一些更基础的数据也必须存储起来,所有的这些数据连通ValueStack都存储在一个叫做ActionContext的对象中。

1.1 ActionContext和OGNL

实际上,OGNL表达式可以根据任何一系列对象求值,而ValueStack只是这些对象中的一个,默认的那个。OGNL可用来求值的更宽泛的一系列对象叫做ActionContext。ActionContex包含了框架的请求处理过程可以访问的所有数据,包含内容从应用程序数据到会话作用域或者应用程序作用域的映射。默认情况下OGNL解析会选择ValueStack,但你可以指定其他的对象的名字,例如会话作用域。

下图展示了ActionContext和它包含的对象,可以把OGNL解析指向其中的任何对象:

ActionContex包含的对象.png

  • parameters:当前请求中请求参数的映射
  • request:请求作用域的属性的映射
  • session:会话作用域的属性的映射
  • application:应用程序作用域的属性的映射
  • attr:按照page、request、session、application作用域的顺序,返回第一个出现的属性
  • ValueStack:包含当前请求的应用程序特定领域的所有数据

每一个OGNL表达式的解析都需要一个根对象,有了它才能开始引用的解析。现在让我们看看如何选择ActionContext中的对象让OGNL基于此对象解析。

为OGNL选择根对象

OGNL表达式可以以一个特殊的语法开始,从上下中命名一个解析表达式所根据的对象:

#session['user']

OGNL表达式通过表达式语言的#操作符主动命名ActionContext中的会话映射。#操作符告诉OGNL表达式使用位于上下文中的命名对象作为初始对象解析表达式的剩余部分。

1.2 虚拟对象ValueStack

当Struts2接收到一个请求时,它立即创佳一个ActionContext、一个ValueStack和一个动作对象。作为应用程序数据的承载者,动作被马上放在ValueStack中,以便框架可以通过OGNL访问它的属性。

ValueStack只有一个微妙的地方,当OGNL表达式根据ValueStack解析时,它装作一个对象。这个虚拟对象包含放在上面的所有对象的所有属性。如果相同的属性出现多次,栈下面对象的属性会被栈上层的同名属性覆盖。

ValueStack是OGNL表达式默认解析对象.png

如上图所示,一个给定属性的引用的解析指向栈中最高位置出现的属性。以name为例,ValueStack总会返回对象栈中出现在最顶层的name属性。在上述的情况下,动作对象有一个name属性,但由于上层模型对象也有一个name属性,所以动作对象中的name属性永远不能被访问到。

2. Struts2标签概要

Struts2标签API通过使用条件呈现以继承ValueStack上应用程序的域模型数据,提供了动态创建健壮的网页功能。这些标签可分为4类:数据标签、流程控制标签、UI标签和其他标签。现在我们先不看UI标签,先研究另外3种标签。

2.1 Struts 2标签API语法

Struts2标签定义在比任何具体的视图层技术更高的层。使用这些标签与调用API一样简单,标签API指定了标签公开的属性和参数。标签API的接口已经全部在(JSP、Velocity和FreeMarker)中实现。

1.JSP语法

Struts2标签的JSP版本与其他JSP标签一样。异侠是property标签的简单语法:

<s:property value="name"/>

其他唯一需要说明的是,在使用Struts 2标签之前,在页面的开始部分必须声明包含property标签库的声明:

<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>

2.2 使用OGNL设置标签属性

向标签的属性设置值时,这个属性期望一个字符串字面值,还是一个指向ValueStack上有具体类型的值的OGNL表达式呢?下面我们就讲解这个问题。

在JSP页面中,所有的内容都是字符串,我们应该很熟悉使用某种转义字符序列来强制这些属性作为表达式解析而不是字符串字面值解析,例如EL表达式的写法${expression}的写法。但是这样会导致标签的边界难于阅读,导出都是EL表达式。为了让OGNL在标签中使用更加直观、可读性更强,Struts2会假定每一个属性都期望得到什么类型的数据,尤其在处理标签的属性时,会区分谁的类型是String,谁的类型不是String。

1.字符串和字符串类型的属性

如果一个属性的类型是String,那么在真实的JSP页面中,写入属性的值会被作为字符串字面值解析。如果一个属性不是String类型,那么属性中写入的值会被当作OGNL表达式解析。

例如下面两个标签,value的值会默认作为OGNL表达式解析,而default的值则会作为字符串解析。

nonExistingProperty on the ValueStack = <s:property value="nonExistingProperty" />
nonExistingProperty on the ValueStack = <s:property value="nonExistingProperty" default="doesNotExist" />

2.强制使用OGNL解析

但是,假如前面的例子,我们想使用一个来源于ValueStack的String属性作为标签的default属性值。这时候就需要强制使用OGNL解析的方式了,写法如下,有些像EL表达式,但不同的是使用%:

nonExistingProperty on the ValueStack =<s:property value="nonExistingProperty" default="%{myDefaultString}" />

Struts 2 的数据转移和类型转换机制(一)

在实际的开发中的大部分情况,Struts2框剪已经非常好的自动完成了数据转移和类型转换任务。然而若想进一步提高我们的能力,花一点时间和精力来学习数据转移和类型转换究竟是如何工作的将是必要的。也许你已经学会了在简单的情况下如何利用自动数据转移,然而在面临更加复杂的Java端类型(例如Map和List)时,将怎样编写代码呢?本节内容正是要解答上述疑问的。

1.数据转移和类型转换:Web应用程序领域的常见任务

Web应用程序领域的一个常见任务是从基于字符床的HTTP向Java语言的不同类型移动和转换数据。将字符床解析为double或者float,捕获坏数据抛出的异常,这些任务没有一点意思。更糟的是,这些任务实际上是纯基础设施。

数据转移和类型转换实际上发生在请求处理周期的两端。几乎Web应用程序中的每一个请求都会发生这个过程,它是这个领域与生俱来的部分。大部分时候我们会将这个责任交给框架,然而有些时候我们会想扩展或者配置这个自动化支持。Struts2类型转换机制功能强大并且特别容易扩展。

1.1 OGNL和Struts2

我们还没有解释所有这些数据如何从HTTP请求到Java语言,以及如何再通过JSP标签回到HTML。下面的内容将阐明这个神秘的过程。

1.1.1 OGNL是什么

OGNL是Object-Graph Navigation Language(对象图导航语言)的简称。这听起来有些让人恐惧,似乎我们在学校里学的还不够,它听起来太学术味了。OGNL是一种强大的技术,它被集成在Struts2框架中用来帮助实现数据转移和类型转换。从开发人员基于Struts2框架构建应用程序的角度看,OGNL包含两件事:表达式语言和类型转换器。

1.表达式语言

我们已经在表单输入字段的name属性和JSP标签中使用过OGNL表达式语言了。这这两个地方,我们使用OGNL表达式将Java端的数据属性和基于文本的视图层中的字符串绑定起来。

<h5>Congratulations! You have created </h5>
<h3>The <s:property value="portfolioName" /> Portfolio</h3>

OGNL表达式语言是value属性双引号之间的片段,Struts2 property标签从对象的属性中取值,然后将它写入到HTML中代替这个标签,这是表达式语言的要点。表达式语言允许我们使用简单的语法来引用Java环境中存在的对象。

注:OGNL转义序列%{表达式}可用来告诉框架什么时候把表达式当成OGNL表达式而不是作为字符串面值解析;在默认情况下,框架敬爱那个自动把字符串当作OGNL表达式求值。

2.类型转换

基于字符串的HTML世界和框架的本地Java类型之间移动数据时类型转换是如何发生的呢?除了表达式语言,我们也一直在使用OGNL类型转换器。数据转移时类型转换必定发生,即使是两端类型(是字符串时)相同,这仅仅意味着类型转换比较容易。

1.1.2 OGNL如何融入框架

数据进入和离开框架时,数据在不同的区域间移动时,OGNL如何帮助绑定和转换数据呢?下图展示了整个过程。

数据转移和类型转换过程.png

1.数据进入

用户输入名字和年龄,并提交了表单,数据的旅程就开始了。当数据进入框架后,它作为一个HttpServletRequest对象公开给Java语言。Struts2经请求参数作为名/值对存储,名和值都是String类型。接下来框架开始处理这些参数的数据转移以及类型转换。

如图所示,在开始时,Struts2就会将动作对象置于叫做ValueStack的对象上,而User对象作为动作的组件的JavaBean属性也被公开出来。而另一方面,当用户发出请求时,params拦截器会把请求对象中的数据转移到ValueStack上。于是,用户提交的数据属性和Struts2公开的属性就会在ValueStack上出现重复。此时神奇的事情发生了,接下来就是见证奇迹的时刻:

ValueStack是一个Struts2结构,它呈现了一堆对象属性的聚合。如果有重复属性存在,那么栈中最高的对象的属性会是由ValueStack代表的虚拟对象公开的属性。在上述情况下,由于Struts2公开属性在前,而用户提交数据在后,在栈中用户提交的数据更高,于是数据就自动找到了转移到Struts2公开的对象的属性上的道路。

在使用OGNL表达式定位到目标属性之后,可以通过使用正确的值调用属性的set方法把数据移动到这个属性上。这是类型转换就开始工作了。我们需要把字符串转换为OGNL指向的age属性的Java类型。OGNL会咨询它可用的类型转换器的集合一确定是否他们中某个可以处理这个特定的转换。

2.数据流出

与数据进入相反,在动作完成自身的业务、调用业务逻辑、做数据操作后,某个最终结果会触发,它会向用户呈现一个新的应用程序试图。在这个过程中,数据对象会一直保留在ValueStack上。当结果开始自己的呈现过程时,它也通过标签中的OGNL表达式语言访问ValueStack,从其中取得数据。