上一节在将阐述ModelDriven的机制时,常常提到一个名词ValueStack。也许你会毫不犹豫脱口而出,不就是值栈吗?对,就是它,那你知道Struts为什么需要引入它?它是如何工作的?它和OGNL有何私情?如果你对以上问题的答案很模糊,但是又确实想知道答案,那么本文将带你去看看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的匹配问题。
大家一定对下面这段代码不陌生:
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一直伴随左右,那么什么是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对象中的成员变量一一对应吗!这下二者的关系你心中也有数了吧。