摘要:
MVC框架的分层结构提高了程序的可维护性、可移植性、可扩展性与可重用性,降低了程序的开发难度,但是也引来了一些新的问题,比如不同层次间的数据流转问题。OGNL的出现填平了这条沟壑,成为字符串与Java对象之间沟通的桥梁,这也是Struts1与WebWork整合成Struts2时引入WebWork的OGNL的原因。本文给出了Struts2官方对OGNL的描述,并就OGNL在Struts2中的进一步应用进行了详述,包括数据访问、控制标签等。
版权声明:
本文原创作者:书呆子Rico
作者博客地址:http://blog.csdn.net/justloveyou_/
友情提示:
为了更好地了解 OGNL,笔者以两篇博客的篇幅来介绍OGNL:《与MVC框架解耦的OGNL:前世今生及其基本用法》和《再述OGNL:在Struts2中的应用》。其中,在本文(《与MVC框架解耦的OGNL:前世今生及其基本用法》)中,我们首先介绍了OGNL的前世今生,并结合具体实例和源码阐述了OGNL的实质、OGNL三要素和基本用法语法。在此基础上,本篇的姊妹篇《再述OGNL:在Struts2中的应用》详尽地介绍了OGNL的在Struts2中的应用。
Ps:读者强烈建议读者在阅读本篇博文前,先阅读其姊妹篇《与MVC框架解耦的OGNL:前世今生及其基本用法》,因为二者之间关联性很强。通过对《与MVC框架解耦的OGNL:前世今生及其基本用法》的阅读,相信读者会对Struts2中的OGNL有一个更加深刻地认识。
一. Struts2 为何要集成WebWork的OGNL
众所周知,OGNL充斥在Struts2前后台数据传递与存储的方方面面,给Struts2中数据的处理带来了极大的方便。实际上,在我们使用 MVC 架构模式进行开发Web应用时,数据往往需要在各层之间进行流转。由于数据在不同层次中的表现形式不尽相同,所以这种流转会很麻烦,特别是在Controller层与View层之间进行流转。我们知道,数据在View层(视图页面)的表现形式是一个 扁平的、不带任何数据类型的字符串,无论该数据的数据结构有多复杂,数据类型有多丰富,在View层都只能以字符串的形式进行展现。 与此相反,数据在Controller层(Java世界)可以有 丰富的数据结构和数据类型,比如我们可以自定义各种各样的类型,这些类型间也可以有多种关系(包含继承、聚合等),我们通常可以把这种丰富的数据模型抽象成 对象树。
在这种情况下,由于Controller与View层次中的数据不能直接匹配,因此数据在二者之间互相流转传递时就会很麻烦。实际上,这也是任何一个成熟的MVC框架必须要解决的问题,具体如下:
- 第一,当数据从View层传递到Controller层时,MVC框架应该能够保证将扁平而分散在各处的数据能以一定的规则高效地映射到Java世界中的对象树中去。也就是说,能够自动地将字符串类型转化为Java中各种类型;
- 第二,当数据从Controller层传递到View层时,MVC框架应该保证在View层能够以某种简易的规则对Java世界中的对象(树)进行访问,并且能够在一定程度上控制对象树中的数据在View层的显示格式。
事实上,解决数据由于表现形式的不同而发生流转不匹配的问题对我们来说其实并不陌生。同样的问题也会出现在Java世界与数据库世界中,面对这种对象与关系模型的不匹配,我们可以使用 ORM 框架去解决,比如Hibernate,MyBatis等。类似地,现在在Web开发中也同样发生了这种不匹配,所以我们理论上也可以借助一些工具来帮助我们解决问题,并且任何一个成熟的MVC框架也应该提供这种工具来帮助我们解决这个问题。不出所料,为了解决数据在View层与Controller层流转时的不匹配性,Struts2 提供了 OGNL 来帮助我们解决个问题。
二. Struts2 对 OGNL 描述
在本篇的姊妹篇《与MVC框架解耦的OGNL:前世今生及其基本用法》,我们已经知道OGNL是一种功能强大的表达式语言。其中,OGNL表达式、OGNL上下文和OGNL根对象是OGNL的三要素,它们三者的关系是:OGNL表达式的计算是围绕OGNL上下文(OgnlContext)进行的,OGNL根对象侧面指明了OGNL表达式的操作对象,而OGNL表达式则指明了其操作语义。也就是说,OGNL的操作实际上就是围绕着这三个参数而进行的。在此基础上,它通过简单一致的语法,可以存取Java对象树中的任意属性、调用Java对象树的方法,并能够自动实现必要的类型转化。更形象地说,如果我们把表达式看做是一个带有语义的字符串,那么OGNL无疑是这个语义字符串与Java对象之间沟通的桥梁。以下Struts2官方对OGNL的两段描述:
The framework uses a standard naming context to evaluate OGNL expressions. The top level object dealing with OGNL is a Map (usually referred as a context map or context). OGNL has a notion of there being a root (or default) object within the context. In expression, the properties of the root object can be referenced without any special “marker” notion. References to other objects are marked with a pound sign (#).
The framework sets the OGNL context to be our ActionContext, and the value stack to be the OGNL root object. (The value stack is a set of several objects, but to OGNL it appears to be a single object.) Along with the value stack, the framework places other objects in the ActionContext, including Maps representing the application, session, and request contexts. These objects coexist in the ActionContext, alongside the value stack (our OGNL root).
根据上面的表述我们可以知道,针对根对象(Root Object)的操作,表达式是 自根对象到被访问对象的某个链式操作的字符串表示;而针对上下文环境(Context)的操作,表达式是 自上下文环境(Context)到被访问对象的某个链式操作的字符串表示,但是必须给这个字符串加上前缀 “#”,以表示与访问根对象的区别。此外,在 Struts2 中,OGNLContext的实现者为ActionContext(Stack Context),而 OGNLContext中的Root Object是Value Stack,并且 Application、Session、Request等我们经常访问的对象(Map表示)都被放到了ActionContext中。结构示意图如下:
1、ActionContext(StackContext)
ActionContext是一个Action对象执行所依赖的上下文环境,每一个Action在执行时都会有一个专属的上下文环境。这个上下文环境可以看作是运行Action所需的资源集合,其中包括 application、session、request、parameters、attr及当前Action对象。实际上,正因为 Struts2 将 OGNLContext 设置为 ActionContext,我们才能在Struts2中随意地使用OGNL来完成数据在Controller层与View层之间进行流转。
2、ValueStack
ValueStack对象相当于一个栈,它贯穿整个Action的生命周期,并且每个Action类的对象实例都会拥有一个自身的ValueStack对象。当Struts2接收到一个*.action请求时,在调用Action方法处理请求前会迅速创建ActionContext,ValueStack,Action 等对象,并会把当前的Action存放进ValueStack的栈顶。ValueStack对象和Parameters、Request、Session、Application、Attr等对象都属于 ActionContext 的一部分,只不过ValueStack对象是ActionContext的根对象。根对象和另外五个对象是有区别的,根对象可以省写#号,比如<s:property value=”user.username”/>。需要特别指出的是,ValueStack 是ActionContext的栈顶(根)对象,而当前运行的Action是ValueStack的栈顶对象。并且,ActionContext(ValueStack)的生命周期与请求相关,每次请求产生一个ActionContext(ValueStack),默认当前的Action会被自动放到ValueStack里。
注意,在struts2中,根对象ValueStack的实现类为com.opensymphony.xwork2.ognl.OgnlValueStack(该类实现了ValueStack接口),它能够存放一组对象。通常,我们在OGNL表达式里直接写上属性的名称即可访问ValueStack的对象属性,搜索顺序是从ValueStack的栈顶对象开始寻找,如果栈顶对象不存在该属性,就会从第二个对象寻找,如果没有找到就从第三个对象寻找,依次往下访问,直到找到为止。
特别地,ValueStack中可以有多个Action对象,Struts2的服务器跳转(通过设置result类型为chain实现)就会共用值栈。比如,从Action1通过服务器跳转到Action2时,就意味着这两个Action是共享一个值栈的,因为一次请求只会有一个值栈。具体流程是这样的:服务器首先接收到Action1请求后,会产生一个值栈并将Action1对象存放到栈顶。然后,经过服务器跳转到Action2,这时就会把Action2对象压入值栈的栈顶位置,此时Action1对象就位于栈底了,因为值栈也是栈嘛,所以一定遵循FILO原则。另外,与服务器端跳转不同的是,客户端跳转(通过设置result类型为redirect实现)使用的是各自的值栈,因为是两个请求嘛。
3、ActionContext 中的其他常用对象
在Struts2中,Struts2会把我们经常需要用到的 Request、Session、Application 等对象自动放到 ActionContext中,因此我们可以很方便地使用 OGNL 去访问它们范围的属性,如下表所示:
名称 | 作用 | 实例 |
---|---|---|
parameters | 包含当前HTTP 请求参数 的Map | #parameters.userName作用相当于request.getParameter(“userName”) |
request | 包含当前HttpServletRequest的 属性(attribute) 的Map | #request.userName相当于request.getAttribute(“userName”) |
session | 包含当前HttpSession的 属性(attribute) 的Map | #session.userName相当于session.getAttribute(“userName”) |
application | 包含当前应用的ServletContext的 属性(attribute) 的Map | #application.userName相当于application.getAttribute(“userName”) |
attr | 依次从 Request、Session、Application范围搜索 属性(attribute) |
三. OGNL在Struts2中的使用实例
到此为止,加上我们在本篇的姊妹篇《与MVC框架解耦的OGNL:前世今生及其基本用法》对OGNL介绍,我们基本介绍完了OGNL的基本原理和基本概念。下面我们借用马士兵老师的例子介绍一下OGNL表达式在Struts2中的用法,实际上,其与我们在本篇的姊妹篇《与MVC框架解耦的OGNL:前世今生及其基本用法》介绍的单独使用OGNL的情况类似。在这里,相关Bean类就不给出了,大家知道OGNL怎么使用就足够了。
<?xml version="1.0" encoding="GB18030" ?>
<%@ page language="java" contentType="text/html; charset=GB18030"
pageEncoding="GB18030"%>
<%@ taglib uri="/struts-tags" prefix="s" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=GB18030" />
<title>OGNL表达式语言学习</title>
</head>
<body>
<ol>
<li>访问值栈中的action的普通属性: username = <s:property value="username"/> </li>
<li>访问值栈中对象的普通属性(get set方法):<s:property value="user.age"/> | <s:property value="user[‘age‘]"/> | <s:property value="user[\"age\"]"/> | wrong: <%--<s:property value="user[age]"/>--%></li>
<li>访问值栈中对象的普通属性(get set方法): <s:property value="cat.friend.name"/></li>
<li>访问值栈中对象的普通方法:<s:property value="password.length()"/></li>
<li>访问值栈中对象的普通方法:<s:property value="cat.miaomiao()" /></li>
<li>访问值栈中action的普通方法:<s:property value="m()" /></li>
<hr />
<li>访问静态方法:<s:property value="@[email protected]()"/></li>
<li>访问静态属性:<s:property value="@[email protected]"/></li>
<li>访问Math类的静态方法:<s:property value="@@max(2,3)" /></li>
<hr />
<li>访问普通类的构造方法:<s:property value="new com.bjsxt.struts2.ognl.User(8)"/></li>
<hr />
<li>访问List:<s:property value="users"/></li>
<li>访问List中某个元素:<s:property value="users[1]"/></li>
<li>访问List中元素某个属性的集合:<s:property value="users.{age}"/></li>
<li>访问List中元素某个属性的集合中的特定值:<s:property value="users.{age}[0]"/> | <s:property value="users[0].age"/></li>
<li>访问Set:<s:property value="dogs"/></li>
<li>访问Set中某个元素:<s:property value="dogs[1]"/></li> <!--不会有输出,因为Set无序 -->
<li>访问Map:<s:property value="dogMap"/></li>
<li>访问Map中某个元素:<s:property value="dogMap.dog101"/> | <s:property value="dogMap[‘dog101‘]"/> | <s:property value="dogMap[\"dog101\"]"/></li>
<li>访问Map中所有的key:<s:property value="dogMap.keys"/></li>
<li>访问Map中所有的value:<s:property value="dogMap.values"/></li>
<li>访问容器的大小:<s:property value="dogMap.size()"/> | <s:property value="users.size"/> <!--容器的伪属性 --></li>
<hr />
<li>投影(过滤):<s:property value="users.{?#this.age==1}[0]"/></li>
<li>投影:<s:property value="users.{^#this.age>1}.{age}"/></li>
<li>投影:<s:property value="users.{$#this.age>1}.{age}"/></li>
<li>投影:<s:property value="users.{$#this.age>1}.{age} == null"/></li>
<hr />
<li>[]:<s:property value="[0].username"/></li>
</ol>
<s:debug></s:debug>
</body>
</html>
四. 容器的伪属性
OGNL能够直接引用容器的一些特殊的属性,即使这些属性没有getter/setter。比如,我们可以这样获取List的大小:<s:property value=”testList.size”/>,我们称这些特殊的属性为
伪属性。当使用OGNL表达式引用这些属性时,OGNL会调用自动相应的方法,比如size()、length()。以下是一些常用容器的伪属性:
- List的伪属性:size、isEmpty、iterator;
- Set的伪属性:size、isEmpty、iterator;
- Map的伪属性:size、isEmpty、keys、values;
- Iterator的伪属性:next、hasNext;
- Enumeration伪属性:next、hasNext、nextElement、hasMoreElements。
五. 常用控制标签
对于Struts2 中标签的使用,我们必须要知道:Struts2 中的很多标签都可以指定 var/id 属性的,一旦指定了 var/id 属性,则Struts2就会将新生成、新设置的值放入ActionContext中,对该值的访问就必须加前缀 “#”。若不指定 var 属性,则新生成、新设置的值就不会放入ActionContext中,因此我们就只能在该标签内部访问新生成、新设置的值。特别需要注意的是,此时新生成、新设置的值位于ValueStack中,因此我们可以直接访问。特别地,若对set标签指定指定 var/id 属性,则Struts2就会将新生成、新设置的值放入ValueStack中。
下面,我们介绍几个Struts2常用的几个标签。
1、条件标签
条件标签主要有<s:if>、<s:elseif> 和 <s:esle>3个子标签构成,用于执行基本的条件流转。其中,test是条件判定语句,值为boolean类型。
<s:if test="#request.username ==‘admin‘">
您是admin!
</s:if>
<s:elseif test="#request.username ==‘manager‘">
您是manager!
</s:elseif>
<s:else>
您没有身份!
</s:else>
2、迭代标签
<s:iterator>标签用于遍历 容器(Java.util.Collection、Java.util.Map) 对象,如下所示:
使用id/var属性,加前缀"#":</br>
<s:iterator value="users" id="uuu">
<!-- OK -->
<s:property value="#uuu.age"/>
<!-- OK -->
<s:property value="age"/>
<br/>
</s:iterator>
使用id/var属性,不加前缀"#":</br>
<s:iterator value="users" id="uuuu">
<!-- 访问不到~ -->
<s:property value="uuuu.age"/>
<br/>
</s:iterator>
使用上述代码遍历List类型的users,结果如下图所示:
其中,使用上述代码遍历List类型的users且指定 var/id 属性时,Struts2就会将新生成、新设置的值uuu放入ActionContext中,如下图所示:
其中,id/var 是指定集合中元素在 ActionContext 中的名称,value 是指定迭代的 迭代体。此外,status 属性在迭代时会产生一个IteratorStatus对象,该对象可以 判断当前元素的位置。也就是说,当声明iterator的status属性时,通过 #statusName.method 可以使用以下方法:
- even : boolean - 如果当前迭代位置是偶数返回true
- odd : boolean - 如果当前迭代位置是奇数返回true
- count : int - 返回当前迭代位置的计数(从1开始)
- index : int - 返回当前迭代位置的编号(从0开始)
- first : boolean - 如果当前迭代位置是第一位时返回true
- last : boolean - 如果当前迭代位置是最后一位时返回true
- modulus(operand : int) : int - 返回当前计数(从1开始)与指定操作数的模数
3、set标签
set标签用于将某个值放入指定范围内,比如,application、session范围等。特别地,当某个值所在的对象图深度非常深时,例如:person.worker.wife.parent.age,每次访问该值不仅性能地下,而可读性也很差。为了解决这个问题,可以将改该值设置成一个新值,并放入特定范围内。set标签有以下几个属性:
- name: 必填,重新生成的新变量的名字;
- scope: 可选,指定新变量被放置的范围,该属性可以接受application、session、request、page或action 5个值。如果没有指定,默认是放置在Stack Context中;
- value: 可选,指定将赋给变量的值。如果没指定,则将ValueStack栈顶的值赋给新变量;
- var/id: 可选,指定该元素的引用ID,如果指定了将会将该值放在 ValueStack 中。
set标签用于生成一个新变量,并把该变量放置到指定的范围内,这样就允许直接使用JSP表达式语言(EL)来访问这些变量,当然也可以通过struts2标签来访问,示例如下:
<!--使用param标签为JavaBean实例传入参数-->
<s:bean name="lee.Person" id="p">
<s:param name="name" value="‘yeeku‘"/>
<s:param name="age" value="29"/>
</s:bean>
将Stack Context中的p值放入默认范围内。<br>
<s:set value="#p" name="xxx"/>
<s:property value="#xxx.name"/> <br>
将Stack Context中的p值放入application范围内<br>
<s:set value="#p" name="xxx" scope="application"/>
<s:property value="#attr.xxx.name"/> <br>
将Stack Context中的p值放入session范围内<br>
<s:set value="#p" name="xxx" scope="session"/>
<!--使用JSP2.0表达式语言直接访问session中的属性-->
${sessionScope.xxx.name}<br>
六. 注意
下面是我们需要在Struts2中注意的几点:
(1). OGNL表达式的计算是围绕OGNL上下文进行的。由本篇的姊妹篇《与MVC框架解耦的OGNL:前世今生及其基本用法》知,OGNL上下文实际上就是一个Map对象,由ognl.OgnlContext类表示,它里面可以存放很多个JavaBean对象。它有一个上下文根对象,根对象可以直接使用名来访问或直接使用它的属性名访问它的属性值,而其他对象要加前缀“#key;
(2). 在Struts2中,OGNL必须配合Struts标签使用(<s:property value=”xxx”/>)而EL表达式可以直接使用,并且能够使用EL的一定可以使用OGNL,能使用OGNL的不一定能够使用EL;
(3). Struts2将ActionContext设置为OGNL上下文,并将值栈作为OGNL的根对象放置到ActionContext中;
(4). 值栈(ValueStack)贯穿整个Action的生命周期,并且每个Action类的对象实例都会拥有一个自身的ValueStack对象,并且Struts2总是把当前Action实例放置在栈顶。
七. 更多
更多关于OGNL的前世今生,并结合具体实例和源码阐述了OGNL的实质、OGNL三要素和基本用法语法,请读者移步我的博客《与MVC框架解耦的OGNL:前世今生及其基本用法》。
更多关于如何开发一个Struts2应用,请读者移步我的博客《 Struts2 实战:从 登录Demo 看 Struts2 应用开发》。
引用