7.1 类型转换

在所有的基于Web的Java开发框架中,Struts2拥有最优秀的类型转换(type conversion)能力。通常情况下,要利用这种能力,只需要把HTML输入项(表单元素和其他GET/POST的参数)命名为合法的OGNL表达式。

7.1.1 为什么需要类型转换

在Web世界中输入输出是没有数据类型的概念的,任何数据都被当作字符串或字符串数组来传递。如果需要转化为其他类型(如int)进行计算,就要使用Java的转换函数,在本书关于EL的章节中已经介绍过,EL提供了某些转换功能,如String对基本类型的数据的转化。但这样还是不够的,当需要将一个字符串转换成一个更为复杂的对象时,类型转换能发挥强大的作用。例如,如果提示用户使用字符串格式("3,22")输入一个坐标,需要让Struts2完成String到Point和Point到String的转换,Struts2确实提供了这样的功能。下面介绍如何配置和使用Struts2提供的类型转化功能。

7.1.2 定义类型转换器

类型转换器都需要实现ognl.TypeConverter类,而Struts2提供了一个很好的工具类org.apache.struts2. util.StrutsTypeConverter。该类可以让用户很方便地编写处理对象和字符串相互转换的类型转换器,如实例7-1所示。

【实例7-1】实现简单的类型转换:PointConverter.java

public class PointConverter extends StrutsTypeConverter {
    //由字符串转化为对象
    @Override
    public Object convertFromString(Map context, String[] values, Class toClass) {
          Point p = new Point();                               //创建Point类对象
              int x = Integer.parseInt(values[0]);                  //获取坐标的左坐标
              int y = Integer.parseInt(values[1]);                  //获取坐标的右坐标
              p.setLocation(x, y);                             //设置Point类对象坐标
                    return p;                                //返回对象p
    }
    //对象转化为String, 一般调用对象的toString方法即可
    @Override
    public String convertToString(Map context, Object o) {
        Point p = (Point)o;                                    //转换类型p
        return "("+p.x+p.y+")";
    }
}

【代码剖析】在上述代码中,主要实现继承类StrutsTypeConverter的两个方法convertFromString()和convertToString()。

接下来需要配置一下这个转换器,让框架知道什么时候使用这个转换器。在Struts2中配置类型转换器有两种方法:既可以基于全局为每个类来指定一个转换器,也可以基于一个单独类的每个字段来指定。指定全局性的类型转换器需要创建一个struts-conversion.properties文件,并把它放在CLASSPATH的根下。这通常在WEB/INF/classes或者在项目的jar文件根目录下。

java.awt.Point= tag.PointConverter

另一种方法是基于每个类来指定转换器。当想要为一个普通的字段指定转换器而不希望影响全局时,这种方法特别有用。需要创建一个文件名格式为ClassName-conversion.properties的文件,并把它放在和类相同的包中。

point= tag.PointConverter

这个文件和全局配置稍微有点不同。不再是指定一个类型,而是一个字段。对应的这个action类中应该有setPoint和getPoint方法。

注意

类级别的类型转换器遵循正常的类和接口的层次关系规则。也就是说,如果配置父类中定义了类型转换,它的所有子类也会继承到这个类型转换,而不必再配置一遍。

7.1.3 内建的类型转换支持

Struts2可以自动完成大多数常用的类型转换。所以在Struts2中字符串对基本类型(如int)的转化是自动完成的,无须任何配置和代码。已支持的与字符串自动转换的类型包括:

❑ String。

❑ boolean/Boolean。

❑ char/Character。

❑ int/Integer、float/Float、long/Long、double/Double。

❑ dates:使用当前request指定的Locale信息对应的SHORT格式。

❑ arrays:假定每一个字符串都能够转换成对应的数组元素。

❑ collections:如果不能确定对象类型,将假定集合元素类型为String,并创建一个新的ArrayList。

注意

对于数组的类型转换将按照数组元素的类型来单独转换每一个元素。而在其他类型转换中,如果转换无法实现,将使用标准的类型转换错误报告。

7.1.4 参数名称的关系

利用Struts2的类型转换最好的方式是直接装配对象(理想情况下应当直接使用业务对象(domain objects),而不是使用基本类型或字符串类型的表单参数值作为中间值),然后在Action的execute()方法中把这些中间值组装成完整的对象。下面是一些提示:

❑ 使用组合的(complex)OGNL表达式。Struts2能自动创建实际对象。

❑ 使用JavaBeans。Struts2只能创建遵守JavaBean规范的对象,这需要对象提供一个无参数构造函数,并包含适当的getter和setter方法。

❑ 记住person.name将调用getPerson().setName(),但如果希望Struts2创建Person对象,那么必须包含一个setPerson()方法。

❑ 对于list和map对象,使用索引符号,如people[0].name or friends['patrick'].name。通常这些HTML表单元素是在一个循环中绘制出来的,因此可以在JSP Tags中使用iterator标签的状态属性(status attribute)或在FreeMarker Tags中使用${foo_index}来指定这一属性。

❑ 对于多选的列表,显然不能为每个单独的选项使用对应的属性符号来命名。替代的方法是,使用简单的名称people.name 来命名表单元素,Struts2知道需要为每一个选中的选项创建一个新的Person对象并设定它的名字。

7.1.5 空值属性处理

Struts2还有一些非常有用的类型转换特性。对空值(Null)属性的处理可以在发现空值引用时自动创建对象。对Collection和Map的支持提供了针对Java集合的智能空值处理和类型转换。类型转换错误处理提供了一种简单的方法,可以把输入校验问题和输入类型转换问题区别开。

通过把actioncontext中的键值CREATE_NULL_OBJECTS设置为true支持空值处理。这样,出现NullPointerException异常的OGNL表达式将被自动临时中断,然后系统将通过创建所需对象的方法来自动尝试解决空值引用。

处理空值引用时将遵循下列规则:

❑ 如果属性声明为Collection或List,将返回一个ArrayList并赋值给空引用。

❑ 如果属性声明为Map,将返回一个HashMap并给这个Map赋空值。

❑ 如果空值类型是一个有无参数构造函数的简单Bean,将使用ObjectFactory.buildBean ()方法创建一个实例。

例如,如果表单中包含一个名为person.name的文本字段而表达式person运算结果为null,那么该类将被调用。由于表达式person类型为Person,因此将创建一个新的Person实例并赋值给空值引用。最后,name值被赋给该实例的name属性。全部过程是系统自动创建一个Person实例,并调用setPerson()方法将它赋给空值引用,最后调用getPerson().setName(),而这通常是想要的结果。

7.1.6 Collection和Map支持

Struts2支持多种方法来判断集合中的对象类型。这是通过一个ObjectTypeDeterminer完成的。Struts2对这个接口提供了一个默认实现的类DefaultObjectTypeDeterminer。

ObjectTypeDeterminer检查Class-conversion.properties文件中包含的用于表示Map和Collection中包含的对象类型的相关内容。对于Collection(如List)使用格式Element_xxx来指定其中的元素类型,这里xxx是action或其他对象中的集合属性名称。对于Map,需要按照格式Key_xxx和Element_xxx分别指定key和value的类型。如上例中如果有个包含point的list属性,那么conversion文件中应该配置为:

Element_point= tag.PointConverter

类型转换的流程如图7.1所示。

图7.1 类型转换流程图

除此之外,也可以实现接口ObjectTypeDeterminer来创建自己的定制ObjectTypeDeterminer。Struts2也包含一个可选的使用Java5泛型(generics)技术实现的ObjectTypeDeterminer(更多信息参见7.8节)。

还有一大特性是可以根据某个属性的值来获取集合中的唯一元素(类似于数据库中可根据主键直接获取某行数据)。默认情况下,这个属性由Class-conversion.properties中定义的KeyProperty_xxx=yyy决定,这里的xxx是返回集合的JavaBean类型名称,yyy是用于索引集合中元素的属性名称。下面的两个类是一个示例,如实例7-2和实例7-3所示,MyAction中有一个属性是包含foo对象的集合,而Foo类中有一个id字段,这个字段可以作为区分Foo对象的唯一标识(主键)。

【实例7-2】自动获取元素

public Collection getFooCollection()
{
    return fooList;
}

【实例7-3】获取id

public Long getId()
{
    return id;
}

然后将KeyProperty_fooCollection=id放在MyAction-conversion.properties文件中。这样就可以使用fooCollection(someIdValue)从集合fooCollection中获取id等于someIdValue的Foo对象。例如,fooCollection(22)将得到id值为22的Foo对象。

这一点十分有用,因为这直接将一个集合中的元素与它的唯一标识符联系起来,而不需要强制使用索引,从而允许修改一个Bean的集合中的元素而不需要编写额外的代码。例如,值为Phil的参数fooCollection(22).name将集合fooCollection中id属性值为22的Foo对象的name属性设置为"Phil"。

下面用一个完整的实例来说明如何使用自动转化。如实例7-4所示,Tel是用于List中的模型bean,该类的KeyProperty是telNo属性。

【实例7-4】集合中实体类:Tel.java

01     public class Tel {
02          //创建成员变量
03          private String telNo;
04          private String sectionNo;
05          // 属性telNo的getter和setter方法
06          public String getTelNo() {
07                return telNo;
08          }
09          public void setTelNo(String telNo) {
10                this.telNo = telNo;
11          }
12
13          // 属性sectionNo的getter和setter方法
14          public String getSectionNo() {
15                return sectionNo;
16          }
17          public void setSectionNo(String sectionNo) {
18                this.sectionNo = sectionNo;
19          }
20     }

【代码剖析】在上述代码中首先创建了两个成员变量,然后设置这两个成员变量的getter和setter方法。

下面的action有一个tel属性,它是List类型属性,如实例7-5所示。

【实例7-5】包含集合(List)类型属性:TelAction.java

01     public class TelAction implements Action {
02          // List类型的tel属性
03          private List tel;
04          // Tel的getter和setter方法
05          public List getTel() {
06                return tel;
07          }
08          public void setTel(List tel) {
09                this.tel = tel;
10          }
11          // 返回SUCCESS把tel返回到页面上
12          public String execute() throws Exception {
13                return SUCCESS;
14          }
15     }

【代码剖析】在上述代码中首先创建了一个List集合类型的变量tel,然后设置该变量的getter和setter方法。

定义在TelAction-conversion.properties中的内容告诉TypeConverter使用Tel的实例作为List的元素,查询的主键是telNo字段,如果tel属性为空则创建新的list,如实例7-6所示。

【实例7-6】属性文件:TelAction-conversion.properties

Element_tel=cjgong.Tel
KeyProperty_tel=telNo

当通过表单input.jsp提交到Action时,就可以直接通过索引属性telNo来访问集合元素,关于访问的集合在页面result.jsp里,这两个页面具体内容分别如实例7-7和实例7-8所示。

【实例7-7】关于提交页面:input.jsp

01     <%@ page language="java" contentType="text/html; charset=GBK"%>
02     <html>
03          <head>
04               <title>局部类型转换器</title>
05          </head>
06          <body>
07                <form action="login.action" method="post">
08                     <table align="center" style="width:360">
09                          <tr align="center">
10                                <td>
11                                    区号和电话号码用"-"分开
12                                </td>
13                          </tr>
14                          <tr>
15                                <td>
16                                    小区电话:      <!--电话输入框-->
17                                     <input type="text" name="tel" />
18                                </td>
19                          </tr>
20                          <tr>
21                                <td>
22                                    用户电话:      <!--电话输入框-->
23                                     <input type="text" name="tel" />
24                                </td>
25                          </tr>
26                          <tr align="center">
27                               <td>                <!--提交和重置按钮-->
28                                    <input type="submit" value="提交" />
29                                    <input type="reset" value="重置" />
30                                </td>
31                          </tr>
32                     </table>
33                </form>
34          </body>
35     </html>

【代码剖析】在上述代码中,分别创建了两个输入框,一个用来输入小区电话,另一个用来输入用户电话,最后又创建了两个按钮。

【实例7-8】关于集合值的页面:result.jsp

01     <%@ page language="java" contentType="text/html; charset=GBK"%>
02     <%@taglib prefix="s" uri="/struts-tags"%>
03     <html>
04          <head>
05               <title>转换成功</title>
06          </head>
07          <body>
08               <%--调用tel集合属性里索引属性值为86208910的数据--%>
09               三扬区号:
10                <s:property value="tel('86208910').sectionNo" />
11               三扬电话:
12                <s:property value="tel('86208910').telNo" />
13                <br>
14               <%--调用tel集合属性里索引属性值为11111111的数据--%>
15               用户区号:
16                <s:property value="tel('11111111').sectionNo" />
17               用户电话:
18                <s:property value="tel('11111111').telNo" />
19          </body>
20     </html>

【代码剖析】在上述代码中,页面主要显示传递过来的相应信息。

7.1.7 类型转换错误处理

在类型转换发生错误时,有时希望报告这些错误,而有时不希望报告。例如,报告输入的“abc”不能转换成数字可能很重要。另外,报告一个空字符串(“”)不能转换成数字可能不重要。除非是在一个Web环境下,难以区分用户没有输入还是输入了一个空白值。

默认情况下,所有的转换错误使用通用的i18n信息struts.default.invalid.fieldvalue,可以在全局il8n资源包中替换它(默认文本是"Invalid field value for field xxx",这里xxx是字段名称)。

无论如何,有时会希望能够在每个字段上替换这一信息。可以在action相关的资源文件(Action.properties)中添加一个i18n信息:invalid.fieldvalue.xxx,这里xxx是字段名称。

需要知道的是,这些错误不会直接报告出来。它们被添加到ActionContext.conversionErrors中。有几种方法可以访问该map从而可以报告这些错误。

错误报告可以两种方式处理:

1)全局方式,使用Conversion Error Interceptor报告错误。

2)以每一个字段为基点,使用conversion validator报告错误。

默认情况下,conversion interceptor包含在struts2-default.xml的默认截取器栈中,如果不希望使用全局错误报告方式,需要修改截取器栈并添加其他的校验规则。