这几天整理笔记,发现了以前的几篇文章没发出来
在之前的一篇文章中我们看到了OGNL的强大功能。
OGNL并不是专门为Struts2框架而设计的,它是用于获取和设置Java对象属性的一种独立的表达式语言。
所以在看这篇文章之前建议先把之前的一篇文章看完。
Struts2框架就是使用OGNL完成数据的设置与访问的:
数据访问主要体现在JSP页面中我们可以使用OGNL表达式访问Action中的数据;
数据的设置主要体现在Struts2框架自动帮你把请求参数注入到Action中。
下面这篇文章将会根据源码详细分析Struts2是如何运用OGNL表达式的。
先来看一张图,本文会逐步分析源码来解释这张图包含的意义。
一、官方文档导致的错误理解
官方文档中对OGNL有一段描述:Struts2框架将OGNL的上下文context设置为ActionContext,并将ValueStack设置为OGNL的root对象,而且还给出了一个树状图。
看完这文档,按照前一篇文章的介绍的OGNL取值方式去思考,会发现ActionContext和ValueStack(唯一实现类OgnlValueStack)完全不符合OGNL对context和root的要求:context必须是Map类型的映射,root可以是任意Object类型(但是根据调用actionContext.get("root")
并不能获取到ValueStack对象)。
经过查看源码发现:ActionContext虽然没有实现Map,但是内部有一个名为context的Map对象;OgnlValueStack也并没有实现所谓的值栈功能,而是由内部的CompoundRoot实现,而且OgnlValueStack也有一个名为context的Map对象。
二、ActionContext和ValueStack中真正的context
看到上面的代码,我首先想到的是,这两个context不会就是同一个吧。那就写个Action类测试一下试试看吧:
1 | public class OgnlTestAction extends ActionSupport { |
哟呵,真是同一个对象,而且根据map1.getClass()
得到类型也的确实是实现了Map接口的OgnlContext,这就比较符合Ognl的规则了。
再仔细看看下面OgnlValueStack中的setRoot方法,发现里面调用了Ognl.createDefaultContext
创建了一个OgnlContext对象,这才是真正的context。
三、CompoundRoot才是真正的值栈实现
CompoundRoot源码很简单,就是使用List模拟了一个栈的数据结构。
CompoundRoot:2.3版本中继承自ArrayList,在2.5之后换成了CopyOnWriteArrayList。
从上面的代码可以看出CompoundRoot效率很低下,push和pop操作分别使用
add(0,o)
和remove(0)
实现,对于使用数组实现的List而言,这两个操作都伴随着大数据量的拷贝操作。好在CompoundRoot中并不会存放很多数据,一次请求过程中最多都不会存储超过10个元素,所以性能影响并不是明显。但是开发时应该注意不应该往CompoundRoot中push过多的对象。
四、为什么OGNL表达式能直接访问栈顶的数据呢
看过前一篇文章的应该知道,在OGNL表达式中访问List对象需要使用[n]
这样的索引语法。但是平常使用在页面中使用OGNL表达式都是可以直接访问Action中的属性。这个到底是怎么实现的呢?
首先需要知道的是:客户端请求一个Action后,该Action会被放在值栈的栈顶(也就是CompoundRoot的栈顶),如果配置了Action chain,后面的Action会推入栈顶。
然后再看一下刚刚的OgnlValueStack中创建OgnlContext的方法,发现该方法调用时传入了一个CompoundRootAccessor对象,这个CompoundRootAccessor是解密的关键。
五、CompoundRootAccessor让OGNL总是能直接访问栈顶元素
CompoundRootAccessor实现了PropertyAccessor接口,OGNL中可以通过实现PropertyAccessor接口来自定义对象的访问规则
1 | public class CompoundRootAccessor implements PropertyAccessor, MethodAccessor, ClassResolver { |
通过查看源码发现,前面所谓的“访问栈顶元素”其实是遍历List直到找到相应的属性。
真正的访问栈顶可以通过“top”的ognl表达式来获取。
这也导致了一个严重的问题:如果你的Action中有名字为“top”的属性,这个属性将访问不了,这是因为通过“top”会直接返回栈顶元素。
下面写个例子可以验证这个问题:
1 | public class TopTestAction extends ActionSupport{ |
在jsp页面中尝试访问该属性:
1 | <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> |
struts.xml文件中的action配置这里就不贴了。
测试结果:
六、为什么通过EL表达式能访问值栈中的数据
除了能在Struts2的标签中能使用OGNL表达式访问值栈,EL表达式中也能访问值栈中的数据。
比如:
1 | public class LoginAction extends ActionSupport { |
在JSP页面中使用EL表达式访问:
1 | <%@ page language="java" contentType="text/html; charset=UTF-8" |
测试结果:
为什么能在EL表达式中访问Action中的属性呢,Struts2是如何实现的。
先自己思考一下:
如果要我实现,第一种方式是将值栈设置为Request的一个Attribute,但是实现起来相当复杂,而且代码可维护性不高;第二种能想到的就是用代理类包装Request,重写默认的getAttribute方法,这样既不破坏原有代码,而且修改起来也比较方便。
Servlet规范中定义了包装类应该继承自HttpServletRequestWrapper
,查看一下它有多少子类,我擦,还真发现Struts2定义了一个包装类:
1 | public class StrutsRequestWrapper extends HttpServletRequestWrapper { |
通过查看上面这段代码发现:在EL表达式中并不能使用
#xxx
的方式访问context下的内容(主要是几个内置对象的访问)。这个功能也没必要,EL自带的四个范围对象已经具有这个功能了,如果能使用#xxx
就导致功能重复。而且使用#att.foo
或#request.foo
访问request中的数据,反而会导致死循环。