Struts2之ValueStack
上一节在将阐述ModelDriven的机制时,常常提到一个名词ValueStack。也许你会毫不犹豫脱口而出,不就是值栈吗?对,就是它,那你知道Struts为什么需要引入它?它是如何工作的?它和OGNL有何私情?如果你对以上问题的答案很模糊,但是又确实想知道答案,那么本文将带你去看看ValueStack的世界。
Why ValueStack?
说Struts就不得不提MVC,所有的请求都是基于页面(View)提交,页面是数据最初的载体,然后提交给Controller,由Controller将数据做第一次处理,然后交给业务层做进一步的处理。下面,我们还是通过代码直观的体会一下。
JSP页面伪码:
<form action="xxx/user-add.action" method="post">
username:<input type="text" name="username" />
age:<input type="text" name="age" />
groupName:<input type="text" name="group.name" />
<input type="submit" name="submit" value="添加" />
</form>
VO伪码:
public class UserVO {
private String username;
private int age;
private Group group;
//getter and setter
//...
}
public class Group {
private String name;
//getter and setter
//...
}
Action伪码:
public class UserAction implements ModelDriven {
private User user = new User();
public String addUser() {
//相应的业务逻辑
}
public String queryUser() {
//返回user对象
}
@Override
public Object getModel() {
return user;
}
}
通过以上代码,可以看出来, 数据在View层是扁平的,没有数据类型之分,均为字符串。但是在Java中,数据有各种数据类型(age -> int),丰富的数据结构 (group -> Group)。所以数据由页面向Java流转时,就会出现类型如何匹配的问题。
为了解决以上的问题,Struts2采纳了XWork的OGNL(后面会专门讲解)做为解决方案,在OGNL基础上构建了OGNLValueStack机制,完美的实现了数据由View到Controller的匹配问题。
ValueStack如何运转?
大家一定对下面这段代码不陌生:
username:<s:property value="username" />
age:<s:property value="age" />
groupName:<s:property value="group.name" />
使用Struts2的Tag通过OGNL取值,在上一节我们讲到,user对象最终被保存到了ValueStack中。如果你有兴趣阅读struts2的源码时,你会发现ValueStack只是一个接口,而Struts2使用了OgnlValueStack作为其默认实现。
ValueStack接口中声明了getRoot()这样一个接口:
/**
* Get the CompoundRoot which holds the objects pushed onto the stack
*
* @return the root
*/
public abstract CompoundRoot getRoot();
看下CompoundRoot的实现:
public class CompoundRoot extends ArrayList {
//...省略一些其他实现
public Object peek() {
return get(0);
}
public Object pop() {
return remove(0);
}
public void push(Object o) {
add(0, o);
}
}
学过数据结构的一眼就看出来,CompoundRoot具有栈的特性:先进后出。所以在s:property标签中的OGNL表达式,最终会交给ValueStack来解析。username就是一个OGNL表达式,意思是调用root对象的getUsername()方法。Struts2将自动搜索CompoundRoot中有哪些元素(从第0个元素开始搜索),检测这些元素是否有getUsername()方法,如果第0个元素没有getUsername()方法,将继续搜索第1、2、3……个元素是否有getUsername()方法。很明显,user对象有这个方法,所以就可以显示username的值。既然CompoundRoot维护的是一个栈结构,那么如果有多个元素都含有getUsername()方法的话,那么<s:property value="username" />只会得到最后一个压入栈的元素对应的username的值。
ValueStack & OGNL
在讲ValueStack的过程中,OGNL一直伴随左右,那么什么是OGNL呢?官方的解释就是:Object Graph Navigation Language,是一种表达式语言。使用这种表达式语言,你可以通过某种表达式语法,存取Java对象树中的任意属性、调用Java对象树的方法、同时能够自动实现必要的类型转化。如果我们把表达式看做是一个带有语义的字符串,那么OGNL无疑成为了这个语义字符串与Java对象之间沟通的桥梁。下面通过一个简单的程序看看OGNL的用法。
public class OgnlTest {
@Test
public void test01() {
//创建root对象
User u = new User();
u.setUsername("javer");
u.setAge(18);
Group g = new Group();
g.setName("group01");
u.setGroup(g);
//创建context
Map ctx = new HashMap();
ctx.put("context", "get from context:");
//直接取root对象
Object username = Ognl.getValue(Ognl.parseExpression("username"), u);
AssertEquals("javer", username); //true
//从上下文取值
Object value = Ognl.getValue(Ognl.parseExpression("#context", ctx, u):
AssertEquals("get from context:", value); //true
//从上下文取root对象
Object value = Ognl.getValue(Ognl.parseExpression("#context + username", ctx, u);
AssertEquals("get from context:javer", value);
}
}
上面这个例子其实涵盖了OGNL的三大要点:
1. Expression:整个OGNL的核心,所有的OGNL操作都是针对表达式的解析后进行的。表达式会规定此次OGNL操作到底要干什么。
2. Root Object:OGNL的操作对象,即指定到底“对谁干”。
3. Context:OGNL的内部,所有的操作都会在一个特定的环境中运行,这个环境就是OGNL的上下文环境,将规定OGNL的操作“在哪里干”。
回过头来再看看ValueStack,值栈中的数据,也是分两个部分存放:root和context。如果某个OGNL表达式被传递给ValueStack(即调用ValueStack的setValue或findValue方法),而表达式中包含有对root对象的访问操作,ValueStack将依次从栈顶往栈底搜索CompoundRoot对象中所包含的对象,看哪个对象具有相应的属性,找到之后,立刻返回。
所以ValueStack就是对OGNL进行了封装,增强了部分功能。说白了,ValueStack其实就是OGNL的一个扩展,在OGNL只支持一个root的基础上扩展到了支持多个root对象。说到这里,可能你也恍然 大悟,Action就是一个root对象,在不使用ModelDriven的时候,页面上的元素不就是和Action对象中的成员变量一一对应吗!这下二者的关系你心中也有数了吧。