`

《研磨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代码:
  1. <form action="/helloworld/helloworldAction.action" method="post">  
  2.     <input type="hidden" name="submitFlag" value="login"/>  
  3.     账号:<input type="text" name="account"><br>  
  4.     密码:<input type="password" name="password"><br>  
  5.     <input type="submit" value="提交">  
  6. </form>  

在Action中是这么写的:

 

java代码:
  1. public class HelloWorldAction extends ActionSupport {  
  2.     private String account;  
  3.     private String password;  
  4.     private String submitFlag;  
  5.     public String getAccount() {  
  6.         return account;  
  7.     }  
  8.     public void setAccount(String account) {  
  9.         this.account = account;  
  10.     }  
  11.     public String getPassword() {  
  12.         return password;  
  13.     }  
  14.     public void setPassword(String password) {  
  15.         this.password = password;  
  16.     }  
  17.     public String getSubmitFlag() {  
  18.         return submitFlag;  
  19.     }  
  20.     public void setSubmitFlag(String submitFlag) {  
  21.         this.submitFlag = submitFlag;  
  22.     }  
  23.     //其他部分暂时省略掉,好让大家看清楚数据的对应关系  
  24. }  

你会发现,在页面上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代码:
  1. public class HelloWorldModel {  
  2.     private String account;  
  3.     private String password;  
  4.     private String submitFlag;  
  5.       
  6.     public String getAccount() {  
  7.         return account;  
  8.     }  
  9.     public void setAccount(String account) {  
  10.         this.account = account;  
  11.     }  
  12.     public String getPassword() {  
  13.         return password;  
  14.     }  
  15.     public void setPassword(String password) {  
  16.         this.password = password;  
  17.     }  
  18.     public String getSubmitFlag() {  
  19.         return submitFlag;  
  20.     }  
  21.     public void setSubmitFlag(String submitFlag) {  
  22.         this.submitFlag = submitFlag;  
  23.     }  
  24. }  

(2)看看此时,Action写法的变化,主要就是直接使用这个对象,其实就是定义一个属性是这个对象类型,然后为这个属性提供相应的getter/setter方法即可,当然也可以直接把这个属性的可访问属性设置成public,这样就不需要写getter/setter方法了。

原来Action里面直接使用属性值的地方,就修改成使用这个属性对象来获取值了。示例代码如下:

 

java代码:
  1. public class HelloWorldAction extends ActionSupport {  
  2.     private HelloWorldModel hwm = new HelloWorldModel();  
  3.       
  4.     public HelloWorldModel getHwm() {  
  5.         return hwm;  
  6.     }  
  7.     public void setHwm(HelloWorldModel hwm) {  
  8.         this.hwm = hwm;  
  9.     }  
  10.       
  11.     public String execute() throws Exception {  
  12.         //1:收集参数,不用做了,数据会直接映射到上面的hwm里面  
  13.         //2:组织参数,也不用作了,数据会映射到上面的hwm的时候,就已经组织好了  
  14.         //3:调用模型的逻辑功能处理,这里不需要,只是简单的输出一下传入的参数  
  15.         this.businessExecute();  
  16.         //4:根据逻辑处理的结果来选择下一个页面,这里直接选择转向欢迎页面  
  17.         return "toWelcome";  
  18.     }  
  19.       
  20.     public void validate(){  
  21.         if(hwm.getAccount()==null || hwm.getAccount().trim().length()==0){  
  22.             this.addFieldError("account"this.getText("k1"));  
  23.         }  
  24.         if(hwm.getPassword()==null || hwm.getPassword().trim().length()==0){  
  25.             this.addFieldError("password",  this.getText("k2"));  
  26.         }  
  27.         if(hwm.getPassword()==null || hwm.getPassword().trim().length()<6){  
  28.             this.addFieldError("password",  this.getText("k3"));  
  29.         }  
  30.     }  
  31.     /** 
  32.      * 示例方法,表示可以执行业务逻辑处理的方法, 
  33.      */  
  34.     public void businessExecute(){  
  35.         System.out.println("用户输入的参数为==="+"account="+hwm.getAccount()+",password="+hwm.getPassword()+",submitFlag="+hwm.getSubmitFlag());  
  36.     }     
  37. }  

(3)Action发生变化后,登录页面上也需要相应改变,否则数据是无法正确对应的,主要是在相应的name属性上,添加一个域对象的前缀,指明这个值到底对应到哪一个域对象里面去,示例如下:

 

java代码:
  1. <form action="/helloworld/helloworldAction.action" method="post">  
  2.     <input type="hidden" name="hwm.submitFlag" value="login"/>  
  3.     账号:<input type="text" name="hwm.account"><br>  
  4.     密码:<input type="password" name="hwm.password"><br>  
  5.     <input type="submit" value="提交">  
  6. </form>  

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

 

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

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

3:模型驱动ModelDriven

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

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

 

java代码:
  1. import com.opensymphony.xwork2.ActionSupport;  
  2. import com.opensymphony.xwork2.ModelDriven;  
  3.   
  4. public class HelloWorldAction extends ActionSupport implements ModelDriven{  
  5.     private HelloWorldModel hwm = new HelloWorldModel();      
  6.       
  7.     public Object getModel() {  
  8.         return hwm;  
  9.     }  
  10.       
  11.     public String execute() throws Exception {  
  12.         this.businessExecute();  
  13.         return "toWelcome";  
  14.     }  
  15.       
  16.     public void validate(){  
  17.         if(hwm.getAccount()==null || hwm.getAccount().trim().length()==0){  
  18.             this.addFieldError("account"this.getText("k1"));  
  19.         }  
  20.         if(hwm.getPassword()==null || hwm.getPassword().trim().length()==0){  
  21.             this.addFieldError("password",  this.getText("k2"));  
  22.         }  
  23.         if(hwm.getPassword()==null || hwm.getPassword().trim().length()<6){  
  24.             this.addFieldError("password",  this.getText("k3"));  
  25.         }  
  26.     }  
  27.     /** 
  28.      * 示例方法,表示可以执行业务逻辑处理的方法, 
  29.      */  
  30.     public void businessExecute(){  
  31.         System.out.println("用户输入的参数为==="+"account="+hwm.getAccount()+",password="+hwm.getPassword()+",submitFlag="+hwm.getSubmitFlag());  
  32.     }  
  33. }  

(2)登录页面也需要做相应调整,主要就是去掉刚才给name属性添加的“hwm.”这个前缀,示例代码如下:

 

java代码:
  1. <form action="/helloworld/helloworldAction.action" method="post">  
  2.     <input type="hidden" name="submitFlag" value="login"/>  
  3.     账号:<input type="text" name="account"><br>  
  4.     密码:<input type="password" name="password"><br>  
  5.     <input type="submit" value="提交">  
  6. </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代码:
  1. public class HelloWorldAction extends ActionSupport implements ModelDriven{  
  2.     /** 
  3.      * 用于ModelDriven使用 
  4.      */  
  5.     private HelloWorldModel hwm = new HelloWorldModel();  
  6.     /** 
  7.      * 用于域对象的方式使用 
  8.      */  
  9.     public HelloWorldModel hwm2 = new HelloWorldModel();  
  10.     /** 
  11.      * 用于FieldDriven使用 
  12.      */  
  13.     public String account = "";  
  14.       
  15.       
  16.     public Object getModel() {  
  17.         return hwm;  
  18.     }     
  19.       
  20.     public String execute() throws Exception {  
  21.         System.out.println("模型驱动的值:account="+hwm.getAccount()+",password="+hwm.getPassword()+",submitFlag="+hwm.getSubmitFlag());  
  22.         System.out.println("使用域对象的值:account="+hwm2.getAccount()+",password="+hwm2.getPassword()+",submitFlag="+hwm2.getSubmitFlag());  
  23.         System.out.println("属性驱动的值:account="+account);  
  24.           
  25.         return "toWelcome";  
  26.     }  
  27. }  

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

 

java代码:
  1. <form action="/helloworld/helloworldAction.action" method="post">  
  2.     <input type="hidden" name="submitFlag" value="login"/>  
  3.     账号:<input type="text" name="account"><br>  
  4.     密码:<input type="password" name="hwm2.password"><br>  
  5.     <input type="submit" value="提交">  
  6. </form>  

注意,在上述页面的写法中,name="submitFlag"的值,将会使用ModelDriven的方式对应,因为没有其他可供它对应的地方;name="account"的值,既可以对应到Action的account属性,也可以通过ModelDriven的方式对应到hwm的account属性;而name="hwm2.password"的值,只能按照域对象对应的方式,对应到hwm2里面的password属性去。

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

 

java代码:
  1. 模型驱动的值:account=test,password=null,submitFlag=login  
  2. 使用域对象的值:account=null,password=test,submitFlag=null  
  3. 属性驱动的值: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代码:
  1. public class HelloWorldAction extends ActionSupport{  
  2.     public int account;  
  3.     public String password="";  
  4.     public String submitFlag ="";     
  5.       
  6.     public String execute() throws Exception {  
  7.         System.out.println("the account="+account+",password="+password+",submitFlag="+submitFlag);  
  8.         return "toWelcome";  
  9.     }  
  10. }  

(2)此时的登录页面很简单,不需要任何特殊的处理,示例代码如下:

 

java代码:
  1. <form action="/helloworld/helloworldAction.action" method="post">  
  2.     <input type="hidden" name="submitFlag" value="login"/>  
  3.     账号:<input type="text" name="account"><br>  
  4.     密码:<input type="password" name="password"><br>  
  5.     <input type="submit" value="提交">  
  6. </form>  

(3)重新访问登录页面,记得在账号的文本框里面填写数字,填写后点击提交,看看后台输出的值:

 

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

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

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

 

java代码:
  1. ognl.OgnlException: account [java.lang.IllegalArgumentException: Can not set int  
  2.  field cn.javass.action.action.HelloWorldAction.account to java.lang.String]  
  3.         at ognl.ObjectPropertyAccessor.setPossibleProperty(ObjectPropertyAccesso  
  4. r.java:103)  
  5.         ......省略了  
  6. Caused by: java.lang.IllegalArgumentException: Can not set int field cn.javass.a  
  7. ction.action.HelloWorldAction.account to java.lang.String  
  8.         ......省略了  
  9.         ... 62 more  
  10. /-- Encapsulated exception ------------\  
  11. java.lang.IllegalArgumentException: Can not set int field cn.javass.action.actio  
  12. n.HelloWorldAction.account to java.lang.String  
  13.         ......省略了  
  14. \--------------------------------------/  
  15. 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代码:
  1. public class HelloWorldAction extends ActionSupport{  
  2.     private Integer account;  
  3.     public String password="";  
  4.     public String submitFlag ="";     
  5.       
  6.     public String execute() throws Exception {  
  7.         System.out.println("the account="+account+",password="+password+",submitFlag="+submitFlag);  
  8.         return "toWelcome";  
  9.     }  
  10.   
  11.     public Integer getAccount() {  
  12.         return account;  
  13.     }  
  14.     public void setAccount(Integer account) {  
  15.         this.account = account;  
  16.     }     
  17. }  

(2)重新访问登录页面,在账号的文本框里面填写数字,填写后点击提交,看看后台输出的值,没有任何问题,仍然会正常输出:

 

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

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

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

 

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

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

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

 

java代码:
  1. ognl.NoSuchPropertyException: cn.javass.action.action.HelloWorldAction.account  
  2.         at ognl.ObjectPropertyAccessor.setProperty(ObjectPropertyAccessor.java:1  
  3. 66)  
  4. ……省略了  

这个错误的意思是:没有找到一个叫做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代码:
  1. <input type="checkbox" name=" habits" value="sports">运动  
  2. <input type="checkbox" name=" habits" value="reading">读书  
  3. <input type="checkbox" name=" habits" value="sleep">睡觉  

注意:“habits”在传入action的时候并不知道到底有几个值,可能是一个值,也可能是多个值,如果用户选择了其中两个,就会得到一个有两个字符串的数组。

       那么,Action中该如何写才能正确接收这些值呢?

       在这种情况下,Action中可以有以下两种写法来对应:

(1)定义一个私有的String数组类型的属性,提供相应的getter/setter方法,示例如下:

 

java代码:
  1. private String[] habits;  
  2. public String[] getHabits () {  
  3.     return habits;  
  4. }  
  5. public void setHabits (String[]habits) {  
  6.     this. habits= habits;  
  7. }  

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

 

java代码:
  1. public String[] habits;  

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

 

java代码:
  1. private List<String> habits;  
  2. public List<String> getHabits () {  
  3.     return habits;  
  4. }  
  5. public void setHabits (List<String> habits) {  
  6.     this. habits = habits;  
  7. }  

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

 

java代码:
  1. public List<String> hobis;  

 

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

 

java代码:
  1. private List<UserModel> users;  
  2. public List<UserModel> getUsers() {  
  3.     return users;  
  4. }  
  5. public void setUsers(List<UserModel> users) {  
  6.     this.users = users;  
  7. }  

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

 

java代码:
  1. <input type="text" name="users[0].account">  
  2. <input type="text" name="users[0].password">  
  3. <input type="text" name="users[1].account">  
  4. <input type="text" name="users[1].password">  

要注意上面的写法,“属性名称[索引]”。上面这样写Struts2就会把上面的4个文本框组成两个UserModel,第1个和第2个一组,第3个和第4个一组。

(3)除了使用private的List及其getter/setter之外,同样还可以使用public的List,示例代码如下:

 

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

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

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

 

java代码:
  1. <input type="text" name="users.account">  
  2. <input type="text" name="users.password">  
  3. <input type="text" name="users.account">  
  4. <input type="text" name="users.password">  

那么Action接到的将不是两个对象,而是四个,分别拥有一个属性的值。

 

私塾在线网站原创《研磨struts2》系列

转自请注明出处:【http://sishuok.com/forum/blogPost/list/0/4047.html

欢迎访问http://sishuok.com获取更多内容

分享到:
评论
2 楼 vsddvsd 2012-11-21  
不错不错,很好很好
1 楼 onewill 2012-07-15  
资料很好,大赞楼主

相关推荐

Global site tag (gtag.js) - Google Analytics