Struts2的Action
对于Struts2应用中的开发者来说,Action才是整个应用系统的核心,开发者需要提供大量的Action类,并且在struts.xml文件中对Action类进行配置,Action中包含了对用户请求的处理逻辑,因此,我们也称Action为业务控制器.
1) 实现Action实现类
相对于Struts1而言,Struts2采用的低侵入式的设计,Struts2不要求Action类继承任何的Struts2基类,或者实现任何的Struts2的接口,在这种设计方式下,Struts2的Action类是一个非常普通的POJO(但是通常会包含一个没有参数的execute()方法),从而,有很好的代码复用性.
Struts2通常直接使用Action来封装HTTP请求参数,因此,Action类里面还应该包含与请求参数相对应的属性,并且为这些属性提供getter和setter方法.
比如说,用户请求包含user和password两个请求参数,那么Action就应该提供user和两个属性来封装用户的请求参数,并且为user和password提供对应的getter和setter方法,西面是处理该请求的Action的代码片段:
package com.supermos.app.Action;
//处理用户请求的Action类,这个只是一个POJO类
//不需要继承任何的基类,不需要实现任何的接口
public class LoginAction {
//提供两个属性来封装HTTP请求的参数
private String user;
private String password;
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
//Action类默认来处理用户请求的方法,execute
public String execute(){
//处理结束,返回一个字符串,这个字符串将对应视图物理资源
return "success";
}
}
上面的Action类只是一个普通的Java类,这个Java类提供了两个属性,user和password,这个两个属性分别对应两个HTTP请求参数.
(即使Action需要处理的请求包含user和password两个HTTP请求参数,Action类也是可以不包含user和password两个属性的,因为系统是通过对应的getter和setter方法来处理请求参数的,而不是通过属性的名字来处理请求参数的,也就是说,如果包含user的HTTP请求参数,Action类里面是否包含user属性并不重要,重要的是要包含void setUser(String user)和String getUser()这两个方法)
Action类的属性,不仅可以用于封装请求参数,还可以用于封装处理结果.比如在前面的Action代码中看到的,如果希望将服务器提示的”登录成功”在下一个页面中输出,那么,我们可以在Action类中增加一个tip属性,并且为该属性提供对应的getter和setter方法,也就是为Action类增加如下的代码片段:
//封装服务器提示的tip属性
private String tip;
public String getTip() {
return tip;
}
public void setTip(String tip) {
this.tip = tip;
}
一旦在Action中设置了tip属性的值,我们就可以在下一个页面当中使用struts2标签来输出该属性的值,在jSP页面当中输出tip属性值的代码片段如下:
<!-- 使用Struts2标签来输出tip属性的值 -->
<s:property value=”tip”/>
系统不会严格区分Actino里面的那个属性是用来封装请求参数的属性,那个事用来封装结果的属性,对于系统来说,封装请求参数和封装处理结果的属性石完全平等的.如果用户的HTTP请求里面包含了tip的请求参数,系统就会调用Action的void setTip(String tip)方法,通过这种方式,名为tip的请求参数就可以传给Action实例,如果Action类里面没有包含对应的方法,那么名字为tip的请求参数也就无法传入该Action.
同样,在JSP页面中输出的Action属性的时候,它也不会区分该属性是用来封装请求参数的还是用于封装处理结果的属性,所以说,使用Struts2的标签既可以输出Action的处理结果,也可以输出Http请求参数值.
从上面的代码可以看到,需要在JSP页面中输出的处理结果是一个非常简单的字符串,可以使用<s:property…/>标签来控制输出.实际上,Action类里面可以封装非常复杂的属性,包括其他用户自己定义的属性,数组,集合对象和Map对象等等.对于这些复杂类型的输出,一样可以通过Struts2的标签来完成.
为了让用户开发的Action来更加的规范,Struts2提供了一个Action接口,这个接口定义了Struts2的Action处理类应该实现的规范,下面是Action接口的代码:
public interface Action {
public static final String SUCCESS="success";
public static final String ERROR="error";
public static final String INPUT="input";
public static final String LOGIN="login";
public static final String NONE="none";
public String execute()throws Exception;
}
上面的Action接口里面只定义了一个execute方法,该接口的规范规定了Action类应该包含一个execute方法,该方法的作用是返回一个字符串,除此之外,该接口还定义了5个字符串常量,他们的作用是统一execute方法的返回值.
比如说,当Action处理完用户的请求成功之后,有人喜欢返回welcome这种字符串,有的人喜欢返回success字符串…这样不利于项目的统一管理.Struts2的Action定义上面的5个字符串:error,none,input,longin和success等等分别代表了特定的含义,当然,如果开发者依然希望使用特定的字符串做为逻辑视图的名字,开发者依然可以返回自己的视图.
另外,Struts2还提供了Action类的一个实现类:ActionSupport,下面是Action类的代码:
package com.opensymphony.xwork2;
import com.opensymphony.xwork2.util.ValueStack;
import com.opensymphony.xwork2.util.logging.Logger;
import com.opensymphony.xwork2.util.logging.LoggerFactory;
import java.io.Serializable;
import java.util.*;
/**
* Provides a default implementation for the most common actions.
*/
//系统提供的ActionSupport类
public class ActionSupport implements Action, Validateable, ValidationAware, TextProvider, LocaleProvider, Serializable {
protected static Logger LOG = LoggerFactory.getLogger(ActionSupport.class);
private final transient TextProvider textProvider = new TextProviderFactory().createInstance(getClass(), this);
private final ValidationAwareSupport validationAware = new ValidationAwareSupport();
//收集校验错误的方法
public void setActionErrors(Collection<String> errorMessages) {
validationAware.setActionErrors(errorMessages);
}
//返回校验错误的方法
public Collection<String> getActionErrors() {
return validationAware.getActionErrors();
}
public void setActionMessages(Collection<String> messages) {
validationAware.setActionMessages(messages);
}
public Collection<String> getActionMessages() {
return validationAware.getActionMessages();
}
/**
* @deprecated Use {@link #getActionErrors()}.
*/
@Deprecated public Collection<String> getErrorMessages() {
return getActionErrors();
}
/**
* @deprecated Use {@link #getFieldErrors()}.
*/
@Deprecated public Map<String, List<String>> getErrors() {
return getFieldErrors();
}
//设置表单域校验错误信息
public void setFieldErrors(Map<String, List<String>> errorMap) {
validationAware.setFieldErrors(errorMap);
}
//返回表单域校验错误信息
public Map<String, List<String>> getFieldErrors() {
return validationAware.getFieldErrors();
}
//控制locale的相关信息
public Locale getLocale() {
ActionContext ctx = ActionContext.getContext();
if (ctx != null) {
return ctx.getLocale();
} else {
LOG.debug("Action context not initialized");
return null;
}
}
public boolean hasKey(String key) {
return textProvider.hasKey(key);
}
//返回国际化信息的方法
public String getText(String aTextName) {
return textProvider.getText(aTextName);
}
public String getText(String aTextName, String defaultValue) {
return textProvider.getText(aTextName, defaultValue);
}
public String getText(String aTextName, String defaultValue, String obj) {
return textProvider.getText(aTextName, defaultValue, obj);
}
public String getText(String aTextName, List<Object> args) {
return textProvider.getText(aTextName, args);
}
public String getText(String key, String[] args) {
return textProvider.getText(key, args);
}
public String getText(String aTextName, String defaultValue, List<Object> args) {
return textProvider.getText(aTextName, defaultValue, args);
}
public String getText(String key, String defaultValue, String[] args) {
return textProvider.getText(key, defaultValue, args);
}
public String getText(String key, String defaultValue, List<Object> args, ValueStack stack) {
return textProvider.getText(key, defaultValue, args, stack);
}
public String getText(String key, String defaultValue, String[] args, ValueStack stack) {
return textProvider.getText(key, defaultValue, args, stack);
}
public ResourceBundle getTexts() {
return textProvider.getTexts();
}
public ResourceBundle getTexts(String aBundleName) {
return textProvider.getTexts(aBundleName);
}
public void addActionError(String anErrorMessage) {
validationAware.addActionError(anErrorMessage);
}
public void addActionMessage(String aMessage) {
validationAware.addActionMessage(aMessage);
}
public void addFieldError(String fieldName, String errorMessage) {
validationAware.addFieldError(fieldName, errorMessage);
}
public String input() throws Exception {
return INPUT;
}
public String doDefault() throws Exception {
return SUCCESS;
}
/**
* A default implementation that does nothing an returns "success".
* <p/>
* Subclasses should override this method to provide their business logic.
* <p/>
* See also {@link com.opensymphony.xwork2.Action#execute()}.
*
* @return returns {@link #SUCCESS}
* @throws Exception can be thrown by subclasses.
*/
//默认的处理用户请求的方法,直接返回success字符串
public String execute() throws Exception {
return SUCCESS;
}
public boolean hasActionErrors() {
return validationAware.hasActionErrors();
}
public boolean hasActionMessages() {
return validationAware.hasActionMessages();
}
public boolean hasErrors() {
return validationAware.hasErrors();
}
public boolean hasFieldErrors() {
return validationAware.hasFieldErrors();
}
/**
* Clears field errors. Useful for Continuations and other situations
* where you might want to clear parts of the state on the same action.
*/
public void clearFieldErrors() {
validationAware.clearFieldErrors();
}
/**
* Clears action errors. Useful for Continuations and other situations
* where you might want to clear parts of the state on the same action.
*/
public void clearActionErrors() {
validationAware.clearActionErrors();
}
/**
* Clears messages. Useful for Continuations and other situations
* where you might want to clear parts of the state on the same action.
*/
public void clearMessages() {
validationAware.clearMessages();
}
/**
* Clears all errors. Useful for Continuations and other situations
* where you might want to clear parts of the state on the same action.
*/
public void clearErrors() {
validationAware.clearErrors();
}
/**
* Clears all errors and messages. Useful for Continuations and other situations
* where you might want to clear parts of the state on the same action.
*/
public void clearErrorsAndMessages() {
validationAware.clearErrorsAndMessages();
}
/**
* A default implementation that validates nothing.
* Subclasses should override this method to provide validations.
*/
//包含空的输入校验方法
public void validate() {
}
@Override public Object clone() throws CloneNotSupportedException {
return super.clone();
}
public void pause(String result) {
}
}
正如上面的代码看到的,该Action是一个默认的Action类,该类里面已经提供了很多默认的方法,这些默认的方法包括获取国际化信息的方法,数据校验的方法,默认的处理用户请求的方法等等,实际上,ActionSupport类是Struts2默认的Action处理类,如果让开发者的Action类继承该ActionSupport来,就会大大的简化Action的开发.
(当用户配置的Action类没有指定class属性的时候,系统会自动使用ActionSupport类作为默认的Action处理类)
2) Action访问Servlet API
Struts2的Action并没有直接和任何的Servlet API进行耦合,这是Struts2的一个改良之处,因为Action类不再和Servlet API耦合,就可以更加轻松的去测试该Action,就不用mock去模拟数据测试了.
但是对于web应用的控制器而言,不访问Servlet API几乎是不可能的,比如说跟踪HTTP Session的状态等等.Struts2框架提供了一种更加轻松的方式来访问Servlet API,Web应用中通常需要访问的Servlet API就是HttpServletRequest,HttpSession和HttpContext,这三个类分别代表JSP内置对象里面的request,session和application.
Struts2提供了一个ActionContext类,Struts2的Action可以通过该类来访问Servlet API,下面是ActionContext类中包含的几个常用的方法:
l Object get(Object key):该方法类似于调用HttpServletRequest的getAttribute(String name)方法.
l Map getApplication():返回一个Map对象,该对象模拟了该应用的ServletContext实例.
l static ActionContext getContext():静态方法,获取系统的ActionContext实例.
l Map getParameters():获取所有的请求参数,类似于调用HttpServletRequest对象的getParameterMap方法.
l Map getSession():返回一个Map对象,这个Map对象模拟了HttpSession实例.
l void setApplication(Map application):直接传入一个Map实例,将该Map实例里面的key-value对转换成application的属性名属性值.
l void setSession(Map session):直接传入一个Map实例,将该Map实例里面的key-value对转换成session的属性名和属性值.
下面的一个应用将在Action类中通过ActionContext访问ServletAPI:
① 系统提交请求到LoginAction,LoginAction的代码如下:
package com.supermos.app.Action;
import com.opensymphony.xwork2.Action;
import com.opensymphony.xwork2.ActionContext;
public class LoginAction implements Action{
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String execute(){
//获取ActionContext实例,通过该实例访问Servlet API
ActionContext ctx=ActionContext.getContext();
//获取ServletContext里面的counter属性
Integer counter=(Integer)ctx.getApplication().get("counter");
//如果counter的属性为空,就设置该counter的属性为1
if(counter==null){
counter=1;
}
//否则,将counter加1
else{
counter=counter+1;
}
//将增加1之后的counter的值设置为counter属性
ctx.getApplication().put("counter", counter);
//将登陆用的username属性设置为一个HttpSession属性
ctx.getSession().put("user",this.getUsername());
if("supermos".equalsIgnoreCase(this.getUsername())&&"ziwen".equalsIgnoreCase(this.getPassword())){
//直接设置HttpServletRequest属性,下面的代码的作用类似于设置HttpServletRequest属性
//request.setAttribute("tip","服务器提示,您已经成功登陆");
ctx.put("tip", "服务器提示,您已经成功登陆");
return this.SUCCESS;
}else{
//直接设置HttpServletRequest属性
ctx.put("tip","服务器提示,您登陆失败,请检查您的用户名和密码");
return this.ERROR;
}
}
}
② 上面的Action访问了HttpServletRequest的属性,也访问了HttpSession的属性,还让问了HttpContext的属性,将该Action配置在struts.xml文件中,struts.xml文件的代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<package name="supermos" extends="struts-default">
<action name="login" class="com.supermos.app.Action.LoginAction">
<result name="success">/success.jsp</result>
<result name="error">/error.jsp</result>
<result name="input">/login.jsp</result>
</action>
</package>
</struts>
③ 如果浏览者在页面中输入用户名supermos密码ziwen的时候,将提交给LoginAction来进行处理,该Action的execute方法分别设置了HttpServletRequest属性,HttpSession属性和ServletContext属性,为了验证是否成功设置了这三个属性,我们在success.jsp页面中访问这三个对象的属性:代码如下:
<%@ page language="java" pageEncoding="utf-8"%>
<%@ taglib uri="/struts-tags" prefix="s" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>登录系统</title>
</head>
<body>
<!-- 通过表达式来访问ServletContext对象的属性 -->
本站的访问次数为:${applicationScope.counter}<br>
<!-- 通过表达式来访问HttpSession对象的属性 -->
${sessionScope.user },您已经登录!<br>
${requestScope.tip }
</body>
</html>
显然,相比Struts1中直接访问ServletAPI,Strut2通过ActionContext访问ServletAPI更加的优雅,让Action彻底从ServletAPI中分离出来,从而可以允许该Action脱离web容器.最大的好处就是可以脱离web容器测试Action.
3) Action直接访问ServletAPI
虽然Struts2提供了ActionContext来访问ServletAPI,但是这种方式毕竟不能够直接获得ServletAPI的实例,为了在Action中直接访问ServletAPI,Struts2还提供了如下的几个接口:
l ServletContextAware:实现该接口的Action可以直接访问Web应用的ServletContext实例.(aware[əˈwɛə]意识到的,知道的)
l ServletRequestAware:实现该接口的Action可以直接访问服务器相应的HttpServletRequest实例
l ServletResponseAware:实现该接口的Action可以直接访问服务器相应的HttpServletResponse实例.
下面以ServletResponseAware为例子,介绍如何在Action中访问HttpServletResponse对象,本应用通过HttpServletResponse为系统添加Cookie对象.下面是该上面例子的Action的修改:
package com.supermos.app.Action;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts2.interceptor.ServletResponseAware;
import com.opensymphony.xwork2.Action;
import com.opensymphony.xwork2.ActionContext;
public class LoginAction implements Action,ServletResponseAware{
//需要访问的HttpServletResponse对象
private HttpServletResponse response;
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
//实现ServletResponseAware接口所必须要实现的一个方法
public void setServletResponse(HttpServletResponse response) {
//在该方法内就可以访问web应用对客户的响应对象
this.response = response;
}
public String execute(){
//获取ActionContext实例,通过该实例访问Servlet API
ActionContext ctx=ActionContext.getContext();
//获取ServletContext里面的counter属性
Integer counter=(Integer)ctx.getApplication().get("counter");
//如果counter的属性为空,就设置该counter的属性为1
if(counter==null){
counter=1;
}
//否则,将counter加1
else{
counter=counter+1;
}
//将增加1之后的counter的值设置为counter属性
ctx.getApplication().put("counter", counter);
//将登陆用的username属性设置为一个HttpSession属性
ctx.getSession().put("user",this.getUsername());
if("supermos".equalsIgnoreCase(this.getUsername())&&"ziwen".equalsIgnoreCase(this.getPassword())){
//直接设置HttpServletRequest属性,下面的代码的作用类似于设置HttpServletRequest属性
//request.setAttribute("tip","服务器提示,您已经成功登陆");
ctx.put("tip", "服务器提示,您已经成功登陆");
//创建一个user对象
Cookie c=new Cookie("user",this.getUsername());
//设置cookie对象的最大的生存时间
c.setMaxAge(60*60);
//使用HttpServletResponse来添加Cookie对象
response.addCookie(c);
return this.SUCCESS;
}else{
//直接设置HttpServletRequest属性
ctx.put("tip","服务器提示,您登陆失败,请检查您的用户名和密码");
return this.ERROR;
}
}
}
通过查看Struts2的API的相关文档,发现,实现ServletResponseAware,仅仅需要实现如下的方法:
public void setServletResponse(HttpServletResponse response)
实现了上面的方法的时候,该方法内有一个HttpServletResponse参数,该参数就代表了web应用中对客户端的一个相应对象,并将该对象设置成Action的成员属性,从而允许在execute方法中访问该HttpServletResponse对象.
于此类似的是,如果一个Action实现了ServletRequestAware接口,就必须要实现如下的方法:
public void setServletRequest(HttpServletRequest request)
通过实现上面的方法,从而允许Action直接访问HttpServletRequest对象.
为了直接访问ServletAPI,Struts2提供了一个ServletActionContext,这个类包含了以下几个静态的方法:
l static PageContext getPageContext():取得Web应用的PageContext对象.
l static HttpServletRequest gettRequest():取得Web应用的HttpServletRequest对象
l static HttpServletResponse getResponse():取得Web应用的HttpServletResponse对象.
l static ServletContext getServletContext():取得Web应用的ServletContext对象
借助于ServletActionContext这个类的帮助,开发者也可以直接在Action中直接访问ServletAPI,却可以避免Action类需要实现XXXAware接口-虽然如此,但该Action却依然需要与ServletAPI直接耦合,一样不利于程序的解耦.
(可以通过ServletActionContext类的帮助,从而以更加简单的方式来直接的访问ServletAPI)
当该Action处理完用户的请求之后,web应用就向客户机中添加了一个Cookie对象下面的JSP页面通过表达式来访问Cookie的值,该JSP页面的代码如下:
<%@ page language="java" pageEncoding="utf-8"%>
<%@ taglib uri="/struts-tags" prefix="s" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>登录系统</title>
</head>
<body>
<!-- 通过表达式来访问ServletContext对象的属性 -->
本站的访问次数为:${applicationScope.counter}<br>
<!-- 通过表达式来访问HttpSession对象的属性 -->
${sessionScope.user },您已经登录!<br>
${requestScope.tip }
<h2>============传说中的分割线,以下为了验证Cookie===============</h2>
<!-- 通过表达式语言来读取Cookie的值 -->
从系统中读取的Cookie值:${cookie.user.value}
</body>
</html>
必须应该指出的问题是,虽然可以在Action中获取HttpServletResponse,但是如果希望HttpServletResponse来生成服务器相应是不可能的,因为Action只是控制器,也就是说,如果在Action中书写如下的代码response.getWriter().println(“Helloworld”);上面的代码在标准的Servlet中会生成对客户端的输出,但是在Struts2的Action中,就没有任何的实际意义了.
(提示:即使我们在Struts2的Action类中获得了HttpServletResponse的对象,也不要尝试直接在Action中生成对客户端的输出.!!!!)
4) 配置Action
一旦提供了Action的实现类之后,我们就可以在struts.xml当中对该Action进行配置,配置Action就是让Struts2容器知道该Action的存在,并且可以调用该Action来处理用户的请求,因此,我们认为,Action是Struts2的基本”程序单位”.
Struts2使用包来组织Action,因此将Action定义是放在包定义下面来完成的,定义Action通过使用package下的action子元素来完成,定义Action的时候,至少需要制定该Action的name属性,该name属性既是该Action的名字,也是该Action需要处理的URL的前半部分.
(Struts2的Action名字就是它所要处理的URL的前半部分,与Struts1不同,Struts1中Action配置中的name属性指定的是该Action关联的ActionForm,而path属性才是该Action要处理的URL,可以理解,Struts2中的Action的name属性就等同于Struts1中Action的path属性)
除此之外,通常还要为action指定一个class属性,其中class属性指定了改Action的实现类.
(该class属性并不是必需的,如果我们不为<action…/>元素指定class属性,系统就会默认使用系统的ActionSupport类来进行处理)
因此,一个Action的配置片段如下:
<package>
<!—配置处理用户请求的Action,实现类是com.supermos.app.LoginAction -->
<action name=”login” class=”com.supermos.app.LoginAction”/>
<package>
Action只是一个控制器,它并不直接对浏览者产生任何的响应,因此Action处理完用户的请求之后,Action需要将制定的视图资源呈现给用户.因此配置Action的时候,应该配置逻辑视图和物理视图资源之间的映射.
配置逻辑视图和物理视图之间的映射关系是通过<result…/>元素来定义的,每个<result…/>元素定义逻辑视图和物理视图之间的一次映射关系.
完整的Action配置定义如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<package name="supermos" extends="struts-default">
<action name="login" class="com.supermos.app.Action.LoginAction">
<result name="success">/success.jsp</result>
<result name="error">/error.jsp</result>
<result name="input">/login.jsp</result>
</action>
</package>
</struts>
定义<result../>元素的时候,需要指定name属性,通常name属性都是有字母和数字组成的,如果要在name属性中使用斜线/,则需要指定Struts2框架允许ActionName中出现斜线,设置允许Action name中出现斜线是通过struts.enable.SlashesInActionNames常量来制定,设置该常量的值为true,既允许Struts2框架中使用斜线.
(关于Action的name属性的命名规则,Action的name命名是非常灵活的,但如果name属性分配一个带点.或者带中划线-的值,比如说my.user或者是my-action等等,则可能会引发一些未知的一场.因此不推荐在Action的name中使用点和中划线)