5.5 实现Action
Action是Struts2编程的核心部分,反映了对Web应用程序的功能需求。Action在MVC模式中担任控制部分的角色,在Struts2中也使用得最多。每个请求的动作都对应于一个相应的action,action还可以负责存储数据/状态(以getter和setter的方式)并且执行逻辑处理。
在本节中将关注如何实现action,以及action如何提供Web应用程序中所需的通用功能。除了Action接口之外,Struts2的action也可以选择实现其他可选择的接口,从而使action能够提供诸如国际化、校验、负责工作流和错误信息处理等功能。ActionSupport基类实现了Action接口并提供了大部分可选择口默认实现。本节将深入讲述这些类。除此之外,也将探讨action是如何通过使用JavaBean属性提供输入和输出的,最后将介绍如何处理文件上传。
5.5.1 实现Action接口
Struts2的Action接口来源于WebWork,全包名为com.opensymphony.xwork2.Action,如实例5-10所示。在Struts2中定义action类时已经可以不实现Action接口,Struts2会以反射的方式来调用action类。
【实例5-10】命名空间:com.opensymphony.xwork2.Action接口
01 public interface Action{ 02 //下面定义了5个字符串常量 03 public static final String SUCCESS = "success"; 04 public static final String NONE = "none"; 05 public static final String ERROR = "error"; 06 public static final String INPUT = "input"; 07 public static final String LOGIN = "login"; 08 //定义处理用户请求的execute抽象方法 09 public String execute() throws Exception; 10 }
【代码剖析】上述代码为Action接口的定义,在代码第3行到第7行定义了五个常量,而在第9行定义了一个关于执行的方法。
注意
在Struts1中同样有Action接口(org.apache.strtus.action.Action),但与此有很大不同,不要混淆。Struts1的一个主要缺点就是Action接口中包含了javax.servlet.http包的内容,使其不能脱离JSP容器运行调试,Struts2吸取了WebWork的优点,摒弃了这个接口。
5.5.2 扩展ActionSupport类
ActionSupport是一个让action类能够更快开始工作的基类。它包含了action能够提供的许多可选服务的默认实现,让开发者更容易地开始开发自己的action类,不需要再为这些可选服务提供具体实现了。同时能够改写可选择接口的任意一个方法实现并保持其他方法的默认实现。由于ActionSupport预建了许多开箱即用的功能,建议读者创建自己的action时都扩展ActionSupport类。ActionSupport实现了以下可选择接口,如图5.4所示。
1)Validateable提供一个Validate()方法,可以对action进行校验。
2)ValidationAware提供了保存和取出action级别或者字段级别错误信息的方法。
3)TextProvider提供了获取本地化信息文本的方法。
4)LocalProvider提供了getLocale()方法以返回locale,而locale则用于获取本地化信息。
5.5.3 实现基本校验
通常在执行业务逻辑之前,校验用户提供的数据是十分必要的。这种字段校验包括“某个字段是必需的”、“某个字段必须大于某个值,小于某个值”等内容。为了自动执行校验,Struts2提供了一种能够在execute()方法被调用之前调用其他方法对action进行处理的机制,这个机制由com.opensymphony.xwork2. Validateable接口提供。它包含了一个方法:
public void validate()
图5.4 ActionSupport类图
Validateable接口为action增加了一个标记,通过以上方法使得action能够自动被校验。
保存和显示校验的错误信息由接口ValidationAware来负责,这两个接口一般会同时使用。ValidationAware接口定义如实例5-11所示。
【实例5-11】ValidationAware接口定义
01 public interface ValidationAware { 02 03 /** 04 * 设置用于装载Action级别错误信息字符串Collections 05 * 06 * @param errorMessages 07 */ 08 void setActionErrors(Collection errorMessages); 09 /** 10 * 获取用于装载Action级别错误信息字符串的Collection 11 * 12 * 13 * @return 装载Action级别错误信息字符串的Collection 14 */ 15 Collection getActionErrors(); 16 /** 17 * 设置用于装载Action级别信息字符串Collections (not 不是错误信息) 18 */ 19 void setActionMessages(Collection messages); 20 /** 21 * 获取用于装载Action级别信息字符串Collections (not 不是错误信息) 22 * 23 * @return 装载Action级别信息字符串Collections 24 */ 25 Collection getActionMessages(); 26 /** 27 * 设置字段错误信息map, 该map提供了从字段名到装载了错误信息的Collection之间 28 的映射 29 * 30 * @param errorMap 31 */ 32 void setFieldErrors(Map errorMap); 33 34 /** 35 * 获取字段错误信息map, 该map提供了从字段名到装载了错误信息的Collection之间 36 的映射 37 * 38 * @return Map, 字段错误信息map 39 */ 40 Map getFieldErrors(); 41 /** 42 * 增加一个action级别的错误信息 43 * 44 * @param anErrorMessage 45 */ 46 void addActionError(String anErrorMessage); 47 48 /** 49 * 增加一个action级别的信息 50 */ 51 void addActionMessage(String aMessage); 52 53 /** 54 * 给一个字段增加错误信息 55 * 56 * @param fieldName name of field 57 * @param errorMessage the error message 58 */ 59 void addFieldError(String fieldName, String errorMessage); 60 61 /** 62 * 判断是否有action级别的错误信息存在 63 * 64 * @return 如果有action级别的错误信息存在返回true 65 */ 66 boolean hasActionErrors(); 67 /** 68 * 判断是否有action级别的信息存在 69 * 70 * @return如果有action级别的信息存在返回true 71 */ 72 boolean hasActionMessages(); 73 /** 74 * 判断是否有action级别或字段级别的错误信息存在 75 * 76 * @return 如果有action级别或字段级别的错误信息存在返回true 77 */ 78 boolean hasErrors(); 79 /** 80 *判断是否有action级别或字段级别的错误信息存在 81 * 82 * @return 如果有字段级别的错误信息存在返回true 83 */ 84 boolean hasFieldErrors(); 85 }
【代码剖析】上述代码为类ValidationAware接口的定义,该类定义了如此之多的方法,实现这个接口很复杂,幸运的是ActionSupport类已经默认实现了这个接口,直接使用就可以了。
在Struts2中校验工作流程如图5.5所示。
1)如果action实现了Validateable接口,在执行action之前,调用action的validate方法。
2)如果校验action出错,返回INPUT,否则继续执行这个action。
3)如果值是非法的,validate()方法将调用ValidationAware接口的addFieldError()等方法保存错误信息。错误信息能够依据class级别和字段两个级别进行收集。
4)在错误信息页面中,可以通过ValidationAware接口的getActionErrors等方法获取这些错误信息,显示给用户。
图5.5 校验流程图
说明
Struts2之所以能实现自动调用这两个接口,是通过一个特殊的Interceptor,“DefaultWorkFlow-Interceptor”来调用和协调这两个接口。关于Interceptor的应用将在以后的章节中详细讲述。
注意
如果由于某种原因不能从扩展ActionSupport类来创建action类又想实现ValidationAware接口的功能,Struts2提供了另一个解决方法:用ValidationAwareSupport类,它是从ActionSupport提取出来的,专门用来实现ValidationAware。
实例5-12是一个创建用户的execute方法,在创建用户之前需要检验这个用户是否已经存在了。实例5-13是使用validate重构之后的代码。可以看出,使用validate接口后,校验代码与处理流程完全解耦了,在execute方法中只专注于添加用户的业务逻辑。
【实例5-12】在execute中校验:createAction.java
01 //校验函数, 直接用code来实现校验 02 public String execute() throws Exception { 03 User existing = userDAO.findByUsername(user.getUsername()); 04 if (existing != null) { 05 //把错误信息添加到框架中去 06 addFieldError("user.username","用户已经存在了"); 07 return INPUT; //返回相应信息 08 } 09 userDAO.createUser(user); //添加相应的用户记录 10 return SUCCESS; //返回相应的信息 11 }
【代码剖析】上述代码在execute()方法中直接实现对用户的校验,根据第3行代码的结果显示不同的结果。
【实例5-13】使用Validateable接口校验:createAction.java
01 //使用校验接口, 就不在代码中直接调用校验方法 02 public void validate() { 03 // 判断此用户是否存在 04 if (user != null) { 05 User existing = userDAO.findByUsername(user.getUsername()); 06 if (existing != null) { 07 addFieldError("user.username","用户已经存在了"); 08 } 09 } 10 } 11 public String execute() throws Exception { //实现执行方法 12 userDAO.createUser(user); //添加用户相应记录 13 return SUCCESS; //返回相应信息 14 }
【代码剖析】通过使用Validateable接口,可以使校验代码与逻辑业务完全分开。
注意 代码中无须显式地调用validate()方法,也不用返回INPUT。
5.5.4 使用本地的资源文件
本节中将介绍另外两个接口:TextProvider和LocalProvider,它们都是为了使用本地的资源文件而设计的。
在Java中,用户语言和地区的信息被封装在java.util.Local类中,而action则通过定义一个com. opensymphony.xwork.LocaleProvider接口的方法,来判断使用哪个Locale获取用于显示的信息文本。这个接口中只定义了一个方法:
在ActionSupport中,这个方法的默认实现为:通过调用ActionContext.getContext ()和getLocale()方法,利用ActionContext获得locale的值(关于ActionContext的使用将在后面的章节详细描述)。Struts2通过查询HttpServletRequest对象并调用它的getLocale ()方法将Local与action调用联系起来。
在绝大多数情况下,这是一个不错的默认实现。因为用户的浏览器会在HTTP请求的头信息(request header)中指出所支持的Locale值,而以上实现正是基于此。但是action类也可以选择改写这个方法提供不同的实现,譬如从存储在数据库的用户资料中得到用户的Locale信息。
如实例5-14所示,TextProvider接口基本上由多个不同参数的getText()方法组成,而getText()方法则用于查出本地化信息文本。Struts2提供了一个类com.opensymphony.xwork2. TextProviderSupport作为TextProvider的默认。它实现使用了ResourceBundle接口的PropertyResourceBundle的实现方式,而该实现方式则使用了基于action类名的资源文件。
【实例5-14】TextProvider接口定义
01 public interface TextProvider { 02 /** 03 * 判断资源包中是否有某个键值定义 04 * 05 * @param key 06 * @return 如果存在, 返回true; 不存在, 则返回false 07 */ 08 boolean hasKey(String key); 09 10 /** 11 * 基于信息键获取信息文本, 并且是使用提供的参数填充获取的信息文本 12 * 参见{@link java.text.MessageFormat}的定义, 13 * 14 * @param key 用于搜索的资源包 15 * @return中资源中找到的文本, 如果找不到, 则返回null 16 */ 17 String getText(String key); 18 19 /** 20 * 基于信息键获取信息文本, 并且是使用提供的参数填充获取的信息文本 21 * 参见{@link java.text.MessageFormat}的定义, 22 * 23 * @param key 用于搜索的资源包 24 * @param defaultValue 在找不到所需信息的情况下所返回的默认值 25 * @return中资源中找到的文本, 如果找不到, 则返回提供的defaultValue 26 */ 27 String getText(String key, String defaultValue); 28 29 /** 30 * 基于信息键获取信息文本, 并且是使用提供的参数填充获取的信息文本 31 * 参见{@link java.text.MessageFormat}的定义, 32 * 33 * @param key 用于搜索的资源包 34 * @param defaultValue 在找不到所需信息的情况下所返回的默认值 35 * @param obj obj to be used in a {@link java.text.MessageFormat} message 36 * @return中资源中找到的文本, 如果找不到, 返回提供的defaultValue 37 */ 38 String getText(String key, String defaultValue, String obj); 39 40 /** 41 * 基于信息键获取信息文本, 并且是使用提供的参数填充获取的信息文本 42 * 参见{@link java.text.MessageFormat}的定义, 43 * @param key 用于搜索的资源包 44 * @param args 用于填充 {@link java.text.MessageFormat}的信息 45 * @return 中资源中找到的文本, 如果找不到, 返回null 46 */ 47 48 String getText(String key, List args); 49 /** 50 * 基于信息键获取信息文本, 并且是使用提供的参数填充获取的信息文本 51 * 参见{@link java.text.MessageFormat}的定义, 52 * @param key 用于搜索的资源包 53 * @param args 用于填充 {@link java.text.MessageFormat}的信息 54 * @return 中资源中找到的文本, 如果找不到, 返回null 55 */ 56 String getText(String key, String[] args); 57 58 /** 59 * 基于信息键获取信息文本, 并且是使用提供的参数填充获取的信息文本 60 * 参见{@link java.text.MessageFormat}的定义, 61 * @param key 用于搜索的资源包 62 * @param defaultValue 在找不到所需信息的情况下所返回的默认值 63 * @param args 用于填充 {@link java.text.MessageFormat}的信息 64 * @return 中资源中找到的文本, 如果找不到, 返回提供的defaultValue 65 */ 66 String getText(String key, String defaultValue, List args); 67 68 /** 69 * 基于信息键获取信息文本, 并且是使用提供的参数填充获取的信息文本 70 * 参见{@link java.text.MessageFormat}的定义, 71 * 如果找不到所需的信息, 则返回提供的默认值 72 * 这个版本的getText()方法并不是使用Actioncontext中的指栈 73 * @param key 用于搜索的资源包 74 * @param defaultValue 在找不到所需信息的情况下所返回的默认值 75 * @param args 用于填充 {@link java.text.MessageFormat}的信息 76 * @return 中资源中找到的文本, 如果找不到, 返回提供的defaultValue 77 */ 78 String getText(String key, String defaultValue, String[] args); 79 80 /** 81 * 基于信息键获取信息文本, 并且是使用提供的参数填充获取的信息文本 82 * 参见{@link java.text.MessageFormat}的定义, 83 * 如果找不到所需的信息则返回提供的默认值 84 * 这个版本的getText()方法并不是使用Actioncontext中的指栈 85 * @param key 用于搜索的资源包 86 * @param defaultValue 在找不到所需信息的情况下所返回的默认值 87 * @param args 用于填充 {@link java.text.MessageFormat}的信息 88 * @param stack 用于查找信息文本的指栈 89 * @return 中资源中找到的文本, 如果找不到, 返回提供的defaultValue 90 */ 91 String getText(String key, String defaultValue, List args, OgnlValueStack stack); 92 93 /** 94 * 基于信息键获取信息文本, 并且是使用提供的参数填充获取的信息文本 95 * 参见{@link java.text.MessageFormat}的定义, 96 * 如果找不到所需的信息, 则返回提供的默认值 97 * 这个版本的getText()方法并不是使用Actioncontext中的指栈 98 * @param key 用于搜索的资源包 99 * @param defaultValue 在找不到所需信息的情况下所返回的默认值 100 * @param args 用于填充 {@link java.text.MessageFormat}的信息 101 * @param stack 用于查找信息文本的指栈 102 * @return 中资源中找到的文本, 如果找不到返回提供的defaultValue 103 */ 1 0 4 String getText(String key, String defaultValue, String[] args, OgnlValueStack stack); 105 106 /** 107 * 获取资源包, 如 "com/acme/Foo" 108 * 109 * @param bundleName资源包名, 110 * 如 "com/acme/Foo" 111 */ 112 ResourceBundle getTexts(String bundleName); 113 114 /** 115 * 获取与实现类(通常是action)相关者资源包 116 */ 117 ResourceBundle getTexts(); 118 }
【代码剖析】对于TextProvider接口的内容,已经定义许多方法。在具体使用时,资源文件(*.properties)的文件名应该与类名相同,并且与类放在相同的目录下。
在上一节例子中错误信息“用户已经存在了”是以硬代码的形式写在Java文件中的,利用资源文件就可以改为如实例5-15所示。
【实例5-15】资源文件使用:createAction.java
01 public void validate() { 02 // 判断此用户是否存在 03 if (user != null) { 04 User existing = userDAO.findByUsername(user.getUsername()); 05 if (existing != null) { 06 addFieldError("user.username", getText("user.exists")); 07 } 08 } 09 }
【代码剖析】在上述代码中,第6行通过getText()方法来获取错误信息。
5.5.5 用ActionContext与Web容器发生联系
在Action的接口定义中,execute()方法并没有HttpServletRequest和HttpServletResponse参数。也就是说,Struts2的Action不用去依赖于任何Web容器(不像Struts1必须在Web容器中才能运行),不用与那些JavaServlet复杂的请求(Request)、响应(Response)关联在一起。但在Web应用程序开发中,往往需要在Action里直接获取请求(Request)或会话(Session)的一些信息,甚至需要直接对JavaServlet HTTP的请求、响应操作。Struts2提供了一个工具,用ActionContext对象来与Web容器发生联系。
ActionContext(com.opensymphony.xwork.ActionContext)是Action执行时的上下文,上下文可以把它看做是一个Map,它存放Action在执行时需要用到的对象,比如:上下文放有请求的参数(Parameter)、会话(Session)、Servlet上下文(ServletContext)、本地化(Locale)信息等。在每次执行Action之前都会创建新的ActionContext,ActionContext是线程安全的。也就是说,在同一个线程中ActionContext的属性是唯一的,这样的Action就可以在多线程中使用。
可以通过ActionContext的静态方法:ActionContext.getContext()来取得当前的ActionContext对象,如实例5-16所示。
【实例5-16】ActionContext的定义
01 /* 02 *创建ActionContext 03 */ 04 public static ActionContext getContext() { 05 ActionContext context = (ActionContext) actionContext.get();//创建context对象 06 if (context == null) { //判断对象context 07 OgnlValueStack vs = new OgnlValueStack(); 08 context = new ActionContext(vs.getContext()); 09 setContext(context); //设置对象context 10 } 11 return context; //返回对象context 12 }
【代码剖析】一般情况下,ActionContext都是通过如下代码来获取:
ActionContext context =(ActionContext) actionContext.get();
ActionContext是保存在一个ThreadLocal中,ThreadLocal可以命名为“线程局部变量”,它为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。这样ActionContext里的属性只会在对应的当前请求线程中可见,从而保证它是线程安全的。
ActionContext的使用非常方便,如取得request中的某个参数的值:
ActionContext context = ActionContext.getContext(); Map params = context.getParameters(); String username = (String) params.get("username");
如通过ActionContext取得的HttpSession:
Map session = ActionContext.getContext().getSession();
Struts2框架将与Web相关的很多对象重新进行了包装,比如这里就将HttpSession和request中的参数都重新包装成了Map对象,供Action使用,而不用直接和底层的HttpSession打交道。也正是框架的包装,让Action可以完全和Web层解耦。
如果Action必须直接与JavaServlet的HttpSession、HttpServletRequest等一些对象进行操作,那么就要用到ServletActionContext。com.opensymphony.webwork. ServletActionContext这个类直接继承了上面介绍的ActionContext,它提供了直接与JavaServlet相关对象访问的功能。它可以取得的对象有:
1)javax.servlet.http.HttpServletRequest:HTTPservlet请求对象。
2)javax.servlet.http.HttpServletResponse:HTTPservlet响应对象。
3)javax.servlet.ServletContext:Servlet上下文信息。
4)javax.servlet.ServletConfig:Servlet配置对象。
5)javax.servlet.jsp.PageContext:HTTP页面上下文。
取得这些对象的方法如下:
HttpServletRequest request = ServletActionContext. getRequest(); HttpSession session = ServletActionContext. getRequest().getSession();
说明
ServletActionContext和ActionContext有着一些重复的功能,给读者的建议是:ActionContext能够实现的功能,最好就不要使用ServletActionContext,让Action尽量不要直接去访问JavaServlet的相关对象。
注意
不要在Action的构造函数里使用ActionContext.getContext(),因为这个时候ActionContext里的一些值也许没有设置,这时通过ActionContext取得的值也许是null。
5.5.6 高级输入
应用程序经常使用JavaBean表示一个域中的对象,包括User、Address、Block在内的类就是这种JavaBean很好的例子。而在Web程序中,很大一部分工作都是将信息填充到这些对象中,以及从Bean中获取数据信息在网页中表现。本节将以一个完整的实例来说明Struts2在这些方面提供了哪些便利。
1)定义域对象。如实例5-17所示,User是一个典型的JavaBean,只包含属性的setter和getter方法。
【实例5-17】User.java接口定义:JavaBean
01 package register; 02 //只是一个简单的JavaBean 03 public class User { 04 //用户名 05 private String username; 06 //密码 07 private String password; 08 //电子信箱 09 private String email; 10 //年龄 11 private int age; 12 //设置属性的相应getter和setter方法 13 public String getUsername() { //关于属性username的getter和setter方法 14 return username; 15 } 16 public void setUsername(String username) { 17 this.username = username; 18 } 19 public int getAge() { //关于属性age的getter和setter方法 20 return age; 21 } 22 public void setAge(int age) { 23 this.age = age; 24 } 25 public String getPassword() { //关于属性password的getter和setter方法 26 return password; 27 } 28 public void setPassword(String password) { 29 this.password = password; 30 } 31 public String getEmail() { //关于属性email的getter和setter方法 32 return email; 33 } 34 public void setEmail(String email) { 35 this.email = email; 36 } 37 public String toString() { //重写toString()方法 38 return "username=" + username 39 + ";password=" + password 40 + ";email=" + email 41 + ";age=" + age; 42 } 43 }
【代码剖析】在上述代码中首先定义了四个属性username、password、email和age,然后为每个属性设置了setter和getter方法。
2)典型的输入界面。实例5-18是有关这个JavaBean的输入界面createUser.jsp,界面如图5.6所示。
【实例5-18】界面页面:createUser.jsp
01 <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> 02 <html> 03 <head> 04 <title>Register Example</title> 05 </head> 06 <body> <!--设置表单--> 07 <form name="register" action="createUser.action" method="post"> 08 <table border=0 width=97%> 09 <tr> 10 <td> 11 用户名: 12 </td> 13 <td> <!--用户名输入框--> 14 <input type="text" name="user.username"> 15 </td> 16 </tr> 17 <tr> 18 <td> 19 密码: 20 </td> 21 <td> <!--密码输入框--> 22 <input type="password" name="user.password"> 23 </td> 24 </tr> 25 <tr> 26 <td> 27 Email: 28 </td> 29 <td> <!--Email输入框--> 30 <input type="text" name="user.email"> 31 </td> 32 </tr> 33 <tr> 34 <td> 35 年龄: 36 </td> 37 <td> <!--年龄输入框--> 38 <input type="text" name="user.age"> 39 </td> 40 </tr> 41 <tr> 42 <td></td> 43 <td> <!--提交按钮--> 44 <input type="submit" name="submit" value="确定"> 45 </tr> 46 </table> 47 </form> 48 </body> 49 </html>
图5.6 createUser JSP界面图
【代码剖析】在上述代码中,在第14行创建了一个用户名输入框,在第22行创建了一个密码输入框,在第30行创建了一个关于Email的输入框,在第38行创建了一个用户年龄输入框,最后在第44行创建了“确定”按钮。
3)处理输入请求。这个JSP页面提交后将由一个action类CreateUserAction来处理,如实例5-19所示,这个action在struts.xml文件中的配置如实例5-20所示。
【实例5-19】处理逻辑的Action:CreateUserAction.java
01 package register; 02 import com.opensymphony.xwork2.Action; 03 public class CreateUserAction implements com.opensymphony.xwork2.Action { 04 //User 属性 05 private User user = new User(); 06 public User getUser() { //设置属性的getter方法 07 return this.user; 08 } 09 public String execute() { //实现执行方法 10 System.out.println("Begin execute ."); 11 System.out.println("User=" + user); 12 //在这里调用用户注册的业务逻辑, 比如:将注册信息存储到数据库 13 return SUCCESS; 14 } 15 }
【代码剖析】在上述代码中,首先创建了一个User对象,该对象用来存储页面传递过来的关于用户的信息,然后在execute()方法中实现逻辑功能。
【实例5-20】struts.xml文件的配置:struts.xml
01 <!--CreateUserAction在struts.xml中的配置--> 02 <package name="tutorial" extends="struts-default"> 03 <action name="createUser" class="register.CreateUserAction"> 04 <result>create-result.jsp</result> 05 </action> 06 </package>
【代码剖析】在上述代码的第3行到第5行,设置了类CreateUserAction执行成功后转向页面create-result.jsp的功能。
4)结果页面。把处理结果显示给用户,这里需要把当初输入的内容带到结果页面中来,如实例5-21所示的createResult.jsp。
【实例5-21】成功后页面:createResult.jsp
01 <%@ taglib prefix="s" uri="/struts-tags" %> 02 <html> 03 <head><title>Register result</title></head> 04 <body> 05 <table border=0 width=97%> 06 <tr> 07 <td align="left"> 08 Congratulation, your register success!<p> 09 Username:<s:property value="user.username"/><br> 10 Password:<s:property value="user.password"/><br> 11 Email:<s:property value="user.email"/><br> 12 Age:<s:property value="user.age"/><br> 13 </td> 14 </tr> 15 </table> 16 </body> 17 </html>
【代码剖析】在上述代码的第9行到第12行中通过标签<s:property>获取相应的值。
5)执行程序查看结果。单击“确定”按钮提交,CreateUser程序的执行结果如图5.7所示。
在控制台中打印输出为:
Begin execute User=username=First Users;password=123456;email=fristuser@hotmail.com;age=28
图5.7 createResult.jsp结果图
下面来详细解读这个例子。
1)如何获取输入。首先关注一下这个例子的输入:在action中并没有使用常用的request.getParameter ("username")这样的方法来获取参数,那么action中的user是如何被赋值的呢?
在这段配置文件里,tutorial包是从struts-default包继承而来,查看struts-default.xml可以发现所有的Action都用<interceptor-ref>标签设置这个Action用到的拦截器(Interceptor) ,其中有一个拦截器名词为“params”,它引用的是配置文件中的com.opensymphony.xwork.interceptor. ParametersInterceptor,这个拦截器将在CreateUserAction的execute()方法执行之前调用,作用正是将request请求的参数值通过表达式语言设置到相应CreateUserAction的模型里。
2)如何给Action类填充属性。正如本例中的createUser.jsp,它输入的值会由CreateUserAction类的getUser()和User类的setUserName()设置到这个User模型里。如图5.6所示页面输入用户名“First User”,提交表单ParametersInterceptor就会进行下面的操作:首先从请求中取得参数的名字和名字对应的值,分别为“user.username”和“First User”,根据这个名字,从OgnlValueStack中取得堆栈最上面的getUser().setUsername("First User")操作,即取得CreateUserAction对象的User模型,并设置username属性的值为“First User”。流程如图5.8所示。
图5.8 createUser输入流程图
原来Action是通过Struts2的拦截器ParametersInterceptor从提交的表单中取得请求的参数和值,再通过OgnlValueStack来执行表达式,调用Action和模型里相应的get或set方法,将从请求中取得的值设置到模型中去。createUser.jsp中Input输入框的name="user.username"也不是随便取名的,必须要遵守OGNL的命名规则。正是很多拦截器的使用,使得Action类和Web实现了完全的解耦,让Action能如此得简单、优雅。
把“user.username”这样的语句叫做表达式语言(Expression Language,EL),这是JSP 2.0提出的新规范。OGNL(Object Graph Notation Language)是Struts2提供的一种功能强大、技术成熟、应用广泛的表达式语言,将在后面的章节详细介绍。
3)如何在输出页面中获得数据。接下来再看createResult.jsp是如何输出的。这个JSP页面使用了Struts2的<property>标签库,在第4章中HelloReader的例子里也使用了这个标签库。它是一个普通的使用标签库语句,查看这个标签库的源程序(见包org.apache.struts2.views.jsp里的PropertyTag.java文件),这个类会根据value后面赋予的表达式值,去OgnlValueStack里查找这个表达式值所对应的操作。执行这个语句OgnlValueStack会根据value的值(一个表达式)“user.username”去分别调用CreateUserAction类的getUser()和User类的getUsername()方法,即getUser().getUsername(),取得的数据就是前面注册页面输入的用户名。
5.5.7 使用Model-Driven
Struts2中提供了两种Action驱动模式:Property-Driven(属性驱动)、Model-Driven(模型驱动)。
模型驱动的Action很像Struts1中的FormBean,在传递过程中有一个单独的值对象来作为参数的载体,但在Struts2中这个值对象不必再继承任何接口,只要普通JavaBean就可以充当模型部分。很多情况下Bean的定义已经存在了,而且是不能修改的(如从外部引入的类或者是已经被大量代码引用的类),如果必须实现某个接口才能作为FormBean,不得不再新增一个类,Struts2的这个改进非常及时。
Struts2提供了一种更加明显的Model-Driven方法,那就是让Action实现com.opensymphony. xwork.ModelDriven接口,这个接口有一个方法:Object getModel () ,用这个方法返回模型对象就可以了。这种做法相当显性地告诉了Struts2 model是哪个对象,如实例5-22所示。用getModel()方法代替了getUser()方法。
【实例5-22】Model-Driven下的action:CreateUserAction1.java
01 package register; 02 import com.opensymphony.xwork2.Action; 03 import com.opensymphony.xwork2.ModelDriven; 04 // Model-Driven下的action 05 public class CreateUserAction implements Action,ModelDriven { 06 //Model属性 07 private User user = new User(); //创建User类型对象 08 public User getUser() { //获取user对象 09 return this.user; 10 } 11 public String execute() { //实现执行方法 12 System.out.println("Begin execute ."); //输出相应信息 13 System.out.println("User=" + user); //输出相应的信息 14 //在这里调用用户注册的业务逻辑, 比如:将注册信息存储到数据库 15 return SUCCESS; 16 } 17 // 实现ModelDriven接口 18 @Override 19 public Object getModel() { 20 // 返回user对象 21 return user; 22 } 23 }
【代码剖析】在上述代码的第21行返回user对象。
在两个JSP文件中也不必用user.xxx来引用属性,直接使用属性名,如实例5-23和实例5-24所示。
【实例5-23】Model-Driven下的输入:createUser1.jsp
01 <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> 02 <html> 03 <head> 04 <title>Register Example</title> 05 </head> 06 <body> 07 <form name="register" action="createUser1.action" method="post"> 08 <table border=0 width=97%> 09 <tr> 10 <td> 11 用户名: 12 </td> 13 <td> <!--关于用户名输入框--> 14 <input type="text" name="username"> 15 </td> 16 </tr> 17 <tr> 18 <td> 19 密码: 20 </td> 21 <td> <!--关于密码输入框--> 22 <input type="password" name="password"> 23 </td> 24 </tr> 25 <tr> 26 <td> 27 Email: 28 </td> 29 <td> <!--关于Email输入框--> 30 <input type="text" name="email"> 31 </td> 32 </tr> 33 <tr> 34 <td> 35 年龄: 36 </td> 37 <td> <!--关于年龄输入框--> 38 <input type="text" name="age"> 39 </td> 40 </tr> 41 <tr> 42 <td></td> 43 <td> <!--关于提交按钮--> 44 <input type="submit" name="submit" value="确定"> 45 </tr> 46 </table> 47 </form> 48 </body> 49 </html>
【代码剖析】在上述代码的第14行、第22行、第30行和第38行中,对于标签<input>属性name的值现在只需要类user的属性就可以。
【实例5-24】Model-Driven下的结果页面:creatReult1.jsp
01 <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> 02 <%@ taglib prefix="s" uri="/struts-tags" %> 03 <html> 04 <head><title>Register result</title></head> 05 <body> 06 <table border=0 width=97%> 07 <tr> 08 <td align="left"> 09 添加用户成功<p> 10 用户名:<s:property value="username"/><br> <!--显示用户名--> 11 密码:<s:property value="password"/><br> <!--显示密码--> 12 Email:<s:property value="email"/><br> <!--显示Email--> 13 年龄:<s:property value="age"/><br> <!--显示年龄--> 14 </td> 15 </tr> 16 </table> 17 </body> 18 </html>
【代码剖析】在上述代码的第10行到第13行中,对于标签< s:property >属性value的值,现在只需要类user的属性就可以。
同时要在struts.xml配置文件为这个action指定一个modelDriven拦截器,如实例5-25所示。
【实例5-25】Model-Driven配置:struts.xml
01 <action name="createUser1" class="register.CreateUserAction1"> 02 <result>createResult1.jsp</result> 03 <!--必须有modelDrivenStack才能发挥Model-Driven的作用--> 04 <interceptor-ref name="modelDrivenStack" /> 05 </action>
图5.9展示了使用ModelDriven接口后创建用户的流程。
5.5.8 使用Property-Driven
Property-Driven就是Action将直接用自己的字段来充当FormBean的功能。在第4章中,HelloReader这个例子就是采用的这种方法,在Action中直接包含了message属性和它的set、get方法。它一般用在页面表单比较简单的情况下,而且可以直接把属性作为Action的字段,这样就不用再另写FormBean,减少了重复代码。
图5.9 createUser输入流程图
上一节的例子如果使用Property-Driven方法,那就是将User与action类合并定义,把User中的属性值直接转移到action中去,在配置文件中也不必再增加modelDriven这个过滤器,如实例5-26所示。
【实例5-26】Property-Driven模式的Action:CreateUserAction2.java
01 package register; 02 import com.opensymphony.xwork2.Action; 03 import com.opensymphony.xwork2.ModelDriven; 04 // Property-Driven模式的Action类 05 public class CreateUserAction2 implements Action { 06 //直接把域对象的属性转移到Action中 07 private String username; 08 private String password; 09 private String email; 10 private int age; 11 //关于设置所有属性的getter和setter方法 12 public String getUsername() { //关于username变量的getter和setter方法 13 return username; 14 } 15 public void setUsername(String username) { 16 this.username = username; 17 } 18 public int getAge() { //关于age变量的getter和setter方法 19 return age; 20 } 21 public void setAge(int age) { 22 this.age = age; 23 } 24 25 public String getPassword() { //关于password变量的getter和setter方法 26 return password; 27 } 28 public void setPassword(String password) { 29 this.password = password; 30 } 31 public String getEmail() { //关于Email变量的getter和setter方法 32 return email; 33 } 34 public void setEmail(String email) { 35 this.email = email; 36 } 37 public String toString() { //改写toString()方法 38 return "username=" + username 39 + ";password=" + password 40 + ";email=" + email 41 + ";age=" + age; 42 } 43 public String execute() { //重写执行方法 44 System.out.println("Begin execute。"); 45 System.out.println("User=" + this); 46 //在这里调用用户注册的业务逻辑, 比如:将注册信息存储到数据库 47 return SUCCESS; 48 } 49 }
【代码剖析】在上述代码中由于使用Property-Driven模式,所以把关于类user定义的内容也合并到Action内容中。
Property-Driven和Model-Driven模式的比较:
从上面改造后的例子可以感觉到,Property-Driven驱动模式似乎更加简单,无须再实现ModelDriven接口,也减少了一个Model类,struts.xml也不必配置modelDriven拦截器。出于对框架灵活性的考虑,Struts2 提供了以上两种驱动模式。使用Property-Driven模式似乎更加简单自由,而使用Model-Driven模式又似乎更加清晰,它们各有优势,不能完全说哪种更好,以解决实际问题更方便来选择采用哪种模式。
技巧
笔者给出的原则是,如果程序中已经存在了这样一个域对象或者需要定义这样一个域对象,那当然选择Model-Driven模式,没有必要将这个Bean的逻辑在action中再拷贝一遍。如果需要输入的参数很少(3个以内),而且参数组合不能明显构成一个实体逻辑,可考虑使用Property-Driven。