首页 > 代码库 > Struts2之OGNL
Struts2之OGNL
一、OGNL是什么?
OGNL(Object-Graph Navigation Language)对象图导航语言,是一种表达式语言,它可以
1.遍历对象的结构图
2.存取对象的属性(实例属性和静态属性)
3.调用对象的方法(实例方法和静态方法)
4.操作集合对象(数组、List、Map)
5.new对象 (自动初始化,需要无参构造器)
6.自动进行类型转换
……
总的来说这货就是提供一种简便的方式来进行数据转移和类型转换。
1.数据转移
数据从前端页面过来,参数拦截器将它们拍到Action对应的属性中,但是,参数拦截器是怎么知道哪个参数名对应着哪个Action属性的呢?
2.类型转换
请求参数都是一个<String,String>类型的<K,V>对,那么它们是如何从String类型完成到各种类型(比如int,double,Date)类型的转换的呢?
二、OGNL的提供的功能
OGNL是一个对象导航语言,既然涉及到对象(这里的对象大多指的是JavaBean,而对于JavaBean访问成员属性一般都是通过getter/setter,所以这里提一下OGNL和很多其它的表达式语言一样也是通过getter/setter来访问成员属性的)那对象就要有一个存储的地方,这个存储对象的地方就是OGNL上下文(OGNLContext),对象存储在OGNLContext上,而OGNL表达式去OGNLContext上找对象,就这样通过OGNLContext将参数和对象关联到了一起。OGNLContext是一个Map,它里面可以存放JavaBean对象,并且有一个根对象,这个对象称为root(在Struts2中是CompoundRoot来充当root),我们只需要了解OGNL上下文由一个Map结构的context和一个对象结构的root组成就可以了,但是Strtus2对OGNL做了扩展,扩展为了支持多root,是通过将很多个对象摞起来成为栈结构来实现的(总结:标准的OGNL是单root,Sturts2通过栈扩展为了多root),其中root栈上的对象会暴露自己的属性也就是说可以直接使用OGNL表达式访问它的属性,从栈顶依次往下找,找到的第一个同名属性即返回,所以root栈高层的对象的属性会屏蔽低层对象的同名属性。
OGNLContext由context和root组成,访问context中的元素需要加上#前缀,比如#foo[bar],这是因为如果不加的话默认是从root开始找的,如果root找不到才会再去context中找,而如果context和root中有同名属性的话不加#就惨了,明明想取context中的结果取出了root中的,所以一个好习惯就是如果明确知道在context中的话就加上#。总结一下:不加#的意思就是先去root中找,找不到再去context,加上#的意思就是先去context中找,找不到再去root中找。 (此处存疑?多次相同实验结果竟然不一致???一定是智子在搞鬼…)
注意:OGNL是用来在不方便或者不合适写java代码的地方用来当做java代码使用的,就像jsp里面的标签一样,所以不要滥用OGNL。
ActionContext中有一个context的成员变量,但是这个变量存放的是Action的context并不是OGNL的context,需要注意(大雾) ,当然作为Action的一个属性,ValueStack也存放在这个context中,可以这么获取ValueStack:
ValueStack valueStack=ActionContext.getContext().getValueStack();
什么时候用context?什么时候用root?
因为在OGNL中不支持多个root对象,所以当需要在表达式中访问多个毫不相干的对象的时候就需要有一种方法将它们整合在一起,什么方法呢,就是用一个Map把装到一起,这个Map就叫做context,所以总结一下:单个对象—>root,多个对象—>context 。但是又因为Struts2对OGNL的单root进行了扩展,使得root也是多个了,所以吶,我们平时就把东西往栈里边使劲塞就对了。
一个小实验说明何时应该加上#号:
现在有这么一个Action:
public class FooAction extends ActionSupport { private User user; @Override public String execute() throws Exception { // 这个user对象处于root栈顶Action中,使用root的话一下就能找到 user = new User("username_root", "passwd_root");
// 往context中放了一个名为user的User对象,使用context的话会找到这个 User u2 = new User("username_context", "passwd_context"); ActionContext.getContext().getValueStack().getContext().put("user", u2); return SUCCESS; } public User getUser() { return user; } public void setUser(User user) { this.user = user; }}
在结果JSP页面中访问:
<s:property value="http://www.mamicode.com/user.username" /><br/><s:property value="http://www.mamicode.com/#user.username" /><br/>
结果:
可以看到不加#的话会找到root中的user,加了#的话会找到context中的user,这里只是说重名的情况下,不重名的话,比如。
Action:
public class FooAction extends ActionSupport { private User user; @Override public String execute() throws Exception { // 这个user对象处于root栈顶Action中,使用root的话一下就能找到 user = new User("username_root", "passwd_root"); return SUCCESS; } public User getUser() { return user; } public void setUser(User user) { this.user = user; }}
JSP:
<s:property value="http://www.mamicode.com/#user.username" />
#找不到放在root中的user,反过来呢?
Action:
public class FooAction extends ActionSupport { @Override public String execute() throws Exception { User u2 = new User("username_context", "passwd_context"); // 往context中放了一个名为user的User对象,使用context的话会找到这个 ActionContext.getContext().getValueStack().getContext().put("user", u2); return SUCCESS; }}
JSP:
<s:property value="http://www.mamicode.com/user.username" />
自动初始化 & 构造对象
当尝试导航到目标属性时,如果发现更深层的OGNL表达式中任何中间属性不存在(null),就会自动实例化它们,这里的实例化需要一个无参构造器。
1.构造普通对象
直接使用构造方法new即可,但是记得要用全路径类名,比如:
<s:property value="http://www.mamicode.com/new struts_practice_001.User(‘username‘,‘passwd‘)" />
使用User的这个构造器创建了一个对象:
public class User { ...
public User(String username, String passwd) { this.username = username; this.passwd = passwd; }
...
}
总结:使用new+全路径类名+可访问的构造器就可以创建对象了。
2.构造List对象
使用{},中间使用,进行分割,比如:
<s:property value="http://www.mamicode.com/{‘a‘,‘b‘}" />
构造了一个List容器,放置了两个字符串‘a‘和‘b‘;
<s:property value="http://www.mamicode.com/{new struts_practice_001.User(‘username‘,‘passwd‘),‘b‘}" />
构造了一个List容器里面放置了一个User对象和一个字符串。
3.构造Map对象
使用#{},类似于JSON的格式,比如:
<s:property value=http://www.mamicode.com/‘#{"Tom":new struts_practice_001.User("Tom","qwerty"),"Sam":new struts_practice_001.User("Sam","qwerty")}‘ />
Struts2中的OGNL
在Struts2中使用ValueStack对OGNL的封装,ValueStack是一个接口,OgnlValueStack是ValueStack的默认实现,ValueStack中的数据分两部分存放root和context(符合OGNL的概念),但是Struts2做了一个扩展,它实现了多root,是使用一个栈结构来实现的,当要使用某个属性的时候从栈顶往下搜索直到找到位置,就相当于栈中的每个元素都是root了,同时暴露了每个栈中元素的属性,不足之处时这样处理还是会让栈高层的对象屏蔽低层对象的同名属性,这样所谓的多root就实现的不完美喽哦
来看一下默认的ValueStack接口的实现OgnlValueStack的部分代码:
public class OgnlValueStack implements Serializable, ValueStack, ClearableValueStack, MemberAccessValueStack { ....... CompoundRoot root; //root实现 transient Map<String, Object> context; //context实现 .......}
context是用Map实现的,没什么好说的,但是发现root是一个CompoundRoot ,这是个什么东西呢,来看一下它的源代码:
/** * A Stack that is implemented using a List. * 这是一个借助于List实现的栈结构。 */public class CompoundRoot extends ArrayList { public CompoundRoot() { } public CompoundRoot(List list) { super(list); } public CompoundRoot cutStack(int index) { return new CompoundRoot(subList(index, size())); } //把列表的0下标当做栈顶,取得栈顶元素就返回List的0下标元素 public Object peek() { return get(0); } //同理,pop()就是将0下标移除 public Object pop() { return remove(0); } //插入到栈顶实际上就是插入到List的表头,但是这里我有个疑惑的地方就是既然是使用数组实现的, //那么频繁的插入删除必然导致数组元素移动、空间扩展之类的,这样难道不会影响到效率吗? //一直觉得这样子设计很撒比..... public void push(Object o) { add(0, o); }}
原生的OGNL的概念就是传入一个对象(这里的对象指的是JavaBean这种东西),然后在这个对象上导航,而Struts2的扩展就是将这个对象纵向(理解为竖着)展开变为从一个栈从上向下找,以此来实现多root。
ValueStack暴露了两个方法来实现OGNL取值设值的操作:
setValue(String expr, Object value); findValue(String expr);
查找值的时候会先从root中找,如果找不到的话就再去context中找:
private Object tryFindValue(String expr) throws OgnlException { Object value; expr = lookupForOverrides(expr); if (defaultType != null) { value = findValue(expr, defaultType); } else { //先尝试从root中找 value =http://www.mamicode.com/ getValueUsingOgnl(expr); if (value =http://www.mamicode.com/= null) { //然后root找不到再尝试从context中找 value =http://www.mamicode.com/ findInContext(expr); } } return value;}
OGNL的使用(原始类型或可以自动转换的类型):
首先要明确的是OGNL一般在前端页面中使用(至少是应该在不方便写Java代码的地方使用,那些直接拿Java代码测试的让人感觉好……),比如设置值的时候:
前端:
<form action="loginAction" method="post"> 用户名:<input type="text" name="user.username" /><br/> 密 码:<input type="password" name="user.passwd" /><br/> <input type="submit" value="登录" /> </form>
Action:
public class LoginAction extends ActionSupport { private User user; @Override public String execute() throws Exception { return SUCCESS; } public User getUser() { return user; } public void setUser(User user) { this.user = user; }}
注意前端页面中input的name,即参数的名字就是一个OGNL表达式,它会去ValueStack上匹配,实际上ValueStack总是使用的root栈,因为在请求到来之前就会把Action对象压入栈顶,当拿着OGNL表达式来栈找的时候,结合暴露属性的特点,在栈顶的Action实例上一下就找到了user这个属性,又因为这个属性也是一个对象,所以可以继续使用user.username来访问user对象的属性,这样可以有一个路径走啊走,这就是对象导航的意思。
取值和设置都是这样子,只要在合适的地方写字符串类型的OGNL表达式就可以啦。
来看几个栗子:
1. 访问对象属性
访问对象的属性,使用foo.bar的格式,如果是context里的话,使用#前缀(不使用也能找得到,因为默认的行为是会先去root中找再去context中找,但是我们明明知道在context还让它去root中效率就低了不是,而且万一root中有重名的就不会去找context中的了,这就得到错误结果了,但是呢,因为context几乎用不到(我个人是这样子认为的),所以下面的探讨都只基于root。
一个访问对象属性的例子:
<s:property value="user.username" />
注:使用<s:property />标签需要先导入标签库,如下:
<%@ taglib uri="/struts-tags" prefix="s" %>
<s:property />的value属性会被解析为OGNL表达式,user.username这个表达式的意思是取值栈上的Action中的一个叫user的对象的username属性。
也可以直接设置值,比如:
<s:property value="http://www.mamicode.com/user.username=‘Tom‘" />
直接这样设置值使用的比较少,更多的时候是通过导航来精确地从对象中取出所需要的属性,所以下面的例子就重点说访问了。
2. 访问类(静态)属性
格式:
@packageName.ClassName@staticPropertyName@包名.类名@类属性名
<s:property value="http://www.mamicode.com/@java.lang.Math@PI" />
3.调用对象方法
格式:
foo.bar(args…)对象名.方法名(参数…)
比如:
<s:property value="http://www.mamicode.com/foo()" />
是调用了Action的实例方法,Action的代码如下:
public class LoginAction extends ActionSupport { ... public String foo(){ return "FOO"; } ... }
<s:property value="http://www.mamicode.com/‘foo‘.length()" />
这个是调用了了String对象的length()方法(直接写一个字符串就是一个String对象的哦)
再比如:
<s:property value="http://www.mamicode.com/‘foo‘.substring(0,1)" />
调用了String对象的substring()方法截取[0,1)区间上的字符串。
4.调用类(静态)方法
一般工具类的方法都是静态的,这里一个可能的应用场景就是从前端页面调用工具类的静态方法来处理一些东西,当然这种逻辑最好还是在后台完成,最好保证前端页面和后台只是单纯的交换数据。
格式:
@packageName.ClassName@staticMethodName(args…)@包名.类名@静态方法名(参数…)
<!-- 允许OGNL调用静态方法 --><constant name="struts.ognl.allowStaticMethodAccess" value="true" />
然后就可以调用静态方法啦:
<s:property value="http://www.mamicode.com/@vs@foo()" />
上面的写法看起来很奇怪,这是因为全路径类名一般都很长很长,会很麻烦,如果是调用的值栈中的对象的话干脆就给它一个比较简单的标识,这个标识就是@vs,vs=ValueStack,直接写@vs是引用栈顶元素,@vs+数字是引用栈中相应位置的元素。
@vs是用来引用栈中元素的简洁写法。可以@vs@staticProperty访问静态属性,可以@vs@staticMethod(…args)访问静态方法
再比如:
<s:property value="http://www.mamicode.com/@java.lang.Math@sqrt(10)" />
总结一下:
1.调用格式@全路径类名@静态方法名(参数…)
2.可以用@vs的简写方式引用栈中元素。
5.数组类型
对数组按下标访问,是foo[0]、foo[1]这种格式,比如现在有这么一个Action:
public class FooAction extends ActionSupport { private User users[]; @Override public String execute() throws Exception { users=new User[5]; for(int i=0;i<users.length;i++){ users[i]=new User("username_"+i,"passwd_"+i); } return SUCCESS; } public User[] getUsers() { return users; } public void setUsers(User[] users) { this.users = users; }}
在结果JSP页面这么访问:
<s:property value="http://www.mamicode.com/users[0].username" /><br/><s:property value="http://www.mamicode.com/users[0].passwd" /><br/><s:property value="http://www.mamicode.com/users[1].username" /><br/><s:property value="http://www.mamicode.com/users[1].passwd" /><br/>
结果:
考虑下标越界的情况:
<s:property value="http://www.mamicode.com/users[0].username" /><br/><s:property value="http://www.mamicode.com/users[0].passwd" /><br/><s:property value="http://www.mamicode.com/users[100].username" /><br/><s:property value="http://www.mamicode.com/users[100].passwd" /><br/>
结果:
只是不能访问到,但是并不会抛异常,说明这里有一个错误兼容的机制啊。
总结:数组使用下标访问对应位置的元素,越界不抛异常。
6.List类型
先模拟一个从前端页面往后台传递参数的情景,比如注册的时候:
VO对象:
/** * VO,用来接收注册数据的 * * @author CC11001100 * */public class RegisterUser { private String username; private String passwd; private String repasswd; private String address; private List<String> hobby; @Override public String toString() { return "RegisterUser [username=" + username + ", passwd=" + passwd + ", repasswd=" + repasswd + ", address=" + address + ", hobby=" + hobby + "]"; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPasswd() { return passwd; } public void setPasswd(String passwd) { this.passwd = passwd; } public String getRepasswd() { return repasswd; } public void setRepasswd(String repasswd) { this.repasswd = repasswd; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public List<String> getHobby() { return hobby; } public void setHobby(List<String> hobby) { this.hobby = hobby; }}
Action:
public class RegisterAction extends ActionSupport { private RegisterUser user; @Override public String execute() throws Exception { for(int i=0;i<user.getHobby().size();i++){ System.out.println(user.getHobby().get(i)); } return SUCCESS; } public RegisterUser getUser() { return user; } public void setUser(RegisterUser user) { this.user = user; }}
前端页面:
<form action="registerAction" method="post"> 用户名:<input type="text" name="user.username" /><br/> 密码:<input type="password" name="user.passwd" /><br/> 确认密码:<input type="password" name="user.repasswd" /><br/> 爱好:<input type="checkbox" name="user.hobby" value="读书">读书</input> <input type="checkbox" name="user.hobby" value="看电影">看电影</input> <input type="checkbox" name="user.hobby" value="听音乐">听音乐</input><br/> 地址:<input type="text" name="user.address" /><br/> <input type="submit" value="注册" /></form>
比较关键的几个地方:
1.在往里面放的时候可以不指定下标,List有append这种功能啊。
2.泛型的List一定不要预先初始化化(《Struts2 in action》)。
List的访问与数组基本一致,通过下标就可以访问得到了,不再赘述。
(备注:jdk1.5之前不支持类型指定,基本用不到不浪费精力研究了,如需要解决办法自行百度)
7.Map类型
模拟一个场景,需要输入父母亲的联系方式:
前端页面:
<form action="setContactPersonAction" method="post"> 父亲联系方式:<input type="text" name="contacts[‘father‘]" /><br/> 母亲联系方式:<input type="text" name="contacts[‘mother‘]" /><br/> <input type="submit" value="登录" /></form>
Action:
public class SetContactPersonAction extends ActionSupport { private Map<String, String> contacts; @Override public String execute() throws Exception { for(String key:contacts.keySet()){ System.out.println(key+":"+contacts.get(key)); } return SUCCESS; } public Map<String, String> getContacts() { return contacts; } public void setContacts(Map<String, String> contacts) { this.contacts = contacts; }}
输入:
结果:
8.集合类型的遍历
9.集合运算
? 获得所有符合的元素
^ 只获取第一个
$ 只获取最后一个
串联表达式:
OGNL支持表达式串联,之前一直不知道这种写法该叫做什么,就是可以连着写好几个表达式同时将最后一个表达式的值作为个串联表达式的值返回,串联表达式如下:
a++,b++,c++;
整个表达式的结果是c++的结果,注意该死的Java不支持串联表达式,不知道最新版会不会加上这功能。
Lambda表达式:
略。
三个符号(#、%、$):
1. #号
1.1访问非根对象属性,#相当于ActionContext.getContext();
context中的一些常用的命名对象:
#parameters 用于访问http请求参数#request 用于访问request中的属性#session 用于访问session中的属性#application 用于访问application中的属性#attr 会依次搜索PageContext、HttpServletRequest、HttpSession、ServletContext
1.2.用于过滤和投影,比如
person.{?#this.age>20}
1.3.用于构造Map,比如:
<s:property value="http://www.mamicode.com/{new struts_practice_001.User(‘username‘,‘passwd‘),‘b‘}" />
2.%号
1.将字符串强制解析为OGNL表达式 ,比如
%{#foo[“bar”]}
3.$号
1.在国际化资源文件中,引用OGNL表达式,例如国际化资源文件中的代码:
2.在Struts2框架的配置文件中引用OGNL表达式,例如
OGNL表达式和EL表达式的区别
1.OGNL表达式必须配合Struts2的标签使用,而EL可以单独使用。
类型转换器:
八种基本类型和String、Date、array、List、Map会自动进行转换(因为内置了类型转换器)
更复杂的类型需要自定义类型转换器。
写得太长了,类型转换器另开一篇单独写吧。
参考:(此处替换为类型转换器博文地址)
三、OGNL工作原理?
暂无、
Struts2之OGNL