JavaEE笔记(四)Struts2中的ValueStack 与 ActionContext

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对象,再利用ValueStackgetContext()得到一个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

现在我们可以回到StrutsPrepareAndExecuteFilterdoFilter()方法了,按照接下去的流程,我们将调用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中,而CompoundRootArrayList的子类,该类通过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,通过其实现类DefaultActionProxyexecute()方法执行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对象,让我们可以随时取用。


以上

© 著作权归作者所有

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值