个人认为,Struts2里面的ValueStack概念解释的比较混乱,有时候是ValueStack,有时候又特指其中的ObjectStack。导致在使用OGNL表达式的时候出现一些问题。还有一个问题是,Struts2扩展了一些原始OGNL的概念,导致一些令人迷惑的问题。现对照一些原生OGNL的例子及Struts2的源码分析问题出现原因,消除心中的疑惑。
1.原生的OGNL
1) Introduction
OGNL stands for Object-Graph Navigation Language; it is an expression language for getting and setting properties of Java objects, plus other extras such as list projection and selection and lambda expressions. You use the same expression for both getting and setting the value of a property.
We pronounce OGNL as a word, like the last syllables of a drunken pronunciation of "orthogonal".
Many people have asked exactly what OGNL is good for. Several of the uses to which OGNL has been applied are:
- A binding language between GUI elements (textfield, combobox, etc.) to model objects. Transformations are made easier by OGNL's TypeConverter mechanism to convert values from one type to another (String to numeric types, for example);
- A data source language to map between table columns and a Swing TableModel;
- A binding language between web components and the underlying model objects;
- A more expressive replacement for the property-getting language used by the Apache Commons BeanUtils package or JSTL's EL (which only allow simple property navigation and rudimentary indexed properties).
OGNL常被用来做为View与Model之间的胶水语言,在View中方便的使用Model对象,OGNL负责收集这些Model对象,在View中使用简单的OGNL表达式,就可以取到想要的对象。既消除了在View中直接使用getters 和 setters的重复代码的书写,还避免了在View代码中写的强制类型转换(Cast)。
表达式语言(EL)本质上被设计为:帮助你使用简单的表达式来完成一些“常用”的工作。通常情况下,ELs 可以在一些框架中找到,它被是用来简化我们的工作。例如:大家熟知的 Hibernate,使用 HQL(Hibernate Query Language) 来完成数据库的操作,HQL 成了开发人员与复查的 SQL 表达式之间的一个桥梁。 在 web 框架下,表达式语言起到了相似的目的。它的存在消除了重复代码的书写。
2) Use OGNL
static Object | getValue(Object tree, Map context, Object root) Evaluates the given OGNL expression tree to extract a value from the given root object. |
static Object | getValue(Object tree, Map context, Object root, Class resultType) Evaluates the given OGNL expression tree to extract a value from the given root object. |
static Object | getValue(Object tree, Object root) Evaluates the given OGNL expression tree to extract a value from the given root object. |
static Object | getValue(Object tree, Object root, Class resultType) Evaluates the given OGNL expression tree to extract a value from the given root object. |
static Object | getValue(String expression, Map context, Object root) Evaluates the given OGNL expression to extract a value from the given root object in a given context |
static Object | getValue(String expression, Map context, Object root, Class resultType) Evaluates the given OGNL expression to extract a value from the given root object in a given context |
static Object | getValue(String expression, Object root) Convenience method that combines calls to parseExpression and getValue . |
static Object | getValue(String expression, Object root, Class resultType) Convenience method that combines calls to parseExpression and getValue . |
使用OGNL非常简单,我们只用到了Ognl.jar文件,并且只使用了其中的一个类ognl.Ognl。这个类中的方法都是静态static方法。取值只用到了getValue方法,这个方法是个重载overload方法。
OGNL三要素:
1.expression 求值表达式——首先会被解析成对象树
2.root object 根对象——默认的操作对象
3.context OGNL执行环境——OGNL执行的上下文环境
OGNL context是一个Map结构,ognl.OgnlContext类implements Map接口,root对象也在context里面,并且做这一个特殊的对象处理,具体表现为对root 对象的操作不需要加#指示符号(并且加上了#一定取不到root对象里面的值)。
看一个例子:
Person类:
1 package com.cuillgln.ognl; 2 3 public class Person { 4 private String name; 5 private int age; 6 private String telphone; 7 8 public Person() { 9 10 } 11 12 public Person(String name, int age, String telphone) { 13 this.name = name; 14 this.age = age; 15 this.telphone = telphone; 16 } 17 18 public String getName() { 19 return name; 20 } 21 22 public void setName(String name) { 23 this.name = name; 24 } 25 26 public int getAge() { 27 return age; 28 } 29 30 public void setAge(int age) { 31 this.age = age; 32 } 33 34 public String getTelphone() { 35 return telphone; 36 } 37 38 public void setTelphone(String telphone) { 39 this.telphone = telphone; 40 } 41 42 }
OgnlTest类:
1 package com.cuillgln.ognl; 2 3 import ognl.Ognl; 4 import ognl.OgnlContext; 5 import ognl.OgnlException; 6 7 public class OgnlTest { 8 9 public static void main(String[] args) { 10 try { 11 OgnlContext context = new OgnlContext(); 12 context.put("name", "STRING1~~~"); 13 context.put("age", 100); 14 context.put("telphone", "STRING3~~~"); 15 context.put("person.name", "STRING4~~~"); 16 17 Person p1 = new Person("Lee", 12, "134xxxxxxxx"); 18 Person p2 = new Person("Zhang", 15, "150xxxxxxxx"); 19 Person p3 = new Person("Wang", 18, "186xxxxxxxx"); 20 21 context.put("person1", p1); 22 context.put("person2", p2); 23 context.put("person3", p3); 24 25 Person root = new Person("ROOT", 100, "xxxxxxxx"); 26 27 /* 28 * the output is : 29 * the value is: ROOT====== 30 * obviously, value是对应root object 的name属性的对象。 31 * 输出的是这个对象的值 32 */ 33 Object value = Ognl.getValue("name", context, root); 34 System.out.println("the value is: " + value + "======"); 35 36 /* 37 * the output is : 38 * the value is: STRING1~~~====== 39 * obviously, value是context中,Key为name的对象 40 * 输出的是这个对象的值 41 */ 42 value = Ognl.getValue("#name", context, root); 43 System.out.println("the value is: " + value + "======"); 44 45 /* 46 * the output is : 47 * ognl.NoSuchPropertyException: com.cuillgln.ognl.Person.person1 48 * obviously, OGNL会试图去找root object中属性为person1的对象, 49 * 结果没有找到,所以抛出ognl.NoSuchePropertyException 50 */ 51 value = Ognl.getValue("person1", context, root); 52 System.out.println("the value is: " + value + "======"); 53 54 /* 55 * the output is : 56 * the value is: com.cuillgln.ognl.Person@7d8483====== 57 * obviously, value是context中,Key为person1的对象。 58 * 输出的结果是这个对象的toString的返回值 59 */ 60 value = Ognl.getValue("#person1", context, root); 61 System.out.println("the value is: " + value + "======"); 62 63 /* 64 * the output is : 65 * ognl.NoSuchPropertyException: com.cuillgln.ognl.Person.person1 66 * obviously, OGNL会试图去找root object中属性为person1的对象,然后再找属性person1对应对象的name属性对应的对象 67 * 结果没有找到root object中有person1属性对应的对象, 68 * 所以抛出ognl.NoSuchePropertyException: com.cuillgln.ognl.Person.person1 69 */ 70 value = Ognl.getValue("person1.name", context, root); 71 System.out.println("the value is: " + value + "======"); 72 73 /* 74 * the output is : 75 * the value is: Lee====== 76 * obviously, OGNL会试图去找context中Key为person1对应的对象,再对这个对象取name属性对应的对象 77 * 结果找到了正确输出了……。Lee 78 */ 79 value = Ognl.getValue("#person1.name", context, root); 80 System.out.println("the value is: " + value + "======"); 81 82 /* 83 * the output is : 84 * ognl.OgnlException: source is null for getProperty(null, "name") 85 * obviously, OGNL会试图去找context中Key为person对应的对象,再对这个对象取name属性对应的对象 86 * 结果在context中没有找到Key为person的属性,故返回null,在null上再取name属性,明显会出错。 87 */ 88 value = Ognl.getValue("#person.name", context, root); 89 System.out.println("the value is: " + value + "======"); 90 91 /* 92 * the output is : 93 * ognl.NoSuchPropertyException: java.lang.String.name 94 * obviously, OGNL会试图在root object中查找name属性对应的对象, 95 * 现在root object是一个String字符串,且没有name属性,故会抛出ognl.NoSuchPropertyException 96 */ 97 value = Ognl.getValue("name", context, "abcdefg"); 98 System.out.println("the value is: " + value + "======"); 99 100 /* 101 * the output is : 102 * the value is: STRING1~~~====== 103 * obviously, value是context中,Key为name的对象,跟root object没有关系 104 * 输出的是这个对象的值 105 */ 106 value = Ognl.getValue("#name", context, "abcdefg"); 107 System.out.println("the value is: " + value + "======"); 108 109 } catch (OgnlException e) { 110 // TODO Auto-generated catch block 111 e.printStackTrace(); 112 } 113 } 114 }
这些就是OGNL的基础知识,OGNL另外还有很强大的功能,包括投影、支持lambda表达式,暂不做介绍。
2.XWork-specific OGNL features
1)There can be many "root" objects. XWork中的表示根对象是CompoundRoot对象。CompoundRoot类extends ArrayList类。因为是一个List,里面可以放置多个对象,而这些对象经过XWork的改进对于OGNL表达式引擎来说都是root objects。XWork has a special OGNL PropertyAccessor that will automatically look at the all entries in the stack (in fact the CompoundRoot list) (from the top down) until it finds an object with the property you are looking for.
2)Struts2 Named Objects
Struts 2 places request parameters and request, session, and application attributes on the OGNL stack (in fact the OGNL context). They may be accessed as shown below.
name | value |
---|---|
#parameters['foo'] or #parameters.foo | request parameter ['foo'] (request.getParameter()) |
#request['foo'] or #request.foo | request attribute ['foo'] (request.getAttribute()) |
#session['foo'] or #session.foo | session attribute 'foo' |
#application['foo'] or #application.foo | ServletContext attributes 'foo' |
#attr['foo'] or #attr.foo | Access to PageContext if available, otherwise searches request/session/application respectively |
以OGNL表达式的观点来对上述两点做出解释:
1)的意思是:OGNL中的root object,现在是CompoundRoot对象,CompoundRoot对象并不直接在里面放置属性(指JavaBean中的Field),而是一些对象列表。如果要用原生的OGNL表达式获取里面某个对象的值,会出现什么状况?你会发现根本取不到里面的对象。如下面的代码抛出的异常: ognl.NoSuchPropertyException: java.util.ArrayList.person。所以XWork在使用OGNL前做了一些特殊处理,具体就是使用了一个特殊的PropertyAccessor来访问CompoundRoot里面的对象。(具体没有研究……)
1 Person p1 = new Person("Lee", 12, "134xxxxxxxx"); 2 Person p2 = new Person("Zhang", 15, "150xxxxxxxx"); 3 Person p3 = new Person("Wang", 18, "186xxxxxxxx"); 4 5 context.put("person1", p1); 6 context.put("person2", p2); 7 context.put("person3", p3); 8 9 List<Object> root = new ArrayList<Object>(); 10 root.add(p3); 11 root.add(p2); 12 root.add(p1); 13 /* 14 * the output is : 15 * ognl.NoSuchPropertyException: java.util.ArrayList.person 16 * 你会发现很难用表达式,表达出来你要查找的对象(导航到你要找的对象)————无解…… 17 */ 18 Object value = Ognl.getValue("person.name", context, root); 19 System.out.println("the value is: " + value + "======");
2)的意思是:Struts把跟Web相关的对象,具体就是request parameters, request attributes, session attributes, application attributes。放在OGNL的context中,分别对应parameters, request, session, application的Key。attributes都以Key/Value的形式存在的,所以在OGNL context里key等于parameters, request, session, application的Value对应的对象类型是一个Map类。
3.OgnlValueStack的结构
OgnlValueStack类implements ValueStack接口。里面有两个Field: context和root,分别是OGNL的两大要素OGNL context 和root object。
public class OgnlValueStack implements Serializable, ValueStack, ClearableValueStack, MemberAccessValueStack { CompoundRoot root; transient Map<String, Object> context; Class defaultType; transient OgnlUtil ognlUtil; protected void setRoot(XWorkConverter xworkConverter, CompoundRootAccessor accessor, CompoundRoot compoundRoot, boolean allowStaticMethodAccess) { this.root = compoundRoot; this.securityMemberAccess = new SecurityMemberAccess(allowStaticMethodAccess); this.context = Ognl.createDefaultContext(this.root, accessor, new OgnlTypeConverterWrapper(xworkConverter), securityMemberAccess); context.put(VALUE_STACK, this); Ognl.setClassResolver(context, accessor); ((OgnlContext) context).setTraceEvaluations(false); ((OgnlContext) context).setKeepLastEvaluation(false); } /** * @see com.opensymphony.xwork2.util.ValueStack#getContext() */ public Map<String, Object> getContext() { return context; } /** * @see com.opensymphony.xwork2.util.ValueStack#getRoot() */ public CompoundRoot getRoot() { return root; } /** * @see com.opensymphony.xwork2.util.ValueStack#findString(java.lang.String) */ public String findString(String expr) { return (String) findValue(expr, String.class); } public String findString(String expr, boolean throwExceptionOnFailure) { return (String) findValue(expr, String.class, throwExceptionOnFailure); } /** * @see com.opensymphony.xwork2.util.ValueStack#findValue(java.lang.String) */ public Object findValue(String expr, boolean throwExceptionOnFailure) { try { setupExceptionOnFailure(throwExceptionOnFailure); return tryFindValueWhenExpressionIsNotNull(expr); } catch (OgnlException e) { return handleOgnlException(expr, throwExceptionOnFailure, e); } catch (Exception e) { return handleOtherException(expr, throwExceptionOnFailure, e); } finally { ReflectionContextState.clear(context); } } private Object tryFindValueWhenExpressionIsNotNull(String expr) throws OgnlException { if (expr == null) { return null; } return tryFindValue(expr); } private Object tryFindValue(String expr) throws OgnlException { Object value; expr = lookupForOverrides(expr); if (defaultType != null) { value = findValue(expr, defaultType); } else { value = getValueUsingOgnl(expr); if (value == null) { value = findInContext(expr); } } return value; } private Object getValueUsingOgnl(String expr) throws OgnlException { try { return ognlUtil.getValue(expr, context, root); } finally { context.remove(THROW_EXCEPTION_ON_FAILURE); } } public Object findValue(String expr) { return findValue(expr, false); } /** * @see com.opensymphony.xwork2.util.ValueStack#findValue(java.lang.String, java.lang.Class) */ public Object findValue(String expr, Class asType, boolean throwExceptionOnFailure) { try { setupExceptionOnFailure(throwExceptionOnFailure); return tryFindValueWhenExpressionIsNotNull(expr, asType); } catch (OgnlException e) { return handleOgnlException(expr, throwExceptionOnFailure, e); } catch (Exception e) { return handleOtherException(expr, throwExceptionOnFailure, e); } finally { ReflectionContextState.clear(context); } } private Object tryFindValueWhenExpressionIsNotNull(String expr, Class asType) throws OgnlException { if (expr == null) { return null; } return tryFindValue(expr, asType); } private Object handleOgnlException(String expr, boolean throwExceptionOnFailure, OgnlException e) { Object ret = findInContext(expr); if (ret == null) { if (shouldLogNoSuchPropertyWarning(e)) { LOG.warn("Could not find property [" + ((NoSuchPropertyException) e).getName() + "]"); } if (throwExceptionOnFailure) { throw new XWorkException(e); } } return ret; } 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; } private Object findInContext(String name) { return getContext().get(name); } public Object findValue(String expr, Class asType) { return findValue(expr, asType, false); }
private Object getValue(String expr, Class asType) throws OgnlException {
return ognlUtil.getValue(expr, context, root, asType);
}
/** * @see com.opensymphony.xwork2.util.ValueStack#peek() */ public Object peek() { return root.peek(); } /** * @see com.opensymphony.xwork2.util.ValueStack#pop() */ public Object pop() { return root.pop(); } /** * @see com.opensymphony.xwork2.util.ValueStack#push(java.lang.Object) */ public void push(Object o) { root.push(o); }
public void set(String key, Object o) {
//set basically is backed by a Map pushed on the stack with a key being put on the map and the Object being the value
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;
}
/**
* check if this is a Map put on the stack for setting if so just use the old map (reduces waste)
*/
private boolean shouldUseOldMap(Object topObj) {
return topObj instanceof Map && ((Map) topObj).get(MAP_IDENTIFIER_KEY) != null;
}
}
从源码中可以看出:
1)对ValueStack的栈操作(即peek、pop、push)实际上是对root上的操作。————这就是为什么说有时候ValueStack特指Object Stack。这里的Object Stack即是CompoundRoot对象,它提供了对栈的操作接口。
2)findValue的过程:经过一系列的方法,最后调用tryFindValue方法,tryFindValue首先调用getValue,getValue调用OgnlUtil.getValue,OgnlUtil.getValue调用原生OGNL的Ognl.getValue方法。如果得到的value是null,会再调用findInContext方法,findInContext返回getContext().get(name),这是试图从context里获得Key为name的Value值。这是的name可能很复杂,如"person.name",此时不会再被解释成“先取得属性名为person的对象,再取这个对象的name属性对应的对象”,而是直接做为Map.get()里的一个Key。
3)ValueStack.set(String key, Object value);确实是在CompoundRoot栈顶压入了一个HashMap,里面放入了set的Key/value pairs。
4.ActionContext类结构及初始化过程
ActionContext里只有一个Filed,即context。从FilterDispacher的doFilter方法中,可以看出,ActionContext中的context被初始化为ValueStack中context的内容。并且在后面又在context中添加了一些key/value pairs。主要是把从ValueStack的context中的request parameters, request attributes, session attributes, application attributes,又以不同的Key放在了context中。并且还把整个ValueStack对象也放到了context中,Key是 "com.opensymphony.xwork2.util.ValueStack.ValueStack"。并且提供了这些对象的setters/getters方法。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; ServletContext servletContext = getServletContext(); String timerKey = "FilterDispatcher_doFilter: "; try { //1 处理前的准备 //1.1 创建值栈对象,值栈包含 object stack 和 context map 两个部分。 ValueStack stack = dispatcher.getContainer().getInstance(ValueStackFactory.class).createValueStack(); //1.2 创建 actionContext。 ActionContext ctx = new ActionContext(stack.getContext()); ActionContext.setContext(ctx); // 省略剩余代码
5.use Struts tags with OGNL expression
自定义标签(Customer Tags)一般都继承自javax.servlet.jsp.tagext.BodyTagSupport类,重写(overriding)doStartTag()和doEndTag()方法。
对于像<s:set var="test" value="%{'abc'}" scope="scope"/> assigns a value to a variable in a specified scope。根据源代码可以看出来实际上是把var定义的key放到了context中相应的scope下的Map中或context中。调用的是ValueStack.setValue(expr, Object)方法,而不是ValueStac.set(key, value)方法。两方法的区别:
1)ValueStac.set(key, value)是在CompoundRoot栈顶压入了一个HashMap,里面放入了set的Key/value pairs。
2)ValueStack.setValue(expr, object)是给根据expr查找到的对象赋与object值。
这就可以理解为什么在用<s:set/>绑定一个变量后,一般还是要加#才能取到值的原因了。因为他们的key/value pairs放置在context中,而不是CompoundRoot中。
还要理解为什么有一部分变量就不用加#也可以取到。不加#可以取到条件有(三个条件,缺一不可):
1) <s:set scope="action"/>中scope的值是默认值,即action作用域。
2)<s:set value=""/>中value是个OGNL constants. 即String, 各种Number, Boolean, null等。
3)在CompoundRoot对象中没有名为此key的属性。
为什么能取到:
1)scope="action"是把key/value pairs直接ValueStack.getContext().put(key, value)的。是存放在OGNL context中的。
2)按原生的OGNL,不加#号是不可能取到的。
3)Struts中的OgnlValueStack的tryFindValue()在用原生的OGNL getValue()得到的是null后(说明在CompoundRoot中没有与此key同名的属性),会调用findInContext(key)方法return getContext.get(key)。如此,如果在context中有此key,便可不用#也可能取到想要的值。
1 public boolean end(Writer writer, String body) { 2 ValueStack stack = getStack(); 3 4 Object o; 5 if (value == null) { 6 if (body != null && !body.equals("")) { 7 o = body; 8 } else { 9 o = findValue("top"); 10 } 11 } else { 12 o = findValue(value); 13 } 14 15 body=""; 16 17 if ("application".equalsIgnoreCase(scope)) { 18 stack.setValue("#application['" + getVar() + "']", o); 19 } else if ("session".equalsIgnoreCase(scope)) { 20 stack.setValue("#session['" + getVar() + "']", o); 21 } else if ("request".equalsIgnoreCase(scope)) { 22 stack.setValue("#request['" + getVar() + "']", o); 23 } else if ("page".equalsIgnoreCase(scope)) { 24 stack.setValue("#attr['" + getVar() + "']", o, false); 25 } else { 26 stack.getContext().put(getVar(), o); 27 stack.setValue("#attr['" + getVar() + "']", o, false); 28 } 29 30 return super.end(writer, body); 31 }