深入理解Struts2----类型转换

时间:2021-09-27 03:58:56

     之前的一系列文章主要介绍了有关Struts2的一些基本用法和部分的简单原理,但是始终没有介绍有关拦截器的相关内容,从本篇开始我们将从另一个角度去深入理解框架的使用,核心还是拦截器,但本篇首先来介绍下有关框架中类型转换的相关内容。主要包含以下几小节:

  • 类型转换的使用场景
  • Struts2内默认转换器
  • 基于OGNL的类型转换
  • 自定义类型转换
  • 注册类型转换器
  • 类型转换的错误处理

一、类型转换的使用场景

     何谓类型转换?类型转换就是指我们在客户端使用GET/POST或者action标签的转发到某个具体的Action实例中的时候,我们传入的参数可以自动转换为Action实例的实例属性的值的一个过程。下面看一个熟悉的例子,回顾下:

//login登录页面
<body>
<form method="post" action="login">
姓名:<input type="text" name="username" /><br />
密码:<input type="password" name="password" /><br />
<input type="submit" value="提交"/>
</form>
</body>
//login
public class LoginAction extends ActionSupport {
private String username;
private int password;
public void setUsername(String name){
this.username = name;
}
public String getUsername(){
return this.username;
}
public void setPassword(int pa){this.password = pa;}
public int getPassword(){
return this.password;
} public String execute() throws Exception{
return SUCCESS;
}
}
//index页面
<body>
<h1>this is the index page</h1>
<p><s:property value="username"/></p>
<p><s:property value="password"/></p> <br/>
<s:debug/>
</body>

我们首先在login页面输入两个表单的数值,然后转发到LoginAction ,执行execute方法,拉取index页面的内容,输出结果如下:

深入理解Struts2----类型转换

我们虽然没有显式的为LoginAction 的两个实例属性赋值,但是在index页面中我们依然可以获取到该属性的值,他们的值对应于login表单页面提交过来的值,也就是说从表单页面提交到LoginAction 的时候会自动根据名称传值。这就是类型转换,从表单页面的String类型转换为LoginAction 中对应的属性的类型,但是这种自动转换并不是总是生效的,具体我们接着看。

二、Struts2内默认转换器

     表单中所有输入的值都将作为String类型提交到相应的Action,至于如何将这些String类型转换为Action中的属性的类型是需要做一些判断的,Struts2中默认有一个类型转换器,可以帮助我们完成大部分的自动转换操作。其支持的从String类型转换的目标类型如下:

  • boolean和Boolean:字符串true会转换为布尔类型值true
  • char和Character:字符串转字符
  • int和Integer:字符串转整型类型
  • long和Long:字符串转长整型
  • float和Float:字符串转单精度浮点型
  • double和Double:字符串转双精度浮点型
  • Date:字符串转日期类型,需要字符串满足一定的格式
  • 数组:多个input表单提交给同一个Action的属性,就会构成一个数组传入到该属性中
  • 集合:和数组类似,需要指定了的类型,并且类型不能超出基本数据类型

对于我们在Action中声明的属性的类型,如果是以上的这些类型的话,那么从客户端提交的过来的字符串就可以默认使用该机制自动转换成对应的类型,完成自动赋值。如果不是上述的类型,那么就需要自定义类型转换器来实现显式的转换类型,该内容后文介绍。此处只需要知道Action中的属性的类型为上述的几种,则从表单页面传入的数值会自动根据属性名自动完成赋值。

三、基于OGNL的类型转换

     对于非基本类型,我们使用默认的转换机制是不能解决问题的,例如修改上述的LoginAction:

//其中walker是一个符合Javabean规范的类,其具有两个属性name和age
public class LoginAction extends ActionSupport {
private Walker walker;
public void setWalker(Walker w){
this.walker =w;
}
public Walker getWalker(){
return this.walker;
} public String execute() throws Exception{
return SUCCESS;
}
}

如果Action实例的一个属性是我们自定义的类型,那么login表单页面原有的代码肯定是不能生效的,因为你指定的username和password在Action实例中是没有的。那么我们怎么将一个字符串赋值给Action实例属性呢?ognl语法是可以做到的,例如:

//login页面,使用表单标签
<body>
<s:form method="post" action="/login">
<s:textfield name="walker.username" label="用户名"/>
<s:textfield name="walker.age" label="年龄"/>
<s:submit value="提交"/>
</s:form>
</body>
//index页面
<body>
<h1>this is the index page</h1>
<p><s:property value="walker.getUsername()"/></p>
<p><s:property value="walker.getAge()"/></p> <br/>
<s:debug/>
</body>

我们在login页面使用ognl语法,walker.username指定了为Action实例属性walker的username属性传值,walker.age指定了为Action实例属性的walker的age属性传值。其实我们到这里可以看出来,使用ognl语法可以实现非基本类型的转换,实际上还是将问题转化到我们讨论的第一种情况,也就是把这么一个问题:如何将一个String类型转换为非基本类型,转化为了:如何把一个String类型转化为非基本类型的属性的类型。而这种问题Struts已经帮我们解决了。下面是上述程序的运行截图:

深入理解Struts2----类型转换

深入理解Struts2----类型转换

有关该分类还需要说明一点的是:对于list和map集合,这里的操作是有些变化的。我们详细看看:

//修改属性为一个list集合
public class LoginAction extends ActionSupport {
private List<Walker> list;
public void setList(List<Walker> w){
this.list =w;
}
public List<Walker> getList(){
return this.list;
} public String execute() throws Exception{
return SUCCESS;
}
}
//修改了的login表单页面
<body>
<s:form method="post" action="/login">
<s:textfield name="list[0].username" label="用户名"/>
<s:textfield name="list[0].age" label="年龄"/>
<s:textfield name="list[1].username" label="用户名"/>
<s:textfield name="list[1].age" label="年龄"/>
<s:submit value="提交"/>
</s:form>
</body>

LoginAction 中的改动没什么需要说的,至于login页面中使用了list[0].username表示为Action属性list的第一个元素的username传值,相同的,list[0].age表示为Action属性的list的第一个元素的age属性传值。index页面遍历list的代码没有贴出,因为比较简单。本质上也是和上述介绍的一样,最后都是使用了Struts的默认转换器。下面是输出结果:

深入理解Struts2----类型转换

深入理解Struts2----类型转换

上述介绍的是list集合作为Action属性的情况,对于map集合作为Action实例属性的情况其实是类似的,只是在传值和遍历的方面有细微差别。

//修改后的LoginAction 页面
public class LoginAction extends ActionSupport {
private Map<String,Walker> map;
public void setMap(Map<String,Walker> w){
this.map =w;
}
public Map<String,Walker> getMap(){
return this.map;
} public String execute() throws Exception{
return SUCCESS;
}
}
//login页面的表单传值
<body>
<s:form method="post" action="/login">
<s:textfield name="map['1'].username" label="用户名"/>
<s:textfield name="map['1'].age" label="年龄"/>
<s:textfield name="map['2'].username" label="用户名"/>
<s:textfield name="map['2'].age" label="年龄"/>
<s:submit value="提交"/>
</s:form>
</body>

map['1'].username表示为Action实例的map属性添加一条信息:key为1,key为1的value值为walker的username属性的值为该文本框的值。age属性类似。

四、自定义类型转换

     上一小节,我们使用ognl语法可以完成对非基本类型的转换,但是本质上还是调用了Struts的默认转换器。虽然利用ognl语法,我们可以完成大部分的类型转换,但是在某些极端情况下,这种方式是不能解决问题的,此时我们可以考虑自定义一个类型转换器来解析类型转换。想要自定义一个类型转换器就必须继承TypeConverter这个接口并实现其中的唯一方法:

public abstract Object convertValue(Map<String, Object> paramMap, Object paramObject1, Member paramMember, String paramString, Object paramObject2, Class paramClass);

该方法相当复杂,光参数就有七个。好在框架为我们提供了一个默认实现类:DefaultTypeConverter。该抽象类实现了TypeConverter接口并默认实现了一些方法,我们在自定义自己的类型转换器的时候只需要重写该类的某个方法即可,大大降低了我们的开发成本。当然我们可以进去简单看看DefaultTypeConverter抽象类的内部结构:

public Object convertValue(Map<String, Object> context, Object target, Member member, String propertyName, Object value, Class toType)
{
return convertValue(context, value, toType);
}
public Object convertValue(Map<String, Object> context, Object value, Class toType)
{
return convertValue(value, toType);
}
public Object convertValue(Object value, Class toType)
{
......
}

代码比较多,只贴出一些关键性的代码。该抽象类为我们提供了三个convertValue方法重载,他们之间的关系就是:参数多的重载调用参数少的。最后convertValue(Object value, Class toType)方法提供了默认实现,如果目标类型toType是我们上述介绍的几种基本类型,那么直接将value转换成该类型返回,如果value是个数组类型并且目标类型toType也是个数组类型,那么会获取value中的每个元素递归的调用该方法,为当前元素实现类型转换,最后返回toType类型。这里可能文字说明不能很明朗的会意你,你需要辅助着源代码。

对于默认的实现,我们还是不能完成某些自定义类型的转换,毕竟它只是一个默认实现。因为当系统无法使用默认类型转换器实现类型的转换的时候就会去查找是否有自定义的类型转换器,有则会自动调用convertValue最多参数的重载。所以我们可以重写convertValue的任意一个重载来完成自定义类型转换器。下面我们看一段代码:

public class WalkerConvert extends DefaultTypeConverter {

    public Object convertValue(Object value, Class toType){
if(toType == Walker.class){
String[] params = (String[])value;
Walker w = new Walker();
String[] user = params[0].split(",");
w.setUsername(user[0]);
w.setAge(Integer.valueOf(user[1]));
return w;
}
return null;
}
}

这里我们定义了一个WalkerConvert 类继承自DefaultTypeConverter 并重写了该convertValue方法。该方法具有两个参数,第一个参数表示原类型,第二个参数表示目标类型。这里需要对第一个参数value做一点说明,该参数的值实际上是一个String数组,一般情况下我们的参数被存放在索引位置为0的元素中,其余元素内容只有在表单是下拉框的时候将所有下拉框中的选项传过来(如果不使用下拉框一般只用到该数组的第一个元素)。上述代码中,我们将传入的字符串按照逗号分隔,前半部分是username的值,后半部分是age的值,我们看下结果图:

深入理解Struts2----类型转换

深入理解Struts2----类型转换

当我们从表单中提交我们填入的字符串,到了Action中之后,由于默认转换器不能完成自动转换,于是框架查找是否具有自定义的转换器,找到之后调用convertValue返回的结果就是属性walker的值,最后我们在index页面输出该walker属性的两个子属性。该方法中的操作我们已经介绍过了,此处不再赘述。当然此处还有一些疑问,例如:定义的该WalkerConvert 该放在什么位置,以及它是如何被web应用加载的?等。这些问题我们将在下一小节详细说明。

五、注册类型转换器

     带着上一小节的疑问,我们看如何让web容器知道我们的自定义转换器,并在无法使用默认转换器实现转换的时候查找到我们自己定义的转换器。注册一个类型转换器主要有以下三种方式:

  • 在局部范围内注册一个类型转换器
  • 在全局范围内注册一个类型转换器
  • 使用注解注册一个类型转换器

局部注册一个类型转换器实际上只能对某个Action的属性生效。首先我们需要创建一个局部类型转换文件,该文件的命名规则如下:

ActionName-conversion.properties

例如我们在LoginAction中有一个属性为Walker类型,我们需要注册一个该Action的转换器,则命名如下:

LoginAction-conversion.properties

这是该文件的文件名,对于文件内容,比如我们需要为Walker类型注册转换器,则可以在上述文件中添加如下一行代码:

// 属性名=转换器类的位置
walker=MyPackage.WalkerConvert

最后需要补充一点的是,创建的该文件应该和对应的Action位于同一个包下,这是方便框架搜索。

如果想要注册一个全局范围的类型转换器,那么对于该应用的任意一个Action中,只要存在指定的属性,都会调用该转换器实现转换,这是与局部转换器不同之处。注册全局类型转换器需要提供一个文件,该文件名称如下:

xwork-convertion.properties

为某个属性注册类型转换器的代码是一样的,只是该文件可以在全局使用。以上便简单介绍了注册类型转换器的两种方式,至于使用注解注册也是很简单的。此时,我们知道一旦表单页面传入的字符串不能被默认转换器自动转换成相应的类型,那么会查找相应的自定义转换器,返回该属性的值。

六、类型转换的错误处理

     最后有关类型转换这块还有一个错误处理的内容没有介绍,其实框架为我们在拦截器栈中注册了一个拦截器:convertionError。该拦截器专门用于拦截类型转换异常,一旦该拦截器拦截到异常产生则会封装所有的异常信息到ActionContext中,然后跳转到input处理结果页面,所以一般我们在为Action添加处理结果的时候会为其添加一个input页面。下面看一个错误处理的示例:

//input.jsp
<html>
<head>
<title></title>
</head>
<body>
<h1>this is the input page</h1>
</body>
</html>

我们只需要为LoginAction添加一个input的处理结果即可,当发生类型转换失败的时候就会封装错误信息并跳转到input页面。以下是程序运行的部分截图:

深入理解Struts2----类型转换

深入理解Struts2----类型转换

我们将第二个参数传入一个字符串类型,则必然发生类型转换错误,此时我们看到结果转向了input页面。

至此,我们简单介绍了struts2中有关类型转换的相关内容,有些地方理解不到,总结的不好,望不吝赐教。