JavaEE笔记(四)Struts2中的ValueStack 与 ActionContext
在上一篇学习笔记JavaEE笔记(三)Struts2 拦截器的最后一小节中提到了关于Struts2拦截器与过滤器的区别,其中有一条说到,拦截器可以访问ValueStack与Action上下文。 我们没有展开讨论,因为关于这个话题,内容比较复杂而篇幅有限。今天的笔记将着重讨论这个话题。
1. What is ValueStack
ValueStack是Struts2框架中的一个接口类, com.opensymphony.xwork2.util.ValueStack
。 它的主要功能是作为一个容器将Action携带的数据反映到页面上,在页面上将通过OGNL表达式进行回显。对于ValueStack可以概括为以下4个要点,我们会在后面的内容逐一涉及解释:
1) ValueStack有一个实现类OgnlValueStack
。
2) 每一个Action都有一个与之对应的ValueStack,而Action也与Request一一对应,这样我们可以得出结论,一个Http请求Request对应一个Action对象,也对应一个值栈对象,ValueStack的生命周期就是Request的生命周期。
3) ValueStack中存储当前Action对象以及其他web对象(Request, Session, Application, Parameters)
4 ) Struts2框架将ValueStack以”struts.valueStack
“为名存储到Request域中。
2. ValueStack的结构
之前已经提到ValueStack是接口类,而我们关心的是其实现类OgnlValueStack
。 查看其源代码, OgnlValueStack实际维护了2个核心对象: CompoundRoot 和 OgnlContext, 前者对应Ognl的Root,而后者顾名思义即为Ognl的上下文对象。 从底层分析, CompoundRoot实际是一个继承了ArrayList的集合类,OgnlContext是一个Map
public class OgnlValueStack implements Serializable, ValueStack, ClearableValueStack, MemberAccessValueStack {
// 省略部分源码
CompoundRoot root;
transient Map<String, Object> context; // context对象为一个Map集合
// ......
}
// ComoundRoot为ArryList的子类
public class CompoundRoot extends ArrayList {
public CompoundRoot() {}
public CompoundRoot(List list) {super(list);}
}
public class DemoAction extends ActionSupport {
@Override
public String execute() throws Exception {
// 向valueStack中存储数据(root)
ValueStack vs = ActionContext.getContext().getValueStack();
vs.set("college", "NYU");
vs.push("Good day NYU");
return SUCCESS;
}
}
利用断点以及输出页面的<s:debug />
标签,我们发现List集合存储的是Action对象(以及手动入栈的数据。并且在context对象也持有对root的引用。蓝色标记框可以看到被手动压入栈的数据对象,以一个数组的形式存放。第一个元素为字符串”Good day NYU”,第二个元素为一个HashMap集合,key为”college”, value为”NYU”。蓝灰色标记的为Action对象,DemoAction (id=90)。
3. ValueStack的创建
说起ValueStack的创建,我们要回到Struts2框架的执行流程,这里不再赘述,可以参考上一篇博文,JavaEE笔记(三)Struts2 拦截器 已经详细阐述了收到Request对象后框架的处理过程。这里我们直接根据之前的基础开始讨论。
在StrutsPrepareAndExecuteFilter
过滤器中维护了一个预处理对象 PrepareOperations
, 通过调用createActionContext
方法,我们将得到ActionContext对象。下面看源码:
public class StrutsPrepareAndExecuteFilter implements StrutsStatics, Filter {
protected PrepareOperations prepare;
protected ExecuteOperations execute;
// 初始化方法中创建了PrepareOperations对象
public void init(FilterConfig filterConfig) throws ServletException {
InitOperations init = new InitOperations();
Dispatcher dispatcher = null;
// 省略部分源码
prepare = new PrepareOperations(filterConfig.getServletContext(), dispatcher);
execute = new ExecuteOperations(filterConfig.getServletContext(), dispatcher);
// ......
}
// doFilter方法中prepareOperations对象生成ActionContext对象
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
try {
// 省略部分源码
prepare.createActionContext(request, response);
ActionMapping mapping = prepare.findActionMapping(request, response, true);
if (mapping == null) {
boolean handled = execute.executeStaticResourceRequest(request, response);
if (!handled) {
chain.doFilter(request, response);
}
} else {
execute.executeAction(request, response, mapping);
}
}
} finally {
prepare.cleanupRequest(request);
}
}
下面再来看看createActionContext()
方法是怎样获得ActionContext的:
public ActionContext createActionContext(HttpServletRequest request, HttpServletResponse response) {
ActionContext ctx;
ActionContext oldContext = ActionContext.getContext();
if (oldContext != null) {
// detected existing context, so we are probably in a forward
ctx = new ActionContext(new HashMap<String, Object(oldContext.getContextMap()));
} else {
ValueStack stack = dispatcher.getContainer().getInstance(ValueStackFactory.class).createValueStack();
stack.getContext().putAll(dispatcher.createContextMap(request, response, null, servletContext));
ctx = new ActionContext(stack.getContext());
}
request.setAttribute(CLEANUP_RECURSION_COUNTER, counter);
ActionContext.setContext(ctx);
return ctx;
}
首先,利用ActionContext
静态方法在线程中寻找之前以后的ActionContext
对象,如果有,则说明这个Request已经包含了ActionContext
对象,即为转发的Request
。否则,先创建一个ValueStack
对象,再利用ValueStack
的getContext()
得到一个Map集合对象(还未封装),调用Map的putAll()
方法把另一个Map集合的数据复制进之前还未封装的Map对象。因此我们将注意力放在了封装了那些数据,dispatcher类中有一个方法createContextMap()
可以得到封装数据:
public Map<String,Object> createContextMap(HttpServletRequest request, HttpServletResponse response, ActionMapping mapping, ServletContext context) {}
从中我们可以看到,最终我们封装了一些Web对象,最后在createActionContext()
中,我们将再通过ValueStack.getContext()
得到封装完毕的Map传入ActionContext
的有参构造方法得到最终返回的ActionContext
对象,当然在返回之前,我们还需要再利用静态方法setContext()
把这个得到的对象设置回这个ActionContext
的线程中。值得注意的是,虽然我们是先利用了ValueStack
工厂方法得到了一个ValueStack
对象,但这个对象并没有任何与实际Action(Request)相关联,这也解释了为什么在执行过程中需要从ActionContext
反过来获取与之相对应的ValueStack
。
现在我们可以回到StrutsPrepareAndExecuteFilter
的doFilter()
方法了,按照接下去的流程,我们将调用ExecuteOperations.excuteAction()
–>Dispatcher.serviceAction()
方法来创建代理对象并执行Action,这个流程在这里也不再复述。值得注意的是在serviceAction()
方法中,在代理对象创建之前,必须要获得ValueStack
对象。看源码:
public void serviceAction(HttpServletRequest request, HttpServletResponse response, ServletContext context, ActionMapping mapping) throws ServletException {
Map<String, Object> extraContext = createContextMap(request, response, mapping, context);
// If there was a previous value stack, then create a new copy and pass it in to be used by the new Action
ValueStack stack = (ValueStack) request. getAttribute(ServletActionContext. STRUTS_VALUESTACK_KEY);
boolean nullStack = stack == null;
if (nullStack) {
ActionContext ctx = ActionContext.getContext();
if (ctx != null) {
stack = ctx.getValueStack();
}
}
if (stack != null) {
extraContext.put(ActionContext.VALUE_STACK, valueStackFactory. createValueStack(stack));
}
String timerKey = "Handling request from Dispatcher";
try {
UtilTimerStack.push(timerKey);
String namespace = mapping.getNamespace();
String name = mapping.getName();
String method = mapping.getMethod();
Configuration config = configurationManager.getConfiguration();
ActionProxy proxy = config.getContainer().getInstance(ActionProxyFactory.class).createActionProxy(
namespace, name, method, extraContext, true, false);
request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, proxy.getInvocation().getStack());
// if the ActionMapping says to go straight to a result, do it!
if (mapping.getResult() != null) {
Result result = mapping.getResult();
result.execute(proxy.getInvocation());
} else {
proxy.execute();
}
// If there was a previous value stack then set it back onto the request
if (!nullStack) {
request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, stack);
}
首先要做的一件事是确定我们是否已经在这个生命周期中存有ValueStack对象。判断的依据是如果有,则Request域中必然有一对键值对,key为ServletActionContext.STRUTS_VALUESTACK_KEY
,即”struts.valueStack”字符串,value为ValueStack
对象。如果没有,先通过静态方法得到线程的ActionContext
, 再利用ActionContext
对象得到相应的ValueStack
, 并且当代理对象创建完毕,也将把之前作为判断条件的键值对加入Request域,这样当这个Request转发(非重定向)后,键值对在域中被获取也就可以获取到生命周期中的ValueStack
。
总结一下,简单的讲ValueStack在每一次请求时都会创建,并且在Request的生命周期中保持存在且唯一,ActionContext可以通过ValueStack的get方法得到Map映射,将Map传入构造方法得到ActionContext,ValueStack可以反过来直接通过ActionContext的get方法得到。
4. ValueStack的获取
ValueStack的获取在上一节的源码解释中已经有所提及,这里概括2个方法:
1)要获取ValueStack对象,可以先通过静态方法获取ActionContext对象,由于ActionContext持有ValueStack的引用,因此直接通过get方法调用就可以得到对应的ValueStack。
ValueStack stack = ActionContext.getContext().getValueStack();
2) 在代理对象的创建过程中,我们已经将键值对存入了Request域中,因此也可以问Request域来拿到ValueStack对象。
ValueStack stack = (ValueStack) ServletActionContext.getRequest().getAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY);
5. 如何向ValueStack中保存数据
在1.2节中的例子我们已经看到了2中向ValueStack中保存数据的方法,set(K,V)
以及 push(Object)
并且他们默认将数据存到了Root中。我们现在来通过源码来看下原理,者及方法的实现位于ValueStack
接口的实现类OgnlValueStack
中。
public void push(Object o) {
root.push(o); // 将数据压入了CompoundRoot中
}
public void set(String key, Object o) {
Map setMap = retrieveSetMap();
setMap.put(key, o);
}
private Map retrieveSetMap() {
Map setMap;
Object topObj = peek();
if (shouldUseOldMap(topObj)) {
setMap = (Map) topObj;
} else {
setMap = new HashMap();
setMap.put(MAP_IDENTIFIER_KEY, "");
push(setMap); // 将数据压入了CompoundRoot中
}
return setMap;
}
private boolean shouldUseOldMap(Object topObj) {
return topObj instanceof Map && ((Map) topObj).get(MAP_IDENTIFIER_KEY) != null;
}
对于直接将对象存入alueStack
的方法push()
,通过源码非常明显地看到,数据被默认存到了CompoundRoot
中,而CompoundRoot
是ArrayList
的子类,该类通过ArrayList
的方法实现了栈的数据结构,由于比较简单,这里不再赘述。接下来,set()
方法,底层是创建或者获取原有的HashMap
集合,并将数据存入该Map, 如果是新建一个HashMap
,它也将通过push()
方法压入CompoundRoot
的栈中。因此可以得到结论,所有的Action的数据是默认被存入root中的,并且这个root本身也是盏结构,因此给人的直观影响就是我们把数据存到了ValueStack这个栈中,而ValueStack并非真正的栈结构。
6. JSP页面获取ValueStack中保存的数据
从ValueStack中获取数据的一个基本原则: root中获取不需要加#
,context中获取需要加#
。
1) 如果栈顶是一个Map集合,可以通过Key来获取Value
<s:property value="key" /> //输出key对应的value
2) 如果栈顶不是Map集合,可以通过序号来获取
<s:property value="[0]" /> // 从0的位置向下查找输出所有
<s:property value="[0].top" /> // 只输出0的位置的数据
3) 想要获取Web对象中的数据,需要在context中查找,因此需要加#
, 例子jsp页面将通过浏览器访问http://localhost:8080/demo.jsp?password=123456
<%
request.setAttribute("rname", "rvalue");
session.setAttribute("sname", "svalue");
application.setAttribute("aname", "avalue");
%>
<s:property value="#request.rname"/><br> // output: rvalue
<s:property value="#session.sname"/><br> // output: svalue
<s:property value="#application.aname"/><br> // output: avalue
<s:property value="#attr.sname"/><br> // output: svalue
<s:property value="#parameters.password[0]"/> // output: 123456
attr 将以此从request,session,application搜索参数名称,匹配第一个结果输出。
parameters将携带请求参数。
4)使用<s:iterator>
标签来迭代集合数据
<!-- 迭代栈顶集合对象,迭代中的每一个元素名为 'user' -->
<s:iterator value="[0].top" var="user">
username:<s:property value="#user.username"/><br>
password:<s:property value="#user.password"/>
<hr>
</s:iterator>
需要注意,虽然栈顶元素从root获取,但是迭代的每一个元素默认放入context,因此在获取时需要加#
。
7. 默认保存到ValueStack中的数据
在1.2和1.5中,我们看到了手动保存在ValueStack中的数据以及方法。这一节将介绍ValueStack默认保存的数据对象。
7.1 Action对象的初始化保存
1.2中已经看到了,在手动保存数据的同时还看到了Action也被压入了栈。现在我们来根据源码来看看Action如何及何时被加载入ValueStack。根据Struts2框架的处理流程,我们必须得到Action的代理对象,ActionProxy
,通过其实现类DefaultActionProxy
的execute()
方法执行Action,而execute()
方法内实际是通过了ActionInvocation
的实现类DefaultActionInvocation
调用了invoke()
方法来实现。这里就不贴出源码展开了,现在我们就来看看DefaultActionInvocation
的初始化方法init()
:
public void init(ActionProxy proxy) {
this.proxy = proxy;
Map<String, Object> contextMap = createContextMap();
// 首先得到线程的ActionContext对象
ActionContext actionContext = ActionContext.getContext();
// 将这个ActionInvocation对象存入ActionContext
if (actionContext != null) {
actionContext.setActionInvocation(this);
}
// 得到Action对象,Action对象在类中声明为成员变量
createAction(contextMap);
// 将Action对象存入ValueStack
if (pushAction) {
stack.push(action);
contextMap.put("action", action);
}
// ......省略部分代码
我们需要了解的2点:
1) Action是由Action的代理对象ActionProxy负责存入ValueStack;
2) Action存入栈的时机是在ActionProxy对象的初始化过程中。
3) 存入栈中的Action中会默认保存getXXX()
方法返回的对象。(详见下一节)
7.2 模型驱动对象的默认加载
模型驱动封装数据指的是利用Strut2框架提供的内置拦截器interceptor.ModelDrivenInterceptor
来自动进行封装。Action类需要实现ModelDriven<T>
接口并指明泛型类型。类似于自定义数据封装,我们仍然需要维护一个私有的需要封装的成员变量并初始化,不同于自定义数据封装,我们并不需要提供set(), get()方法,但需要重写接口getModel()方法,方法中直接返回我们需要默认加载入ValueStack的对象,也就是私有化的成员变量即可。完成这些操作后,即使我们在Action的execute()方法中不进行人为的手动加载,这个私有化对象也会被默认加载进ValueStack。有一点需要注意的是,这种方法的加载由于是由拦截器完成的,因此它的加载时机晚于Action本身的加载。换一种讲法,我们会看到如果利用这种加载方式,它的加载对象将处在栈顶。
首先我们来看下拦截器的源码来验证下加载原理:
public class ModelDrivenInterceptor extends AbstractInterceptor {
@Override
public String intercept(ActionInvocation invocation) throws Exception {
Object action = invocation.getAction(); // 获取Action对象
if (action instanceof ModelDriven) { //判断Action是否实现了ModelDriven接口
ModelDriven modelDriven = (ModelDriven) action; // 强转
ValueStack stack = invocation.getStack(); // 获得ValueStack对象
Object model = modelDriven.getModel(); // 调用getModel()方法获得需要加载的对象
if (model != null) {
stack.push(model); // 压入ValueStack
}
}
return invocation.invoke();
}
这段源码的结构和逻辑还是比较清楚的,不再这里赘述了。接下来,我们看一个例子:
public class ModelActionDemo extends ActionSupport implements ModelDriven<Student> {
private Student student = new Student("xx101", "Jack", 784721, "CSE");
public Student getModel() {
return student;
}
public String getHello() {
return "hello world";
}
@Override
public String execute() throws Exception {
student = new Student("xx202", "Ken", 597357, "ECE");
return SUCCESS;
}
}
我们通过JSP页面的<s:debug/>
标签来观察一下Root里的元素,清楚地看到栈顶是一个初始化了得Student对象即getModel()返回的对象,特别注意到,Action对象中也有名为model值是一个Student对象,还有一个名为Hello,值为”hello world“的字符串对象。这边是之前提到的,只要Action中含有getXXX()方法,在Action被默认加载如StackValue的时候,会默认以XXX为名,以该方法的返回对象为值存入Action对象中。因此我们也就看到了在Action中也含有getModel()的返回Student对象。
现在的问题就是,Action中的Student对象和栈顶的Student对象是否指向同一个内存地址,答案是否定的。
首先,我们先要明确这几个对象的加载顺序,从前到后依次为Action初始化–>拦截器调用–>Action被执行。前一节已经讨论了Action和Model对象的加载时机,因此这也就是为什么Model返回对象位于栈顶。 接下来,来看看程序里的2个Student对象是如何加载的。Action初始化相当于也初始化了一个Student对象并连同Action一起存入ValueStack,假设其内存地址为Student@12345678
,接着ModelDriven
拦截器被调用,并调用getModel()
返回了刚才初始化的对象,因次栈顶对象指向Student@12345678
。随后Action被调用,也就是执行Action类中execute()
方法,这个方法中,我们又开辟了新的一个内存地址由图可见为Student@438a3a1d
,并且让student指向新的地址。然而Model返回对象由于是引用的赋值,不会因此而指向新的地址,因此仍然指向Student@12345678
。至此,当JSP页面用<s:debug/>
输出Action视图时,其底层实际会利用Action中的getXXX()
方法得到最后的引用并显示,因此,最后在Action中看到的model指向的是新的地址。
8. EL表达式访问ValueStack数据
我们首先拿第2节中的Action作为例子,JSP页面如下,来看一个效果:
<body>
ognl获取:<s:property value="college"/><br>
el获取:${username}
</body>
输出:
ognl获取:NYU
el获取:NYU
我们都知道,OGNL可以从ValueStack中获取数据,EL则是从域对象中获取数据,在这里,我们的Action中也没有在任何域对象中存放数据,但EL表达式却从ValueStack中获取到了同样的内容,这是怎么实现的?
依旧,我们还是要回到源码中寻找答案,StrutsPreparedAndExecuteFilter
中的doFilter()
方法中,在得到Action映射以及最后调用executeAction()之前,对Request进行了处理:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// ......
request = prepare.wrapRequest(request); // 处理request对象
ActionMapping mapping = prepare.findActionMapping(request, response, true);
// ......
一路跟随源码,最后发现Request对象被当作参数传入了Dispather
中的wrapRequest()
方法,这个方法返回一个StrutsRequestWrapper
对象,这个对象的类又是HttpServletRequestWrapper
的子类,到这里我们可以确定,对request对象的处理其实就是对其进行了包装,在包装类中重写了getAttribute()
方法,让我们能够找到ValueStack
中的参数。
// Dispatcher
public HttpServletRequest wrapRequest(HttpServletRequest request, ServletContext servletContext) throws IOException {
// don't wrap more than once
if (request instanceof StrutsRequestWrapper) {
return request;
}
String content_type = request.getContentType();
// 上传操作请求
if (content_type != null && content_type.contains("multipart/form-data")) {
MultiPartRequest mpr = getMultiPartRequest();
LocaleProvider provider = getContainer().getInstance(LocaleProvider.class);
request = new MultiPartRequestWrapper(mpr, request, getSaveDir(servletContext), provider);
} else {
// 非上传操作请求, 创建一个新的包装类对象
request = new StrutsRequestWrapper(request,disableRequestAttributeValueStackLookup);
}
return request;
}
再来看下getAttribute()
方法是怎么样被增强的:
public Object getAttribute(String key) {
// 省略部分代码
// 用父类方法先查找
ActionContext ctx = ActionContext.getContext();
Object attribute = super.getAttribute(key);
// ActionContext不为空,但没有找到相应参数名
if (ctx != null && attribute == null) {
boolean alreadyIn = isTrue((Boolean) ctx.get(REQUEST_WRAPPER_GET_ATTRIBUTE));
if (!alreadyIn && !key.contains("#")) {
try {
// If not found, then try the ValueStack
ctx.put(REQUEST_WRAPPER_GET_ATTRIBUTE, Boolean.TRUE);
ValueStack stack = ctx.getValueStack();
if (stack != null) {
attribute = stack.findValue(key); // 到ValueStack中去寻找
}
} finally {
ctx.put(REQUEST_WRAPPER_GET_ATTRIBUTE, Boolean.FALSE);
}
}
}
return attribute;
}
getAttribute()
被增强,先调用父类方法查询参数名,如果没有找到则通过ValueStack
对象调用findValue(key)
方法查找。findValue(key)
即为查找Key对应的值对象。
/**
* Find a value by evaluating the given expression against the stack in the default search order.
*
* @param expr the expression giving the path of properties to navigate to find the property value to return
* @return the result of evaluating the expression
*/
public abstract Object findValue(String expr);
总结
ValueStack是Struts2框架中的一个重要概念,为了理解ValueStack的结构的创建,理解框架的处理流程是一个必要的前提。本篇笔记主要介绍了ValueStack的主要功能以及结构和原理。依靠ValueStack的手动加载以及默认自动加载的特性,我们可以不再依靠Web对象(域对象)来进行数据的传递,框架本身也包装了这些Web对象,让我们可以随时取用。
以上
© 著作权归作者所有