在实际的开发中的大部分情况,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,从其中取得数据。
本节将讲述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/中。