笔者不知道该用哪个词来形容ValueStack、ActionContext等可以在Struts2中用来存放数据的类。这些类使用的范围不同,得到的方法也不同,下面就来一一介绍。
声明:本文参考Struts2版本为2.3.1.2,内容仅供参考,限于笔者水平有限,难免有所疏漏,望您能友善指出。本文发表于ITEYE,谢绝转载。
ValueStack在中文版的《Struts2深入浅出》一书中译作“值栈”。其本身数据结构是一个栈,使用者可以把一些对象(又称作bean)存入值栈中,然后使用动态的表达式来读取bean的属性,或者对bean进行一些其他操作。由于值栈中可能有多个bean,值栈会按bean出栈的顺序依次尝试使用动态的表达式来读取值,直到成功读取值为止。在Struts2中,默认的值栈实现是OgnlValueStack,即默认使用Ognl这个动态表达式语言来读取值。
在Struts2执行一次请求的过程中,Struts2会把当前的Action对象自动放入值栈。这样,在渲染JSP时,JSP里的代码使用<s:property value="..."/>之类标签中的Ognl表达式会直接作用于Action对象,从而方便的读取Action的属性。
总之,值栈主要目的是为了让JSP内能方便的访问Action的属性。
// 此类为一个封装数据的简单类,在下面的例子会用到 public class Person { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }
// 本类将演示拦截器中对值栈的操作 public class MyInterceptor extends AbstractInterceptor { public String intercept(ActionInvocation invocation) throws Exception { // 获得值栈 ValueStack valueStack = invocation.getStack(); // 存入值 Person person = new Person(); valueStack.push(person); // 执行表达式获取值 String name = (String) valueStack.findValue("name"); // 其他代码 return invocation.invoke(); } }
// 本类将演示在Action中对值栈进行操作 public class MyAction extends ActionSupport { @Override public String execute() throws Exception { // 获得值栈 ValueStack valueStack = ActionContext.getContext().getValueStack(); // 存入值 Person person = new Person();// 这是之前例子中定义的类 valueStack.push(person); // 执行表达式获取值 String name = (String) valueStack.findValue("name"); // 其他代码 // ...... return SUCCESS; } // 以下定义的属性供接下来的JSP例子使用 private String message; private Person person; private List<Person> personList; public String getMessage() { return message; } public Person getPerson() { return person; } public List<Person> getPersonList() { return personList; } }
<%@page contentType="text/html" pageEncoding="UTF-8"%> <%@taglib uri="/struts-tags" prefix="s" %> <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>JSP Page</title> </head> <body> <!-- 本JSP将演示在JSP中对值栈的使用 --> <!-- 本JSP为MyAction对应的JSP --> <!-- 由于Action已经被存入的值栈,所以可以调用Action的属性 --> <!-- 使用下面的标签和表达式来显示MyAction的message属性 --> <s:property value="message"/> <!-- 使用下面的标签和表达式来调用Action的getText(...)方法,参数为MyAction的message属性 --> <s:property value="getText(message)"/> <!-- 默认情况下传递给cssClass的是字符串常量。可以使用“%{}”来启用Ognl,这样,传递给cssClass的就不是字符串常量"message",而是上面所说的message的值 --> <s:div cssClass="%{message}"/> <!-- 使用s:push标签来将对象放入值栈,如下 --> <s:push value="person"> <!-- 在此s:push标签内,值栈的栈顶元素为person,栈顶第二层为action <!-- 在标签内直接调用person的属性(而不是Action的属性),如下 --> <s:property value="name"/> <!-- 在标签内也可以使用MyAction的属性,值栈会依次先查找Person是否有该属性,由于没找到,会再MyAction中再查找,如下 --> <s:property value="message"/> <!-- 可以使用“[0]”、“[1]”等指定从值栈的哪一层开始查找 --> <!-- 此时,使用“[0]”表示从Person开始查找,当然还是找不到,值栈就接着到MyAction中查找,如下 --> <s:property value="[0].message"/> <!-- 此时,使用“[1]”将从MyAction开始查找,而跳过了person,如下 --> <s:property value="[1].message"/> <!-- 想要访问栈顶元素本身使用关键字“top”,比如,下面的top就代表栈顶的person,如下 --> <s:property value="top"/> <!-- 或者如下 --> <s:property value="[0].top"/> <!-- 想要访问MyAction本身的话使用如下写法 --> <s:property value="[1].top"/> </s:push> <!-- 此时person已被移出值栈,再使用如下标签和表达式将无法得到值 --> <!--<s:property value="name"/>--> <!-- iterator标签会把list的每个元素依次存入栈顶,如下 --> <s:iterator value="personList"> <!-- 获得List每个元素中的name属性 --> <s:property value="name"/> </s:iterator> </body> </html>
ActionContext是Action的上下文,Struts2自动在其中保存了一些在Action执行过程中所需的对象,比如session, parameters, locale等。Struts2会根据每个执行HTTP请求的线程来创建对应的ActionContext,即一个线程有一个唯一的ActionContext。因此,使用者可以使用静态方法ActionContext.getContext()来获取当前线程的ActionContext,也正是由于这个原因,使用者不用去操心让Action是线程安全的。
无论如何,ActionContext都是用来存放数据的。Struts2本身会在其中放入不少数据,而使用者也可以放入自己想要的数据。ActionContext本身的数据结构是映射结构,即一个Map,用key来映射value。所以使用者完全可以像使用Map一样来使用它,或者直接使用Action.getContextMap()方法来对Map进行操作。
Struts2本身在其中放入的数据有ActionInvocation、application(即ServletContext)、conversionErrors、Locale、action的name、request的参数、HTTP的Session以及值栈等。完整的列表请参考它的Javadoc(本文附录有对它包含内容的讨论)。
由于ActionContext的线程唯一和静态方法就能获得的特性,使得在非Action类中可以直接获得它,而不需要等待Action传入或注入。需要注意的是,它仅在由于request而创建的线程中有效(因为request时才创建对应的ActionContext),而在服务器启动的线程中(比如fliter的init方法)无效。由于在非Action类中访问其的方便性,ActionContext也可以用来在非Action类中向JSP传递数据(因为JSP也能很方便的访问它)。
// 本类将演示拦截器中对ActionContext的操作 public class MyInterceptor extends AbstractInterceptor { public String intercept(ActionInvocation invocation) throws Exception { // 获得ActionContext ActionContext actionContext = invocation.getInvocationContext(); // 存入值 Person person = new Person(); actionContext.put("person", person); // 获取值 Object value = actionContext.get("person"); // 获取HttpServletRequest HttpServletRequest request = (HttpServletRequest) actionContext.get(StrutsStatics.HTTP_REQUEST); // 获取request的Map,即HttpServletRequest.getAttribute(...)和HttpServletRequest.setAttribute(...)所操作的值 Map requestMap = (Map) actionContext.get("request"); // 其他代码 // ...... return invocation.invoke(); } }
// 本类将演示在Action中对ActionContext进行操作 public class MyAction extends ActionSupport { @Override public String execute() throws Exception { // 获得值栈 ActionContext actionContext = ActionContext.getContext(); // 存入值 Person person = new Person();// 这是之前例子中定义的类 actionContext.put("person", person); // 获取值 Object object = actionContext.get("person"); // 其他代码 // ...... return SUCCESS; } }
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>JSP Page</title> </head> <body> <!-- 本JSP将演示在JSP中对ActionContext的使用 --> <!-- 本JSP为MyAction对应的JSP --> <!-- 由于Action中已经向ActionContext存入了key为"person"的值,所以可以使用“#person”来获取它,如下 --> <s:property value="#person"/> <!-- 获得person的name属性,如下 --> <s:property value="#person.name"/> <!-- 获得Struts2在ActionContext中存入的值,比如request的Map,如下 --> <s:property value="#request"/> <!-- 获得Struts2在ActionContext中存入的值,比如session的Map,如下 --> <s:property value="#session"/> <!-- 获得Struts2在ActionContext中存入的值,request请求传递的GET参数或POST参数的Map,如下 --> <s:property value="#parameters"/> <!-- 以下演示在JSP中把值存入ActionContext中 --> <!-- 存入一个字符串"myName",key为"myKey",如下 --> <s:set value="%{'myName'}" var="myKey"/> <!-- 使用s:bean标签来创建一个对象,并把它存入ActionContext中,key为myObject,如下 --> <s:bean name="com.example.Person" var="myObject"/> <!-- 之后就可以用“#”来读取它们,如下 --> <s:property value="#myKey"/> <s:property value="#myObject"/> </body> </html>
Struts2中提供了两种对request的操作:一种是Web服务器提供的HttpServletRequest类,这和传统Java Web项目中的操作request的方式相同;另一种是一个“request的Map”,即封装了HttpServletRequest的attributes的映射类,操作该Map相当于操作HttpServletRequest的attributes。之所以提供了Map的操作方式,一是方便操作,二是能方便使用Ognl在JSP标签中读取request。无论如何,这两个request是互通的。至于request的生命周期等概念,与其他的Java Web项目没有区别,本文不再详述。
总之,request仍然符合Java Web网站的一般规律。不过笔者建议使用者应尽量避免用request传值。
一些例子:
// 本类将演示拦截器中对HttpServletRequest和request的Map的操作 public class MyInterceptor extends AbstractInterceptor { public String intercept(ActionInvocation invocation) throws Exception { // 获得ActionContext ActionContext actionContext = invocation.getInvocationContext(); // 获得HttpServletRequest HttpServletRequest httpServletRequest=(HttpServletRequest)actionContext.get(StrutsStatics.HTTP_REQUEST); // 获得request的Map Map requestMap = (Map) actionContext.get("request"); // 创建一个类作为实例 Person person = new Person(); // 以下两行的语句作用相同 httpServletRequest.setAttribute("person", person); requestMap.put("person", person); // 其他代码 // ...... return invocation.invoke(); } }
// 本类将演示在Action中对HttpServletRequest和request的Map进行操作(静态方法获得ActionContext) public class MyAction extends ActionSupport { @Override public String execute() throws Exception { // 获得ActionContext ActionContext actionContext = ActionContext.getContext(); // 获得HttpServletRequest HttpServletRequest httpServletRequest=(HttpServletRequest)actionContext.get(StrutsStatics.HTTP_REQUEST); // 获得request的Map Map requestMap = (Map) actionContext.get("request"); // 创建一个类作为实例 Person person = new Person(); // 以下两行的语句作用相同 httpServletRequest.setAttribute("person", person); requestMap.put("person", person); // 其他代码 // ...... return SUCCESS; } }
// 本类将演示在Action中使用ServletRequestAware获得HttpServletRequest(注意:要使用ServletConfigInterceptor拦截器) public class MyAction extends ActionSupport implements ServletRequestAware { private HttpServletRequest request; //此方法是接口ServletRequestAware的方法 public void setServletRequest(HttpServletRequest request) { this.request = request; } @Override public String execute() throws Exception { // HttpServletRequest已在该类的字段中准备好,可直接使用 // ...... return SUCCESS; } }
// 本类将演示在Action中使用ServletRequestAware获得request的Map(注意:要使用ServletConfigInterceptor拦截器) public class MyAction extends ActionSupport implements RequestAware { Map<String, Object> request; // 该方法是接口RequestAware的方法 public void setRequest(Map<String, Object> request) { this.request = request; } @Override public String execute() throws Exception { // request的Map已在该类的字段中准备好,可直接使用 // ...... return SUCCESS; } }
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>JSP Page</title> </head> <body> <!-- 本JSP将演示在JSP中对request的Map的使用 --> <!-- 本JSP为MyAction对应的JSP --> <!-- request的Map是Struts2自动在ActionContext中存入的值(key为request),所以使用“#”来访问ActionContext,从中读取request --> <s:property value="#request"/> <!-- 以下两行均是访问request的Map中key为“name”的值 --> <s:property value="#request.name"/> <s:property value="#request['name']"/> </body> </html>
Parameters为GET或POST等请求时浏览器向服务器传递而来的参数。在传统的Java Web项目中,使用HttpServletRequest.getParameter()等方法来获取参数,并且可以直接使用HttpServletRequest.getParameterMap()来获得一个封装了参数的Map。而在Struts2中,Struts2直接把上述Map存放到了ActionContext中,key为“parameters”。另外,ActionContext还直接提供了ActionContext.getParameters()方法来获得这个Map。因此,在Struts2的各个部件中操作parameters的方法和操作request的Map的方法十分相似,本段不再详述。
传统Java Web项目中的session是我们都熟悉的,我们用它来记录一个用户的会话状态。Struts2把HttpServletSession封装到了一个Map中,即“session的Map”,这类似对request的处理。然而为了节省系统资源,我们在不需要session的时候不会创建session。可能正是因为这个缘故,Struts2中没有把HttpServletSession放入ActionContext中,如果你的程序需要使用HttpServletSession,应该先获得HttpServletRequest,然后使用getSession()或getSession(boolean b)来获得它,同时决定是否需要创建session。对于session的Map,Struts2仍然把它放入了ActionContext中(key为"session"),但是不要担心,这个Map的机制使得只有put新值时才会创建session。总之,Struts2中对HttpServletSession的操作要先通过HttpServletRequest来获得它,而对session的Map的操作与对request的Map的操作如出一辙,本段不再详述。
传统的Java Web项目中,ServletContext用来存放全局变量,每个Java虚拟机每个Web项目只有一个ServletContext。这个ServletContext是由Web服务器创建的,来保证它的唯一性。ServletContext有一些方法能操作它的attributes,这些操作方法和操作一个Map类似。于是,Struts2又来封装了:它把ServletContext的attributes封装到了一个Map中,即“application的Map”,并且也放入的ActionContext中(key为application),因此,对application的Map的操作就如果对request的Map操作,本段不再详述。
至于对ServletContext的操作,与HttpServletRequest的操作类似:Struts2将ServletContext放到了 ActionContext中,并且ServletConfigInterceptor提供了对ServletContext的注入接口ServletContextAware。因此,本段不再详述。
注意:在Ognl表达式中使用“#application”可以得到application的Map,而不是ServletContext。然而在JSP嵌入的Java代码中(比如“<% application.getAttribute(""); %>”),application为ServletContext,而不是Map。
用一张表格来总结:
变量 | 从ActionContext中获得 | 生命周期 | 用Ongl来读取值 | 使用ServletConfigInterceptor来注入 |
ActionContext类 | 静态方法ActionContext. getContext() | 一次Http请求 | 使用“#”加上key,如“#name” | 无法注入 |
ValueStack类 | ActionContext. getValueStack() | 一次Http请求 | 直接填写来访问栈中对象的方法,或者使用top来直接获得栈中对象 | 无法注入 |
HttpServletRequest类 | ActionContext. get( StrutsStatics. HTTP_REQUEST) | 一次Http请求 | 无方便的方法 | 实现ServletRequestAware接口 |
request的Map | ActionContext. get("request") | 一次Http请求 | 使用“#request”再加上key,如“#request.name”或者“#request['name']” | 实现RequestAware接口 |
parameters的Map | ActionContext. get( "parameters") | 一次Http请求 | 使用“# parameters”再加上key,如“#parameters .name”或者“#parameters ['name']” | 实现ParameterAware接口 |
HttpServletSession类 | 无(需通过HttpServletRequest来获得) | 一次Http Session会话 | 无方便的方法 | 无法注入 |
session的Map | ActionContext. get("session") | 每次请求创建,但在一次Http Session会话中数据都是一样的 | 使用“#session”再加上key,如“# session.name”或者“#session ['name']” | 实现SessionAware接口 |
ServletContext类 | ActionContext. get( StrutsStatics. SERVLET_CONTEXT) | 网站项目启动后一直存在且唯一 | 无方便的方法 | 使用ServletContextAware接口 |
application的Map | ActionContext.get( "application") | 每次请求时创建,但其中的数据是网站项目启动后一直存在且共享 | 使用“# application”再加上key,如“#application .name”或者“#application ['name']” | 使用ApplicationAware接口 |
key | key的声明处 | value的类型 | value.toString() |
com. opensymphony. xwork2. dispatcher. HttpServletRequest |
StrutsStatics. HTTP_REQUEST | org. apache. struts2. dispatcher. StrutsRequestWrapper | org. apache. struts2. dispatcher. StrutsRequestWrapper @10984e0 |
application | 无 | org. apache. struts2. dispatcher. ApplicationMap | 略 |
com. opensymphony. xwork2. ActionContext. locale | ActionContext. LOCALE | java. util. Locale | zh_CN |
com. opensymphony. xwork2. dispatcher. HttpServletResponse | StrutsStatics. HTTP_RESPONSE | org. apache. catalina. connector. ResponseFacade | org. apache. catalina. connector. ResponseFacade @14ecfe8 |
xwork. NullHandler. createNullObjects |
Boolean | false | |
com. opensymphony. xwork2. ActionContext. name | ActionContext. ACTION_NAME | String | index |
com.opensymphony. xwork2.ActionContext. conversionErrors |
ActionContext. CONVERSION_ERRORS |
java. util. HashMap | {} |
com. opensymphony. xwork2. ActionContext. application | ActionContext. APPLICATION | org. apache. struts2. dispatcher. ApplicationMap | 略 |
attr | 无 | org. apache. struts2. util. AttributeMap | org. apache. struts2. util. AttributeMap @133a2a8 |
com. opensymphony. xwork2. ActionContext. container | ActionContext. CONTAINER | com. opensymphony. xwork2. inject. ContainerImpl | com. opensymphony. xwork2. inject. ContainerImpl @fc02c8 |
com. opensymphony. xwork2. dispatcher. ServletContext | StrutsStatics. SERVLET_CONTEXT | org. apache. catalina. core. ApplicationContextFacade | org. apache. catalina. core. ApplicationContextFacade @11ad78c |
com. opensymphony. xwork2. ActionContext. session | ActionContext. SESSION | org.apache.struts2. dispatcher.SessionMap | {} |
com.opensymphony. xwork2.ActionContext. actionInvocation |
ActionContext. ACTION_INVOCATION | com. opensymphony. xwork2. DefaultActionInvocation | com. opensymphony. xwork2. DefaultActionInvocation @13d4497 |
xwork. MethodAccessor. denyMethodExecution | 笔者很懒,没有找 | Boolean | false |
report. conversion. errors | 笔者很懒,没有找 | Boolean | false |
session | 无 | org. apache. struts2. dispatcher. SessionMap | {} |
com. opensymphony. xwork2. util. ValueStack. ValueStack | ValueStack.VALUE_STACK | com. opensymphony. xwork2. ognl. OgnlValueStack | com. opensymphony. xwork2. ognl. OgnlValueStack @16237fd |
request | 无 | org. apache. struts2. dispatcher. RequestMap | 略 |
action | 笔者很懒,没有找 | com. example. MyAction | 略 |
struts. actionMapping | 笔者很懒,没有找 | org. apache. struts2. dispatcher. mapper. ActionMapping | org. apache. struts2. dispatcher. mapper. ActionMapping @892cc5 |
parameters | 无 | java. util. HashMap | {} |
com. opensymphony. xwork2. ActionContext. parameters | ActionContext.PARAMETERS | java. util. TreeMap | {} |
注意:该表格为了排版在某些地方加了空格。
可以看出,有些相同的对象被以不同的key多次设置到ActionContext中。如果想看看创建ActionContext的源代码,请看org.apache.struts2.dispatcher.Dispatcher的serviceAction方法和两个createContextMap方法。
经试验并查看相关源代码后发现,在使用<s:property value="..."/>时,该标签的执行类会先根据value中表达式到值栈中执行表达式来查找值。如果在值栈中找到值,就返回该值;如果没有找到,则把该表达式作为ActionContext的key,到ActionContext中去找值。比如<s:property value="request"/>也会得到ActionContext中的request,等价于<s:property value="#request"/>。但是,由于标签的执行类会认为该值时String类型的,并且会直接进行类型转换。于是,如果直接使用<s:property value="request"/>的话其实会让页面抛出异常:Request类型不能转换成String类型。所以,只能用如果不带#的话只能成功读取ActionContext中String类型的值。这种机制使得某些时候栈顶的属性可以覆盖ActionContext中的key,或许你正需要它。然而,鉴于这种机制的不确定性,笔者建议访问ActionContext中的数据一定要带上“#”,可以免去一些麻烦。
关于这种转型异常,笔者认为是Struts2的bug,源代码如下,当“value = getValue(expr, asType);”时,是考虑了asType的,但从context中读取时“value = findInContext(expr);”,就没有考虑asType,并且没有在其他地方看到类型检查操作:
// 本代码截取Struts2.3.1.2版本com.opensymphony.xwork2.ognl.OgnlValueStack类的第340行-352行 private Object tryFindValue(String expr, Class asType) throws OgnlException { Object value = null; try { expr = lookupForOverrides(expr); value = getValue(expr, asType); if (value == null) { value = findInContext(expr); } } finally { context.remove(THROW_EXCEPTION_ON_FAILURE); } return value; }