《研磨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中的。

比如在登录页面上,我们是这么写的:

java代码:
<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中是这么写的:

java代码:
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的风格来写,示例代码如下:

java代码:
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里面直接使用属性值的地方,就修改成使用这个属性对象来获取值了。示例代码如下:

java代码:
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属性上,添加一个域对象的前缀,指明这个值到底对应到哪一个域对象里面去,示例如下:

java代码:
<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>  

同理欢迎页面也需要相应调整,示例如下:

java代码:
欢迎账号为<s:property value="hwm.account"/>的朋友来访  

好了,去测试一下看看,是否好用。

3:模型驱动ModelDriven

       在Struts2中,还有另外一种对应数据的方式叫模型驱动ModelDriven。它的基本实现方式是让Action实现一个ModelDriven的接口,这个接口需要我们实现一个getModel的方法,这个方法返回的就是Action所使用的数据模型对象。

(1)把Action代码修改成ModelDriven的实现方式,只是添加了ModelDriven的实现,另外去掉了“hwm”属性对应的getter/setter方法,其他地方基本上没有什么变化,示例代码如下:

java代码:
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.”这个前缀,示例代码如下:

java代码:
<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中同时出现三种方式,示例代码如下:

java代码:
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";  
    }  
}  

此时登录页面修改成如下示例:

java代码:
<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属性去。

    去运行一下,看看结果。结果示例如下:

java代码:
模型驱动的值: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的示例代码如下:

java代码:
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)此时的登录页面很简单,不需要任何特殊的处理,示例代码如下:

java代码:
<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)重新访问登录页面,记得在账号的文本框里面填写数字,填写后点击提交,看看后台输出的值:

java代码:
the account=11,password=22,submitFlag=login  

看上去一切很好,Struts2已经正确的帮我们把request中account的字符串转换成int类型了。

(4)但是,如果在登录页面上不填账号,再次运行一下,会发现后台打印了好多好多错误,如下(错误太多,省略了其中的大部分):

java代码:
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方法,示例如下:

java代码:
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)重新访问登录页面,在账号的文本框里面填写数字,填写后点击提交,看看后台输出的值,没有任何问题,仍然会正常输出:

java代码:
the account=111,password=222,submitFlag=login  

(3)接下来,不填写账号,再次运行,看看会怎样呢?

       后台运行不再报错,同样能输出值,只是account的值为null而已,如下:

java代码:
the account=null,password=222,submitFlag=login  

这说明如果使用包装类型的话,就无需关心或者去特别处理Struts2在对应值的时候,自动类型转换所报出的错误了。

(4)可能有些朋友会想,account类型改为Integer后,为什么要为它添加getter/setter方法呢?你可以不去添加getter/setter方法,而是让account为public的,试试看,应该会抛出如下错误:

java代码:
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:传入一组数目不确定的字符串

       比如在注册用户的时候,可能需要添用户的爱好,也就是在一系列文本框中选出用户喜欢做的事情。页面示例如下:

java代码:
<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方法,示例如下:

java代码:
private String[] habits;  
public String[] getHabits () {  
    return habits;  
}  
public void setHabits (String[]habits) {  
    this. habits= habits;  
}  

当然直接定义一个public的String数组类型的属性也可以,示例如下:

java代码:
public String[] habits;  

(2)定义一个私有的集合类型的属性,比如List类型的,提供相应的getter/setter方法,示例如下:

java代码:
private List<String> habits;  
public List<String> getHabits () {  
    return habits;  
}  
public void setHabits (List<String> habits) {  
    this. habits = habits;  
}  

当然直接定义一个public的集合类型的属性也可以,示例如下:

java代码:
public List<String> hobis;  
java代码:
2:传入一组数目不确定的域对象  
这种情况也很常见,比如添加一个旅游团之后,还要把所有参团人员的基本信息添加到后台,要求Action把每个参团人员的基本信息当作一组,封装成一个域对象。  
(1)假设这个域对象名称为UserModel,在Action中定义一个private的List,并给出相应的getter/setter方法,示例代码如下:  
java代码:
private List<UserModel> users;  
public List<UserModel> getUsers() {  
    return users;  
}  
public void setUsers(List<UserModel> users) {  
    this.users = users;  
}  

(2)这时候在页面上要按照如下的示例来写:

java代码:
<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,示例代码如下:

java代码:
public List<UserModel> users = new ArrayList<UserModel>();  

注意,使用public的List的时候,必须在声明的时候就新建一个ArrayList,否则运行会报“NullPointerException”。

(4)另外一点,如果在页面上忘了写索引,如下:

java代码:
<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获取更多内容

相关推荐