《研磨struts2》第四章 Action 之 4.3 Action的数据
4.3 Action的数据
4.3.1 数据来源
在helloworld示例里面,在运行Action的execute方法的时候,你会神奇般的发现,Action的属性是有值的,而这正是Action进行请求处理所需要的数据。那么,这些数据从何而来呢?
很明显,这些数据就是你在登录页面填写的数据,换句话说,这些数据来源于用户请求对象,也就是request对象。
可是,Struts2怎么知道,页面上的值如何和Action的属性进行对应呢?
这就涉及到如何把页面的数据和Action进行对应的问题了,接下来就来讨论页面的数据和Action的三种基本对应方式。
4.3.2 基本的数据对应方式
在Struts2中,页面的数据和Action有两种基本对应方式,分别是:属性驱动(FieldDriven)和模型驱动(ModelDriven)。
属性驱动又有两种情况:一种是基本数据类型的属性对应;另外一种是JavaBean风格的属性对应。为了区分它们,我们约定称呼如下:称呼“基本数据类型的属性对应”为属性驱动,而“JavaBean风格的属性对应”为直接使用域对象。
下面就分别来看看它们都什么意思,都如何实现。
1:属性驱动FieldDriven(基本数据类型的属性对应)
基本数据类型的属性对应,就是web页面上要提交的html控件的name属性,和Action的属性或者与属性相应的getter/setter相对应,这种做法就是基本数据类型的属性对应的属性驱动。
事实上,我们已经使用过这种方式了,前面HelloWorld示例,就是采用的这种方式来把值对应到Action中的。
比如在登录页面上,我们是这么写的:
<form action="/helloworld/helloworldAction.action" method="post"> <input type="hidden" name="submitFlag" value="login"/> 账号:<input type="text" name="account"><br> 密码:<input type="password" name="password"><br> <input type="submit" value="提交"> </form>
在Action中是这么写的:
public class HelloWorldAction extends ActionSupport { private String account; private String password; private String submitFlag; public String getAccount() { return account; } public void setAccount(String account) { this.account = account; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getSubmitFlag() { return submitFlag; } public void setSubmitFlag(String submitFlag) { this.submitFlag = submitFlag; } //其他部分暂时省略掉,好让大家看清楚数据的对应关系 }
你会发现,在页面上input的name属性,和Action的属性是同一个名称,这样一来,当页面提交的时候,Struts2会自动从request对象里面把数据取出来,然后按照名称进行对应,自动设置到Action的属性里面去。
有些朋友可能会说,Action的属性都是private的呀,按道理外部是无法访问的,正是因为如此,才为每个私有的属性提供了getter/setter方法,来让外部访问。
这也意味着,如果你不想为每个属性提供getter/setter方法,觉得很累赘,有一个简单的方式,那就是把属性的可访问权限设置成public的就可以了。但在Java开发中,不是很建议直接开放属性让外部访问,一般都是通过getter/setter方法来访问。当然如何选择,根据实际情况来判断吧,总之两种方式都是可以把值对应上的。
2:属性驱动FieldDriven(直接使用域对象)
仔细察看上面属性驱动的方式,会发现,要是需要传入的数据很多的话,那么Action的属性也就很多了,再加上对应的getter/setter方法,Action类就直接上百行了,再在里面写请求处理的代码,会显得Action非常零乱,不够简洁,而且给人的感觉是Action的功能也不够单一。那么该怎么解决这个问题呢?
很简单,把属性和对应的getter/setter方法从Action里面移出去,单独做成一个域对象,这个对象就是用来封装这些数据的,然后在Action里面直接使用这个对象就可以了。
(1)先看看域对象的写法,按照JavaBean的风格来写,示例代码如下:
public class HelloWorldModel { private String account; private String password; private String submitFlag; public String getAccount() { return account; } public void setAccount(String account) { this.account = account; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getSubmitFlag() { return submitFlag; } public void setSubmitFlag(String submitFlag) { this.submitFlag = submitFlag; } }
(2)看看此时,Action写法的变化,主要就是直接使用这个对象,其实就是定义一个属性是这个对象类型,然后为这个属性提供相应的getter/setter方法即可,当然也可以直接把这个属性的可访问属性设置成public,这样就不需要写getter/setter方法了。
原来Action里面直接使用属性值的地方,就修改成使用这个属性对象来获取值了。示例代码如下:
public class HelloWorldAction extends ActionSupport { private HelloWorldModel hwm = new HelloWorldModel(); public HelloWorldModel getHwm() { return hwm; } public void setHwm(HelloWorldModel hwm) { this.hwm = hwm; } public String execute() throws Exception { //1:收集参数,不用做了,数据会直接映射到上面的hwm里面 //2:组织参数,也不用作了,数据会映射到上面的hwm的时候,就已经组织好了 //3:调用模型的逻辑功能处理,这里不需要,只是简单的输出一下传入的参数 this.businessExecute(); //4:根据逻辑处理的结果来选择下一个页面,这里直接选择转向欢迎页面 return "toWelcome"; } public void validate(){ if(hwm.getAccount()==null || hwm.getAccount().trim().length()==0){ this.addFieldError("account", this.getText("k1")); } if(hwm.getPassword()==null || hwm.getPassword().trim().length()==0){ this.addFieldError("password", this.getText("k2")); } if(hwm.getPassword()==null || hwm.getPassword().trim().length()<6){ this.addFieldError("password", this.getText("k3")); } } /** * 示例方法,表示可以执行业务逻辑处理的方法, */ public void businessExecute(){ System.out.println("用户输入的参数为==="+"account="+hwm.getAccount()+",password="+hwm.getPassword()+",submitFlag="+hwm.getSubmitFlag()); } }
(3)Action发生变化后,登录页面上也需要相应改变,否则数据是无法正确对应的,主要是在相应的name属性上,添加一个域对象的前缀,指明这个值到底对应到哪一个域对象里面去,示例如下:
<form action="/helloworld/helloworldAction.action" method="post"> <input type="hidden" name="hwm.submitFlag" value="login"/> 账号:<input type="text" name="hwm.account"><br> 密码:<input type="password" name="hwm.password"><br> <input type="submit" value="提交"> </form>
同理欢迎页面也需要相应调整,示例如下:
欢迎账号为<s:property value="hwm.account"/>的朋友来访
好了,去测试一下看看,是否好用。
3:模型驱动ModelDriven
在Struts2中,还有另外一种对应数据的方式叫模型驱动ModelDriven。它的基本实现方式是让Action实现一个ModelDriven的接口,这个接口需要我们实现一个getModel的方法,这个方法返回的就是Action所使用的数据模型对象。
(1)把Action代码修改成ModelDriven的实现方式,只是添加了ModelDriven的实现,另外去掉了“hwm”属性对应的getter/setter方法,其他地方基本上没有什么变化,示例代码如下:
import com.opensymphony.xwork2.ActionSupport; import com.opensymphony.xwork2.ModelDriven; public class HelloWorldAction extends ActionSupport implements ModelDriven{ private HelloWorldModel hwm = new HelloWorldModel(); public Object getModel() { return hwm; } public String execute() throws Exception { this.businessExecute(); return "toWelcome"; } public void validate(){ if(hwm.getAccount()==null || hwm.getAccount().trim().length()==0){ this.addFieldError("account", this.getText("k1")); } if(hwm.getPassword()==null || hwm.getPassword().trim().length()==0){ this.addFieldError("password", this.getText("k2")); } if(hwm.getPassword()==null || hwm.getPassword().trim().length()<6){ this.addFieldError("password", this.getText("k3")); } } /** * 示例方法,表示可以执行业务逻辑处理的方法, */ public void businessExecute(){ System.out.println("用户输入的参数为==="+"account="+hwm.getAccount()+",password="+hwm.getPassword()+",submitFlag="+hwm.getSubmitFlag()); } }
(2)登录页面也需要做相应调整,主要就是去掉刚才给name属性添加的“hwm.”这个前缀,示例代码如下:
<form action="/helloworld/helloworldAction.action" method="post"> <input type="hidden" name="submitFlag" value="login"/> 账号:<input type="text" name="account"><br> 密码:<input type="password" name="password"><br> <input type="submit" value="提交"> </form>
同理去调整欢迎页面,这里就不去示范了。
那么这里为什么不需要前缀了呢?
原因很简单,使用ModelDriven的方式,一个Action只能对应一个Model,因此不需要添加前缀,Struts2就能够知道,页面上“account”的值就对应到这个Model的“account”属性。如果你去加上前缀,反而对应不上了。
4:小结
(1)这里学习了三种数据的对应方式,在实际开发中该如何选择呢?
下面简要分析一下:
- 属性驱动(基本数据类型的属性对应):优点:简单,页面name和属性直接对应;缺点:导致Action类看上去比较零乱,显得功能不够单一。因此在实际开发中会酌情使用。
- 属性驱动(直接使用域对象):优点:把模型数据从Action中分离出来,让Action专注于请求处理,使得程序结构更清晰;缺点:页面上在对应的时候,必须添加正确的前缀,稍嫌麻烦。 但正是因为有前缀,在一个Action有多个数据模型的时候,这个缺点反而变成了优点,因为可以根据前缀来区分到底把这个数据对应给谁,这样一来,就不会乱了,比如:“hwm.uuid”、“um.uuid”就表示hwm和um这两个模型里面都有一个uuid的属性,但是,现在是带着前缀来指定值的对应,就不会出错了。在实际开发中,推荐优先使用这个方式。
- 模型驱动:优点:把模型数据从Action中分离出去了,使得程序结构更清晰;缺点:需要Action实现特殊的接口,而且把模型数据和Action作了一个绑定,这极大地限制了一个Action对应多个数据模型的能力,当然也可以做到,就是在这个模型里面包含其他的数据模型。在实际开发中,根据情况来选用。
(2)又有新问题了,这三种方式能不能混合使用呢?如果能?会不会冲突呢?
事实上,这三种方式是可以混合使用,甚至是三种方式一起使用。但是属性驱动(基本数据类型的属性对应)和模型驱动是有可能冲突的,因为这两种对应方式都没有前缀,如果出现这种冲突的情况,那么优先模型驱动的对应方式。
还是举个例子来说明,如果在Action中同时出现三种方式,示例代码如下:
public class HelloWorldAction extends ActionSupport implements ModelDriven{ /** * 用于ModelDriven使用 */ private HelloWorldModel hwm = new HelloWorldModel(); /** * 用于域对象的方式使用 */ public HelloWorldModel hwm2 = new HelloWorldModel(); /** * 用于FieldDriven使用 */ public String account = ""; public Object getModel() { return hwm; } public String execute() throws Exception { System.out.println("模型驱动的值:account="+hwm.getAccount()+",password="+hwm.getPassword()+",submitFlag="+hwm.getSubmitFlag()); System.out.println("使用域对象的值:account="+hwm2.getAccount()+",password="+hwm2.getPassword()+",submitFlag="+hwm2.getSubmitFlag()); System.out.println("属性驱动的值:account="+account); return "toWelcome"; } }
此时登录页面修改成如下示例:
<form action="/helloworld/helloworldAction.action" method="post"> <input type="hidden" name="submitFlag" value="login"/> 账号:<input type="text" name="account"><br> 密码:<input type="password" name="hwm2.password"><br> <input type="submit" value="提交"> </form>
注意,在上述页面的写法中,name="submitFlag"的值,将会使用ModelDriven的方式对应,因为没有其他可供它对应的地方;name="account"的值,既可以对应到Action的account属性,也可以通过ModelDriven的方式对应到hwm的account属性;而name="hwm2.password"的值,只能按照域对象对应的方式,对应到hwm2里面的password属性去。
去运行一下,看看结果。结果示例如下:
模型驱动的值:account=test,password=null,submitFlag=login 使用域对象的值:account=null,password=test,submitFlag=null 属性驱动的值:account=
你会发现,password直接对应到了域对象的password去,毫无争议;而account的值,虽然可以同时对应到模型和属性上,但结果很明显是模型驱动优先,也就是对应到模型的account属性去了。
(3)学到这里,已经掌握了Action类的写法,掌握了Action里面execute方法的写法,也掌握了如何把值跟Action对应起来,看起来,知识好像足够多了。
但是在实际开发中,往往不会像前面示例得这么简单,而是需要面对各种复杂的情况,比如:
- 传入值的类型不一致,需要转换
- 需要传入一组数目不确定的字符串。这在web开发中是非常常见的,比如在注册用户的时候,可能需要添您的爱好,在一系列checkbox框中勾选出您喜欢的。
- 需要传入一组数目不确定的域对象。比如在旅游类的电子商务应用中,添加一个旅游团之后,还需要把所有的参团人员的基本信息添入。
等等问题,那么接下来就来深入的讨论一下。
4.3.3 传入非String类型的值
前面的示例,从页面传入Action的值都是String类型的,可是在实际开发中,并不是每次传递的数据都是String类型,也可能需要传递别的类型的值,比如传递int类型,好在Struts2能帮助我们完成从String类型到基本类型的自动转换。
1:传入基本类型的值
假如把Action的account改成int类型的,那么该如何对应呢?注意这里只是用int类型来做个示例,其他基本类型也是一样的做法。
(1)此时Action的示例代码如下:
public class HelloWorldAction extends ActionSupport{ public int account; public String password=""; public String submitFlag =""; public String execute() throws Exception { System.out.println("the account="+account+",password="+password+",submitFlag="+submitFlag); return "toWelcome"; } }
(2)此时的登录页面很简单,不需要任何特殊的处理,示例代码如下:
<form action="/helloworld/helloworldAction.action" method="post"> <input type="hidden" name="submitFlag" value="login"/> 账号:<input type="text" name="account"><br> 密码:<input type="password" name="password"><br> <input type="submit" value="提交"> </form>
(3)重新访问登录页面,记得在账号的文本框里面填写数字,填写后点击提交,看看后台输出的值:
the account=11,password=22,submitFlag=login
看上去一切很好,Struts2已经正确的帮我们把request中account的字符串转换成int类型了。
(4)但是,如果在登录页面上不填账号,再次运行一下,会发现后台打印了好多好多错误,如下(错误太多,省略了其中的大部分):
ognl.OgnlException: account [java.lang.IllegalArgumentException: Can not set int field cn.javass.action.action.HelloWorldAction.account to java.lang.String] at ognl.ObjectPropertyAccessor.setPossibleProperty(ObjectPropertyAccesso r.java:103) ......省略了 Caused by: java.lang.IllegalArgumentException: Can not set int field cn.javass.a ction.action.HelloWorldAction.account to java.lang.String ......省略了 ... 62 more /-- Encapsulated exception ------------\ java.lang.IllegalArgumentException: Can not set int field cn.javass.action.actio n.HelloWorldAction.account to java.lang.String ......省略了 \--------------------------------------/ the account=0,password=22,submitFlag=login
先看看上面加粗的以“Caused by”开头的那句描述,很明确的表明是在设置int型的account属性时出现错误,因为这次页面没有填写account的值,那么传递过来就是一个空字符串或者是null,但不管是哪种情况,都无法转换成为int类型的值,因此就出错了。
再看看最后一句输出,可以得到结论,虽然对应account的值出错了,但是不影响其他属性的取值,password和submitFlag能正确取到值。
因此,如果属性采用基本类型的时候,如果用户没有填写则会抛错,不过这个错误并不影响其他属性值的对应。
2:使用包装类型
现在,再用Integer来试一试,看看会出现什么情况。
(1)登录页面不需变化,只是把Action中的account属性的类型改为Integer,同时把它变成private的,然后提供相应的getter/setter方法,示例如下:
public class HelloWorldAction extends ActionSupport{ private Integer account; public String password=""; public String submitFlag =""; public String execute() throws Exception { System.out.println("the account="+account+",password="+password+",submitFlag="+submitFlag); return "toWelcome"; } public Integer getAccount() { return account; } public void setAccount(Integer account) { this.account = account; } }
(2)重新访问登录页面,在账号的文本框里面填写数字,填写后点击提交,看看后台输出的值,没有任何问题,仍然会正常输出:
the account=111,password=222,submitFlag=login
(3)接下来,不填写账号,再次运行,看看会怎样呢?
后台运行不再报错,同样能输出值,只是account的值为null而已,如下:
the account=null,password=222,submitFlag=login
这说明如果使用包装类型的话,就无需关心或者去特别处理Struts2在对应值的时候,自动类型转换所报出的错误了。
(4)可能有些朋友会想,account类型改为Integer后,为什么要为它添加getter/setter方法呢?你可以不去添加getter/setter方法,而是让account为public的,试试看,应该会抛出如下错误:
ognl.NoSuchPropertyException: cn.javass.action.action.HelloWorldAction.account at ognl.ObjectPropertyAccessor.setProperty(ObjectPropertyAccessor.java:1 66) ……省略了
这个错误的意思是:没有找到一个叫做account的property,注意这里用了property而不是直接翻译成“属性”,是因为这里有一个准确理解的问题。
在日常开发中,可能大家并不去关心“property”的准确含义,一般都是当作属性理解,那么“attribute”呢?也是当作“属性”理解吧,那么他们有什么区别呢?
做过设计的朋友可能会很清楚,“property”和“attribute”是不同的。简单点说,“attribute”是用来描述对象固有的一些属性,一般是创建过后不变的一些值,比如:人这个对象,有手这个“attribute”,正常情况下,创建一个人的实例对象过后,手这个属性一般就不变了。因此“attribute”通常就表现成为私有的属性。
而“property”也是属性,但是一般是创建过后可变的一些值,比如:人这个对象,有一个头发颜色这个“property”,创建对象实例过后,这个人可能去染发了,变成其他颜色了,也就是这个属性的值是可以通过外部来改变的。因此“property” 通常就表现成为私有的属性,并为它设置相应的getter/setter方法。
好了,现在来理解上面那个错误的意思,“没有找到一个叫做account的property”,这就明确告诉我们了,account不是一个“property”,也就是说account这个属性没有相应的getter/setter方法。
4.3.4 如何处理传入多个值
在实际开发中,同一个属性需要传入多个值的情况也是很常见的。下面就来讨论一下,看看到底如何处理这种情况。
1:传入一组数目不确定的字符串
比如在注册用户的时候,可能需要添用户的爱好,也就是在一系列文本框中选出用户喜欢做的事情。页面示例如下:
<input type="checkbox" name=" habits" value="sports">运动 <input type="checkbox" name=" habits" value="reading">读书 <input type="checkbox" name=" habits" value="sleep">睡觉
注意:“habits”在传入action的时候并不知道到底有几个值,可能是一个值,也可能是多个值,如果用户选择了其中两个,就会得到一个有两个字符串的数组。
那么,Action中该如何写才能正确接收这些值呢?
在这种情况下,Action中可以有以下两种写法来对应:
(1)定义一个私有的String数组类型的属性,提供相应的getter/setter方法,示例如下:
private String[] habits; public String[] getHabits () { return habits; } public void setHabits (String[]habits) { this. habits= habits; }
当然直接定义一个public的String数组类型的属性也可以,示例如下:
public String[] habits;
(2)定义一个私有的集合类型的属性,比如List类型的,提供相应的getter/setter方法,示例如下:
private List<String> habits; public List<String> getHabits () { return habits; } public void setHabits (List<String> habits) { this. habits = habits; }
当然直接定义一个public的集合类型的属性也可以,示例如下:
public List<String> hobis;
2:传入一组数目不确定的域对象 这种情况也很常见,比如添加一个旅游团之后,还要把所有参团人员的基本信息添加到后台,要求Action把每个参团人员的基本信息当作一组,封装成一个域对象。 (1)假设这个域对象名称为UserModel,在Action中定义一个private的List,并给出相应的getter/setter方法,示例代码如下:
private List<UserModel> users; public List<UserModel> getUsers() { return users; } public void setUsers(List<UserModel> users) { this.users = users; }
(2)这时候在页面上要按照如下的示例来写:
<input type="text" name="users[0].account"> <input type="text" name="users[0].password"> <input type="text" name="users[1].account"> <input type="text" name="users[1].password">
要注意上面的写法,“属性名称[索引]”。上面这样写Struts2就会把上面的4个文本框组成两个UserModel,第1个和第2个一组,第3个和第4个一组。
(3)除了使用private的List及其getter/setter之外,同样还可以使用public的List,示例代码如下:
public List<UserModel> users = new ArrayList<UserModel>();
注意,使用public的List的时候,必须在声明的时候就新建一个ArrayList,否则运行会报“NullPointerException”。
(4)另外一点,如果在页面上忘了写索引,如下:
<input type="text" name="users.account"> <input type="text" name="users.password"> <input type="text" name="users.account"> <input type="text" name="users.password">
那么Action接到的将不是两个对象,而是四个,分别拥有一个属性的值。
私塾在线网站原创《研磨struts2》系列
转自请注明出处:【http://sishuok.com/forum/blogPost/list/0/4047.html】
欢迎访问http://sishuok.com获取更多内容