本文已经是《AOM 神奇魔力》系列的第八篇文章了。前七篇,更多的是一种教程、指南类的文字, 现在,是时候写点深入些的东西了。
这篇文章,要写给“JSF 怀疑论者”。
自从 Java 诞生以来,Java在“服务器端编程”已经取得了巨大的成功,这在业界已经达成了共识; 但长久以来,Java在“客户端编程”方面并没有成就大的气候。谈GUI编程,官方标准Swing斗不过开源技术SWT,论Web编程,JSP 依然停留在 ASP 技术所达到的高度, 更无法和.NET的 WebForm等技术相比,现在甚至还受到 RoR 的威胁。
是因为整个Java社区对客户端编程不够关注吗?其实不然,GUI编程的Eclipse RCP技术,已经隐隐有一统天下之势,而在Web编程领域,则有Struts、 WebWork、Spring MVC、 Echo等诸多 Framework供用户选择。但对于J2EE Web开发人员来说,有得选固然是一件好事,但选择面太多,也未免会让人挑花了眼,无所适从。
为什么Java在 Web 编程领域会是今天的这种局面?笔者以为:Java官方组织JCP在Web编程领域无法提出一个广受业界认可并被接受的技术,并且缺乏一个强有力的推动力, 是造成今天这种局面的一个很重要的因素。
这句话可能有点冤枉JCP,事实上,JCP在 Web 编程领域也提出过一些JSR,其中,JSF(Java Server Faces)无疑是其中的矫矫者,并且 JCP 将 JSF 放到很重要的一个位置,从 JSF 1.2 被纳入到 JavaEE 5.0 整体规范即可见一斑。
那么,我们就要问了: JSF 1.0从2004年就被提出,为什么时至今日,它依然没有得到一个广泛的普及呢?
我的个人看法是:因为大家普遍对JSF持一定的怀疑态度。笔者就曾经是一个典型的 JSF 怀疑论者。而对 JSF 最大的怀疑就是:它对状态(ViewState)的处理。 由于JSF需要在客户端及服务器端之间进行状态的维护,使我们普遍认为:这是一种“重量级”的协议,以致于它有可能对系统性能造成严重影响。
在这里,不得不提及一个人,袁红岗先生, 可以说:是他完全改变了我对 JSF 的认识,按照他的原话是:不是规范不好,而是实现不足。
本文,尝试探讨一下 JSF 中最为人所诟病的“状态”,并且阐述:AOM是怎样“干掉”JSF状态的。
同样,在阅读本文之前,我依然建议你首先阅读前几篇文章:
什么是JSF?用最通俗的话来说就是:用C/S模式开发B/S应用。很多同学都做过C/S架构的应用,在传统的GUI编程中,其核心无非是: 控件与事件。控件用来展现界面,事件用来响应操作。JSF同样如此。JSF用组件来描绘页面,用事件来调用后台的业务逻辑。 但有一点需要引起我们的注意:B/S应用的底层支撑协议是 HTTP,这是一种无连接(Stateless)的协议。一旦用户通过 HTTP 获得页面内容以后,服务器就与你断掉连接并且忘记你是谁了,当你点击页面的按钮与服务器再次交互时,你让服务器如何响应? 它连你是谁都不知道了!
于是,JSF提出了 ViewState 的概念。
我们曾经在前文中说过:JSF是基于组件树的,那么, ViewState,就是指“组件树的状态”。
在客户端每次请求到来时,JSF引擎首先要做的第一件事就是:恢复组件树(Restore View),如下图“JSF生命周期”所示。从哪里恢复组件树?从 ViewState 中恢复。

由此,产生了第一个问题:“ViewState到底包含些什么信息?”
主要包含两部分内容:一部分是组件树的结构信息,如,这棵树上有哪些组件,它们的父子关系是怎样的;另一部分是组件树上每个节点的信息, 如,组件树上的某个textField的id是什么?value是什么?某些复杂组件,还有自己的一些特有信息,如:DataGrid当前选中的是哪一行? 树节点的展开状态是怎样的?等等。
“那么,ViewState 保存在什么地方呢?”
可以保存在服务器端(放在用户的session里),也可以保存在客户端(放在html代码中的某个hidden字段, 用户感觉不到)。 并且,你可以决定让 ViewState 保存在哪里。在 web.xml 中增加以下配置:
<context-param>
<param-name>javax.faces.STATE_SAVING_METHOD</param-name>
<param-value>client</param-value>
</context-param>
其中,参数值可以是 client 或者 server。
“这两种保存方式孰优孰劣呢?”
这是一个权衡问题,需要具体场景具体分析。放在客户端,每次与服务器端交互时,要把 ViewState 传递到服务器,无疑消耗带宽,并且恢复组件树需要消耗CPU资源;放在服务器端,减少了带宽的消耗及组件树重建所消耗的 CPU资源,但同时, 这对服务器内存是个巨大的考验。
“照你这么说:由于HTTP协议导致JSF必须维护 ViewState ,而我们根本不可能抛弃 HTTP 协议,那么,此问题无解喽?”
在回答这个问题之前,让我们对 ViewState 先有个感性认识。
首先,让我们做一个简单的 HelloWorld 的应用,从而使各位同学直面 ViewState所带来的“惨淡人生”。 为了尽可能让笔者的观点客观、公正,这个例子运行在 MyFaces 1.1.5 + Tomcat 6上。以下是示例性代码:
helloWorld.jsp
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%>
<html>
<head><title>Hello World</title></head>
<body>
<f:view>
<h:form id="form">
<h:inputText id="txt1" value="#{helloWorldBean.name}"/>
<h:commandButton id="btn1" value="sayHello" action="#{helloWorldBean.sayHello}"/>
<h:outputText value="#{helloWorldBean.result}"/>
</h:form>
</f:view>
</body>
</html>
HelloWorldBean
public class HelloWorldBean {
private String name;
private String result;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getResult() {
return result;
}
public void setResult(String result) {
this.result = result;
}
public Object sayHello() {
this.result = "Hello " + name;
return null;
}
}
当然,为了让上述程序能够在 MyFaces 中运行,你不能像在 AOM 中那样偷懒省事,你还需要配置 faces-confg.xml:
<managed-bean>
<managed-bean-name>helloWorldBean</managed-bean-name>
<managed-bean-class>demo.HelloWorldBean</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
</managed-bean>
程序运行效果如下:

现在,让我们来看看,到底什么是 ViewState。我们查看该页面的生成的HTML,下面是示例性代码。郑重声明:为了不影响各位看官的注意力,该HTML代码我已经进行过大幅删减。
<html>
<head><title>Hello World</title></head>
<body>
<form id="form" name="form" method="post" action="/myfaces1/helloWorld.jsf" enctype="application/x-www-form-urlencoded">
<input id="form:txt1" name="form:txt1" type="text" value="Kevin" />
<input id="form:btn1" name="form:btn1" type="submit" value="sayHello" onclick="//省略此处js代码" />
<input type="hidden" name="autoScroll" />
Hello Kevin
<input type="hidden" name="form_SUBMIT" value="1" />
<input type="hidden" name="form:_idcl" />
<input type="hidden" name="form:_link_hidden_" />
<script type="text/javascript"><!--
//中间代码省略
}//--></script><input type="hidden" name="javax.faces.ViewState" id="javax.faces.ViewState"
value="8EgC7hvJoXWgMHaUZxk5rx66APlnNueyP32ajDxbvc/i5akMf2jX5W9NF3roe9lcuwszUVsDa0paIZKDZQ/qA9OQRll
/xMrtmLgZKpZNcAtQpr8GrghrNLKYKcMTveuOoPxjW0RBSGWLlbNKis4mRBrO4tujcnsSlCHO1/Ecj9sLLaWs7LYqdJslRL9wH
nfUPRsur/2RBQcVQUT6EI64y6BwzaXQK37vpgyPtA7WPQ9Zg60FtkeWwSPtEDqRX46pa/pdgaxKwT5Qpr8GrghrNJXzObHKV6N
3y91lSAwHr2ypchHMOzsb37act+t62rcJ6OF0Xl+b2Roe1OHE+Cd08IFhSRy0QFDihtkzwJTcNMF/uXdEjTHklsOSgV40cTZS0
e17vwBJhFIgmnPhSnPHsQibPAnCvlbYOCGLTRzJwX/1j1r3IbaA1BDTyeB9EgLJhlIxyXpUaCSEfG4vrCIXNRrtrs2gSGsvkxO
AjcwktcYjarrUWF9mT1BShWb+VgU6izAH8VR0q9hCcegcPPGltwmVhYCHQdjvMFydGhVy05ib+ZRudPSUnDSNXWI0jg5wlxaqJ
XjSfYRy5DmKQ63s1YOtYjlQRBbc77DlnDqgClk9vW+78gcrh5S72UbBLuYklj2/j4/HQN56kefo9v4O0mgZej+pVgOXqXIRzDs7
G9+MX7OeWlYh8+A3qRbRjXYxpXX6rh2l0PCVNdEzObxp+TSNXWI0jg5wF7XZ1xw6U2hy5DmKQ63s1YOtYjlQRBbc77DlnDqgClm
Z0Nd1UilNN4Zdu/sD4UND1r9wt3VQncrGEIJCVqayfNGwH/lgGgUi8noAGlHl2N8VOmWM2Vmx1rwhWWjbGVxqqg8OL+xF2FUdI0
bfKC3NDaY3N2g+3c3FAnSbInA8BwMjNajAaPnmJhsVkimNZwH1RON5D5rl8MbnFJCP83ylN61THadAkuvYfQJBPMTlB9XIOwtJ2
D52lAAXHu9NkRoLT0oYlKop4EKKkLC8/izyGoJJd6rdcms0PhTPz/3NZeVwO+hogJwnJNMIWARV1ROKarlmK0iiZCAw4/TbxeGpz
fLk4fSA/OyjTiVIOSqmIW/mamZgfZeciSmNSMYPIal6jdrK1TC8lRMdm+b4M7RJvZeU5H2RnzvIi5Y4VrFWy+92cKoFPyXRYjGuQ
79chmuB79x4V2jovomxKG/lmTWX1XXiPf+hX90GXbF9OC6TVq0UC3FQl3q5kwXD6RbzWrNR8XIE0c1F3SP3K0QhE7GoN8e1vlR+C
yqAVnRTwB0FivxHtrGcQM9+2+3e73sN885HFscOHjjmdTSoNuNuZVlgHBYKVda+2pPzTNaFOREXAqpmjNu5opnJ+6kpws21Vam7f
1TxbuT18BNrncpLXIkw7Ee5WbpD03dxqwTfqS27AiWNO22MfqoHv+x3T0EIldGtnr/nH24VWmZu0b0Vg4T94MPLblndyyeAJx5v0
HvRRrHs2zBESEeG8kOc7Uh3f0YRBwpD4Q1XJ3AKCKYM/uo03/v33vs2DIH7fc0xLf5rHn+wZDCErtTH/b9N/+3ycKNSwnzcJYgh+
9eFcyEsS1tCBqoyzw7kxC3pIjbGLX1Oh/EsM3EErP7yXov9Y2s2OTc5V9h0ELeoXUSlTnJ9xu9hy0UcI9EG61eqf30cqBA6tagqz
TBU4rrc7fjb9u9bXp32W4L+375TMddCPMK523UaclPttWnM5dAvtGApDyn5DjX3uzosE1vLa/gOEH6KItXUkE7s7EM7JCWQTuzsQ
zskJbpO/KI39WbmL/lwfsH37pJCYckCZ3lSnKyZ7GauxpDNZozbuaKZyfu6Bc8WkMH914Y+tOJ9+/OgFgpV1r7ak/O0K5F4kWDW7I
a9LFO5SL+axhCCQlamsnzb7cbDAAgnwvbvG6JUbihKs/cb0YE9x/vQjIgPboyeKEOc7Uh3f0YR0bXxXBC5c5MKWEpirDmzaBFMXGc
8SvN0n7MyXMfUr/zUriFZjAM6kgdVsCoX5iNSDsIuutEanxoSYuR0WZ4aRsl2IncyYDXz609ZTC5ZMRTi7IP/xHpR/I14pvA/KdGT
C8xl7PAivL8hy9nDl2MP4fQ4TYzl7khXBTQ9yo4fP6/A/umN6ViIrxuYyTVUcjk2HRNM7X2Z89mhG2/hdbhQf+jhdF5fm9ka+djFU
7hnftSFSvTCKp57iLsLM1FbA2tKWiGSg2UP6gNALGUSiMXAOmfZuOa50reMBA96KYiHdhgc2hG6SfiDAXQpPuAqDICrT/LIJ6KycN
VNEeuB7MDpE7MuPGMjTJ2Mt+hVe2bi9GXQTUHVAR5v6JBO7OxDOyQlkE7s7EM7JCWQTuzsQzskJZALzVk/4+SOZozbuaKZyfuzbVn
pwlomGDEPp13O8HHMv50JwxckcrG0K5F4kWDW7C5IDHmWHDicxhCCQlamsnxS/rLgFQwLJLezkYK2SFnXVwWZg8r2+78X/U3NhN/Ox
RwC56I4Wb/rNiEZsXK/Eo7lHzkIomAUGNacJ2Aq6osa2OO4d8sS+RHELi6VHgjyh9lFHGr1+Qc8TKzHt+DgFokOW6i17FpIx9yh6jk
Ne6gyv/g+nuBkvxf08+mIoBczk5T49hGgO2m4T/LIJ6KycNXowkjcV8T200Eqphq9KbNeQles1Ll9okG8OMcFtmC3RpBO7OxDOyQlk
E7s7EM7JCWQTuzsQzskJfs6Hc4oaBiRLyOq13uE9twRIAQkJOfnAMGoQ1mSUs46GxWSKY1nAfXwJ9jo42i1sHqwpuVAY/euF/1NzYT
fzsUcAueiOFm/6zYhGbFyvxKO5R85CKJgFBgLlm2i/lETBmW+IM+U0TN52lN5u1f1H/7f5LhzgtRt95Woqo6zq+WctOTpTRxIAVOWH
kgLWVBlW62trHfdkBAaInBVaJrFwYn3IvDRHI/NBXC6k9e/Pzf+" />
</form>
<!-- MYFACES JAVASCRIPT -->
<script type="text/javascript"><!--
function getScrolling() {
//中间代码省略
}
//--></script>
</body>
</html>
是不是看上去有种头晕眼花的感觉?如果我写文章是按照字数卖钱的话,那么,多贴几个 ViewState 的示例,在下可就大大的发财了:)
上述<input type="hidden" name="javax.faces.ViewState">标签中的内容,就是传说中的 ViewState。
“这堆无意义的字符到底是什么含义,怎么生成的呢?”
ViewState的生成算法依赖于各个厂商的特定实现,但最常见的实现方案是:将组件树序列化,再进行 Base64 编码。
由此,引出了“JSF 怀疑论者”对JSF最担心的一个问题:“类似于HelloWorld这样简单的一个JSF页面, 进行一次客户端与服务器端之间的交互竟然要传递这么多的数据, 倘若我的页面稍微复杂一些,光是在客户端和服务器端之间传递ViewState,就会让我的服务器资源消耗的差不多了,如果是这样的话, JSF能逐渐普及并成为工业标准吗? 我能在我的项目中使用JSF吗?”
确实,笔者也承认,JSF对ViewState的维护,肯定会对系统性能造成影响,这也是“JSF 怀疑论者”对JSF 持怀疑与不信任态度的主要原因之一。
“是不是只有MyFaces才会这样呢?每个JSF引擎都会生成这么一大堆 ViewState 吗?”
基本上所有的JSF引擎,都需要维护ViewState,MyFaces 是这样,Sun RI也是如此。
“你们AOM的 ViewState又是怎样的?”
AOM也强不到哪里。上述HelloWorld示例,在AOM 2.0 + Apusic AS 5.1环境中运行时所产生的 页面源码如下:
<html>
<head>
<title>Hello World</title>
</head>
<body>
<form id="form" name="form" method="post" action="/ear2/helloWorld2.faces;jsessionid=wKgGOxroR8-qttDExTRd9UoJipPw6ZjLPq8A" enctype="application/x-www-form-urlencoded">
<input id="form:txt1" type="text" name="form:txt1" value="Kevin"/>
<input id="form:btn1" type="submit" name="form:btn1" value="sayHello"/>Hello Kevin
<input type="hidden" name="form:_postback"/>
<input type="hidden" name="javax.faces.ViewState" value="H4sIAAAAAAAAAIVWTWwbVRB+/smf+VF+lNCC0iZNhWhp1nG
SOhYp0MZNGktOjGI3hQYpebZf4g3r3cfu23iTQJUiQSUqEBVwQAoCCY5FQlTiwgXEAamoSETiwqlCSAgJOHAggQMws17vOo7T7GH27e7M
vJn5vpm3N38nDaZOOuaSy3SFSgpVl6RUdpnlxOhb3z37QatxQvETYnFCSMDQyZCmL0kaZzotUuMFQ1qkOWZIlHNFzlEha6okF7kiZXTG0
kI3c8LU2bU3fMFszxcv+UkwSZpzBVnJ60wVpNPeMmwKWQmf03W6mpQNMZokLTmFGsY0LTJB2ss6GFYYHMrqEig04q7CEKSjysMkNQpTlM
Nnv5x/kVwhQYtDwB12WqghuXtYV7e63/uGvh8gvgQJGvIas/PzlYIgQwYa+23ZhFlXv+WCPIYOLSfxnFbkmgrJSAVRVKRJEAmVmyLDLAG6
QWGJiGt56gDLuFYsUjU/ZgqhqWidFapnfeIA65QpvI2PLs/L+fllg4PsHx6KjURHYiPD0UgkNhiLxIZOW4L0HeBvQtOLGMQi3EG9p776xc
SszEozmoa7HqnaNRqLRKKno9GB4ehQbHggMjIYNTEVvPzuqtlbAVZtHlYOmg1NP371ddfC9wHinyAhRaP5CZoTmp4gLaKgM6OgKXmLP33W
dtFYakZHuEFNvCWWBYpK8dTUM6np8enMfOJ82gW4DHsPPnZCosfrGV6cmUGz2cT4pfmZVCqDykctjreHbOFGbzfQmKYpjKp3evSNHzb/+c
NPfJdJwwpVTCCaz964h3AoWGgyM5WcHzuXTsQFaQ0XmKJolzRdyQ9KUEXw2epVJKnlqMKu7LQtbA78/Rv0UoI0F6BKOS3PkqQpp5mq0Fdt
4kOXYRgmXWLOc9MK1WWqCqcv/oNLEH98GsRaQRCyO/yEKtgS09t/+vDj7avXYn5sEyf8Ski23rRZzDL9tZvvdt/3zt3rlTEBmXEXVl8d0L
uqQMfVI3vw8+Hr7poWtBF6AEUrig6rXP9OV/SiOCbKEVYQtGnMsdi9VVMqbPWXSqV+5Ha/qStMxSrmee3lBhqok0avu3qwjl4zrzKpma/OI
Ptka/aXX7vXL1QK5xNOlb3qAS6P1m+8tKCCTQL9mZ6mK0x/7vatJ9/evDPlJ/7qCergHzJAJ2/bCNJVnpqyFk4zYIUir9GswkYtjow7WTPg
mSIxCEqaxcDGLQ5NZ0ABEzDlX3n+0/VvtycHAqQhSe4HasOhwfKZVQ57tFXN7TgGA3M5xFzzCk0XVWhy2EyQw7aBFWZKeMJUcwhS+RMYBlV
ABwBMQmxhLzbU5VQ3mB4eT06DCp4dQPQ6LmeR/pCk4xJofKhcVrYns1JoY+7ul//eQFSc48NW8zQ+evX19J+Xt87YuJVOkq6+da9zx6DvJR
Uq/3J1p5RPVM5hvrQg6Z7Ag8Hlc13i2pOc2xQ/5gm+R3oOyi/5LiPAFO8nN4BN3nf38hTrqLk0JHXYf8RdNVZxnlfyw6PLbdVKfu3V+TnHX
Dm+s0i9x/eh3hQTBS2/m3v81l87fQuf7wRIYw330N35e5INn8cdVuF6Yo6EgEe0iObwV9E+V4e8HrHQ5AJQ47BLjdoAPwvfvv7z9vCbSBDU
TpQGycN7SGLQ1Ul8U4co5SlqeegIGJWOeg28fD9Ay0Ov0TCzRdme+6l7nr/7TC5cnalM5KeqBpODGt7ipVPk0J70oBamIvbrguM2Sw74S7H
H/G5+JytJ9rpJ2rnx/wH9Bmb8yAoAAA=="/></form>
</body>
</html>
各位看官可能不由自主的正在对 AOM 中的 ViewState 和 MyFaces 中的 ViewState进行对比分析。您别费这个精气神了,我直接给您分析结果: 抛开其它代码,单以 ViewState为例,MyFaces上述示例中的 ViewState大小是2944个字节,而AOM是 1780 个字节。
看来,同样的JSF页面,AOM的 ViewState 比 MyFaces 的 ViewState 要小一些。
您可能要问了:“难道作者举这个示例的目的,是试图来证明AOM对ViewState的实现机制更优雅一些? ”
倘若本文的目的只是进行 ViewState 大小比较的话,那各位看官大人尽可拂袖而去。本文向大家阐述的是:AOM是怎样“干掉” ViewState 的!
对 ViewState 进行了亲密接触后,让我们调整一下注意力,继续刚才的问题。为了方便阐述,我们定义三个名词:
Faces Page:一个基于 JSF 标准的 Web 页面,它可能是一个 JSP View,也可能是一个 Facelets View
JSP View:用 JSP的tag-lib 技术作为组件树描述的载体
Facelets View:用 Facelets 技术作为组件树描述的载体(请参考前文:AOM 2.0的神奇魔力之七:Facelets,AOM更好的外衣)
回顾刚才的问题,您可能会有这样的想法:“ViewState 即然是无法消除的,那么,是否能够对ViewState的维护机制进行优化,让这个 ViewState变小点呢?”
您说的太对了!
我们来分析一下 ViewState 中所包含的内容:
组件树结构信息:这一部分信息,其实在Faces Page中是存在的。
组件树中每个节点的属性信息:这一部分信息我们再分成两部分来看:一部份是简单组件的简单属性,譬如,一个 textField 的 id是什么,它的 value 是什么,这部分信息其实在Faces Page中也是存在的。 另一部分是复杂组件在运行期产生的状态信息,如, 一个DataGrid组件目前选中的是哪一行,一个Tree组件目前的节点展开状态等,这部分信息,是无法从Faces Page中获取的。
那么,我们能不能做到这样:“ViewState中只保留必备的信息(如复杂组件在运行期所产生的状态信息),而其它信息全部从 Faces Page中获取呢?”
答案是:那要看 Faces Page 的支撑技术是什么。
在前一章节中,我们已经介绍过:Facelets是AOM更好的外衣。同样,在对 ViewState 的处理上,Facelets 也较 JSP 更为友好。
为什么这样说?
首先,我们来看什么是 JSP?JSP最终是要被编译成 Servlet的,也就是说:它本质上就是一个类,是一段程序。 因此,如果用 JSP 来构造JSF的组件树,是非常别扭的。因为编译后的JSP程序,不仅要构造组件树,可能还要执行其他的代码,甚至于它本身嵌入了许多和 JSF 无关的 tag,这样我们就无法有效的通过 JSP 重建组件树。再说的通俗一点: JSP能够干的事情太多了,甚至于它有能力更改重建后的 组件树,而作为JSF引擎,我们对它的这种能力是很惧怕的。
换言之,作为JSF引擎,我们无法有效的通过JSP页面获取组件树信息,所以,如果你用 JSP 作为 Face Page 的支持技术,那么JSF引擎必须采用最保守的策略:维护所有的状态信息。
而如果你用 Facelets 作为Faces Page的支撑技术,那情形就不一样了。
首先,我们来做个小实验。将上述的 helloWorld.jsp 改成Facelets 技术:
<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html">
<h:form>
<h:inputText value="#{helloWorldBean.name}" />
<h:commandButton value="sayHello" action="#{helloWorldBean.sayHello}" />
<h:outputText value="#{helloWorldBean.result}" />
</h:form>
</f:view>
我们在 AOM 2.0 上运行此页面,最终生成的HTML源码如下:
<form id="j_id0" name="j_id0" method="post" action="/ear1/helloWorld.faces" enctype="application/x-www-form-urlencoded">
<input type="text" name="j_id0:j_id1" value="Kevin"/>
<input type="submit" name="j_id0:j_id2" value="sayHello"/>
Hello Kevin
<input type="hidden" name="j_id0:_postback"/>
<input type="hidden" name="javax.faces.ViewState" value="H4sIAAAAAAAAAJVWXWwUVRQ+u/1lQUPb0BShpoCiYJkt20KRit
JfdmVbCFurUiPcztx2B2ZnrjN3ulNQAiZKItFo/ElMaiTRR0yMJL74ovHBBINGEl98IsaEmKgPPgj6oJ57d3ZmuzsLYR7uzt57fr/7nXPm0
u/Q5NrQMZs9QRaJYhBzQTk0d4KqfOjN7575cK2zzYgDeAwAGhwb+i17QbEYtUmBOCcdZZ6o1FEIY4auEq5bpqIXmKFM25TmuO2q3LXp+ddj
jXM9X7wYh8YstKp53dBsanJYJ10mXa4byWHbJktZ3eFDWVilGsRxpkiBcmgvyYiwkmhQNxdQoFl45Q6HjgoLaeLkJwnD47iuvQBnoNFjGHC
HTEtIKIEP79y17ve/IR80QCwDjY5+isr8YsVGXBOOUI7LtUVkXbnLODwsDHp+4qpVYJaJySh5XjCUNC4Zk7l8mnocZZtOHNO1nYFq7x1UR6
1CgZjaiMu5ZZbVU4H6tjuoH3J5let+j8OWO2hNWHahLN+H8j3R8k9lZnRaPGJZnDFXRCSeePDWGr6xsrUBgX9bSKsRyzIoMa/22Gd/XP7nj
zjEjkLTIjFchD8ms9wAQjmRnp7MHhsZzmVGObQl89QwrKct29AUT0SNRteGl5q1VGLQM7faji/3/f0bUiwDrXmkgmppNAstquWa3F6SfEDy
iThcskD9/y2LxNaJyX26/IcPh/joFC6n8hxgZfwZk9MFarf/fPHjm+fO74kL9vjxl0OSclNuYY7ar156t3v1O9cvlKuniVUAF4uAsDOEMPA
rU/SZ3dTy01dfdx7/oQHiE5AwLKJNEJVbdgZW8bxNnbxlaB57Yr800VxsFYaEAw5deI2K45ryUg2sHGVy+MjBY5kxDq2Dqja3e7B/MEpq+PD
hbGZ8DKNJ1pzJMh+j88Q1+ERp84FhbANL09ZJaqaXvx8bMt+7uEYkX9wQcYuIQdf51A3X85hIe12wbBRLNy8BWuahJCkT5NhU0WuS3o5isbh
jHg93uLZBTXHpGqt+AlwbIlDfFLzdGyHXKmnZWwNqTGwrAXyplFdhuqqb+m3rk2szN37tPn2gzIcY98kTkgKB3hpdfTlOOE3jBVM7Rxap/ey
Vy/veXr46GYd4Zb/0aZ1wUEaTOhw6Sz1St5I5imQ39FNkzqBDHhPVubXmXik2b7IwIwIb9xjSykGgofTExHXa0FWKESWrxIqJs7PXv/z3rbg
U6wjEQomPXnkt9+fRa49JEND/9qpxgsIUQak2nEGyvfzcp6e/vZnua4CmLKzBloEjimrTSwxzbKuYEqMCDJwCCRqol6t/3sQyQmcc1ksFL0mN
5IRrqoJMpSNUbDSRRUi0LMaWDGMTsozYDrWT49kpFBGTCvtHhMkZ0VUQZN+kJ/xPFLdD55bTYRmMYCtUTLy0lyp7R2n0MuYV98DumqLp2d/fO
/Boj6TNvs3R1jZjD18tm/leOX2E83vqFpacFkyWYHe4sJo1NFDaZCuUkEvi98GzSJLwPHhCwQixgP4QUZ33B2/Nd1uTO7EZrkAiJc7XViLRXo
mEP3VLmYwIcj5UpzgmKc9bWr3qWB/Qvlrus+SVC7/cHHhDkF84OYA+HqlTANW6ogLY5b9ubTn++a0GaK6qAGHtydtSXvw/6HNbvGdnIYFsJgW
hjl9S7bMRJRTSW6hMyqCfL6bgvhrmOWQpLXZWcLnU+AQ0gs/7YCiCzwO9g7t6iCzACEIHZjd7IYfwhsv7VSRk9WhXGh3NjjtX0OWwz932E+au
+//OPq/K4ONlof0V7d1nlvgZl2um2AtdNVnjFeJQrdMW9sKeCBh39e5K1W0Lvr2qxtAvAuhYWdeHy7BtDGCTaLH/AUXoX/YmDAAA"/>
</form>
可以看到, ViewStae 依然存在(为了多赚点稿费,我忍不住把 ViewState又 贴了一遍)。
现在,让我们在页面中加个参数:transient="true"
<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html">
<h:formtransient="true">
<h:inputText value="#{helloWorldBean.name}" />
<h:commandButton value="sayHello" action="#{helloWorldBean.sayHello}" />
<h:outputText value="#{helloWorldBean.result}" />
</h:form>
</f:view>
然后,重新运行此页面,其生成的 HTML源码 如下:
<form id="j_id0" name="j_id0" method="post" action="/ear1/helloWorld.faces" enctype="application/x-www-form-urlencoded">
<input type="text" name="j_id0:j_id1" value="Kevin"/>
<input type="submit" name="j_id0:j_id2" value="sayHello"/>
Hello Kevin
<input type="hidden" name="j_id0:_postback"/>
<input type="hidden" name="javax.faces.ViewId" value="/helloWorld.xhtml"/>
</form>
哇,ViewState 不见了。
“没有ViewState,该页面还能正常运行吗?”
当然可以!
“那么,你们是怎么做到的呢?”
为了不使问题复杂化,我们先把注意力限制在这样一个问题:假设一棵JSF组件树的所有信息,都已经位于一个基于Facelets View的xhtml页面中,AOM是怎样在没有 ViewState 的情况下恢复这棵组件树的?
回顾JSF的生命周期,有六个阶段:Restore View、Apply Request Values、Process Validations、Update Model Values、Invoke Application、Render Response。在 AOM 2.0 中,我们加入了一个隐含的生命周期阶段,即“BuildTree”。BuildTree位于整个生命周期的开始阶段,于是,在 AOM 中,JSF 生命周期就变成了7个。在BuildTree阶段做的事情,就是在 Restore View之前先把组件树构造出来。具体的做法就是: 如果一个 <h:form>的 transient 属性设为 true(默认为false),AOM就会从 xhtml 页面中恢复组件树,而不再是通过 ViewState 进行。(注:AOM开发团队已经将此建议提交给 JSF 2.0,即 JSR 314)。
“就这么简单?”
是,实现起来很复杂,但说起来就这么简单,如果你有兴趣,可以研究一下 org.operamasks.faces.facelets.FaceletViewHandlerHook.java 的源码。
真正复杂的事情在后面。
“复杂组件如何处理?譬如,页面有一个 DataGrid,它当前选中了哪一行,再如一个Tree组件,它当前节点的展开状态,诸如此类的信息,在 xhtml 页面中根本就不存在也无法定义,这种情况 AOM 如何处理?要知道,是否能够对 ViewState 进行优化,必须要在保证程序运行无误的前提下进行的!”
这就是 StateAware 接口的引入。
AOM 2.0中引入了一个新的接口:org.operamasks.faces.application.StateAware,如下:
/**
* The addon interface to UIComponent that support optimized state holding.
*/
public interface StateAware {
/**
* Save the optimized state.
*/
public Object saveOptimizedState(FacesContext context);
/**
* Restore the optimized state.
*/
public void restoreOptimizedState(FacesContext context, Object state);
}
复杂组件(如DataGrid、Tree等)如果需要保存自己的特有状态信息,必须要实现这个接口,自行对状态进行处理。
还是做个简单的例子:假设我们有一张表格(DataGrid),表格中列出公司的所有员工,我们希望当用户选中表格中不同的员工时, 页面能够显示您选择的员工姓名。借这个例子,来验证 AOM 对复杂组件的状态处理。
dataGrid.xhtml页面代码如下:
<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
xmlns:w="http://www.apusic.com/jsf/widget" xmlns:layout="http://www.apusic.com/jsf/layout"
renderKitId="AJAX" xmlns:h="http://java.sun.com/jsf/html">
<w:page title="datagrid demo">
<w:formtransient="true">
<w:dataGrid id="grid" paged="true" rows="10" toolBarPosition="top" style="height: 121px;">
<w:outputColumn header="name" id="name"></w:outputColumn>
<w:outputColumn header="email" id="email" width="300"></w:outputColumn>
</w:dataGrid>
<layout:panelGrid columns="2">
<w:button id="btn" value="Select" />
<h:outputText id="txt"></h:outputText>
</layout:panelGrid>
</w:form>
</w:page>
</f:view>
请注意:我们是把 <w:form>声明成 transient=true的,按照前文的介绍,将其设置成true就意味着告诉AOM:请不要保存 ViewState。(同样,你也可以在 web.xml 中配置参数从而让全局的form都为transient=true)。
这里顺带着把 DataGrid 的 IoVC 支持介绍一下。
在页面中,我们声明了一个<w:dataGrid>,并且,设置它的id为“grid”。在 grid 里面有两个 <w:outputColumn>, 它们的id分别是 name 和 email。至此,一个 DataGird 组件就设置完毕了。下面是 DataGridBean代码:
@ManagedBean(name="DataGridBean", scope=ManagedBeanScope.SESSION)
public class DataGridBean {
@Bind(id="grid", attribute="binding")
private UIDataGrid grid;
@Bind
private String txt;
@DataModel(id="grid")
private List<Employee> getEmployees(){
List<Employee> emps = new ArrayList<Employee>();
emps.add(new Employee("Kevin", "abc@apusic.com"));
emps.add(new Employee("Mary", "def@apusic.com"));
return emps;
}
@Action(id="btn")
public void btnClicked(){
if(grid.getSelectedRow() < 0) {
grid.setSelectedRow(0);
}
Employee emp = (Employee) grid.getSelectedRowData();
txt = " You selected '" + emp.getName() + "'";
}
}
如何让页面中的 DataGrid正常工作呢?首先需要声明一个 UIDataGird 对象,并用 @Bind 标注进行声明,另外再声明一个 @DataModel,在DataModel里准备数据,至此,DataGrid就可以工作了。另外需要注意的是:DataGrid用到了一个值对象 Employee,代码如下:
public class Employee {
private String name;
private String email;
public Employee(String name, String email) {
super();
this.name = name;
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
Employee的属性名称要和DataGird中列的id保持一致(不一致怎么办?用@Bind指定其id即可)。至此,整个示列程序已经准备妥当, 让我们运行此页面,程序正常运行,如下图所示。

“那么,这种情况下,AOM对 ViewState 是如何处理的呢?”
让我们观察一下生成的 HTML 源码(再次声明,为了不影响各位读者的视线,我对生成的HTML代码进行了删减)。
<html lang="zh">
<body id="j_id0" class="skin-default">
<form id="j_id1" name="j_id1" method="post" action="/ear1/dataGrid.faces" enctype="application/x-www-form-urlencoded">
<div id="j_id1_grid" style="height: 121px;"></div>
<table id="j_id1:j_id2">
<tbody>
<tr><td><span id="j_id1:btn"></span></td>
<td><span id="j_id1:txt"> You selected 'Kevin'</span></td>
</tr>
</tbody>
</table>
<script type="text/javascript">
OM.ajax.actionId='/ear1/dataGrid.faces';
OM.ajax.viewId='/dataGrid.xhtml';
OM.ajax.viewState='H4sIAAAAAAAAAFvzloG1tIhBONonK7EsUS8nMS9dzz8pKzW5xHrCuYj5AsWaOUwMDBUFDAwMzAXFRQyCYGWlJZk5
eh6JxRm+iQWs7LcOHhZLuMjMwOTGwJWTn5jilphckl/kycBZklGUWpyRn5NSUWDvwAACPOUcQFIAiBlLGLiy4jNTDOPTizJTSgsZ6sAK2OB2
gJ3imVeSmp5aJPRowZLvje0WTAyMngysZYk5pakVRQwCCHV+pblJqUVta6bKck950A1zMQOqYU75+TmpiXlnFYoars759Q5oWBTMsAJGkAM4
ikEkJwOCzQY0hKuiAACiXu4vJgEAAA==';
</script>
</body></html>
哦,ViewState又出来了,而这部分信息,只包含“DataGird运行期产生的动态信息”,这部分信息是无法消除的,消除这部分信息将使程序无法正常工作。
至此,我们恍然大悟:在进行 Restore View 时,凡是 facelets页面中能够保存的信息,AOM就用页面中的信息;如果组件需要保存一些特有信息,那么,组件就自行负责维护 ViewState 的存储与恢复。
困扰 JSF 普及的一大难题“状态维护”,在 AOM 中不复存在!
“等等,话别说的太满了。有这样一种场景:某个页面,一开始只有几个组件,随着用户的操作,页面上的组件可能会增加, 也可能会减少,新加的这些组件,无疑在 xhtml 文件中并不存在,那么这些组件的 ViewState 如何维护?”
最简单的方案是:把这个页面的form的transient属性设为false,这样就保存ViewState了,新增或者删除组件就不会出现问题。再或者你把新增的组件放到 session 里,每次请求就将组件重新生成一遍。
“但这个方案有点丑陋哦,因为这种做法实际上是把判断页面是 Stateless 还是Stateful 的责任交给用户了,普通用户哪里知道这些知识?有没有更好的方案?”
AOM 2.0中引入一个新的tag:<om:stateAware>(注意:此 tag 仅适用于 Facelets 技术下的 Faces Page)。<om:stateAware>本身也是一个组件,如果你要动态增加或删减组件,只需把你要增加的组件放到这个tag里即可。
再做一个简单的例子:

如上图所示:我们希望,当用户点击“Add”按钮时,能够自动增加一个 TextField,而当点击“Calc”按时时,把所有动态生成的TextField的值累加起来。页面 dynamicPage.xhtml 如下:
<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
xmlns:w="http://www.apusic.com/jsf/widget" xmlns:layout="http://www.apusic.com/jsf/layout"
renderKitId="AJAX" xmlns:ajax="http://www.apusic.com/jsf/ajax"
xmlns:om="http://www.apusic.com/jsf/misc" xmlns:h="http://java.sun.com/jsf/html">
<w:page title="dynamic page">
<w:formtransient="true">
<om:stateAware deep="true">
<ajax:updater binding="#{DynamicPageBean.updater}">
<layout:panelGrid binding="#{DynamicPageBean.panelGrid}" columns="1" border="1">
</layout:panelGrid>
</ajax:updater>
</om:stateAware>
<layout:panelGrid columns="3">
<w:button id="add" />
<w:button id="calc" />
<h:outputText id="result" />
</layout:panelGrid>
</w:form>
</w:page>
</f:view>
后台的 DynamicPageBean 如下:
ManagedBean(name="DynamicPageBean",scope=ManagedBeanScope.REQUEST)
public class DynamicPageBean {
@ManagedProperty
private AjaxUpdater updater;
@ManagedProperty
private HtmlPanelGrid panelGrid;
@Bind
private int result=0;
@Action(id="add")
public void add(){
UITextField field = new UITextField(panelGrid);
field.setValue(new Random().nextInt(1000));
updater.reload();
}
@Action(id="calc")
public void calc(){
result = 0;
List<UIComponent> components = panelGrid.getChildren();
for(UIComponent component : components) {
if(component instanceof UITextField) {
int value = Integer.parseInt(((UITextField) component).getValue().toString());
result += value;
}
}
}
}
请注意观察:我们将新生成的组件,包在一个<om:stateAware>的tag中,并且,每次交互,让 <ajax:updater>刷新一下。 另外需要注意的是:我们将后台 DynamicPageBean 的生命周期声明为 request,这就意味着每次交互,这个 Bean 都是一个新的实例(Bean自身不会保存状态)。在这种情况下,如果程序能够正常运行,这就说明:新生成的组件的相关信息, 已经被保存在 ViewState 中了。
-
注1:上述示例需在AOM 2.0 M2中才能成功运行
-
注2:关于对组件的服务器端API(即如何动态增减组件)的介绍,不是本文重点,另有文章介绍。
另外需要提及的是:有了<om:stateAware>标签,对引入的第三方组件的状态维护,也就有了解决方案:那就是把第三方组件包在<om:stateAware>之内。
本文对JSF的ViewState做了一些阐述。长久以来,“状态的维护”是困扰 JSF 获得普及的一个很重要的因素。很多朋友一看到 JSF 页面中生成的那一大堆“ViewState”,就会不由的对其“毛骨悚然”:这样一种“重量”级的技术,能够被用到真正的项目中吗?
在 AOM 2.0中,对 JSF 的状态维护进行了彻底的改造。 AOM “干掉”了 ViewState,更确切的说法是: AOM 对 ViewState 的处理要优雅的多,消除了无用的、重复的状态信息。此外,AOM开发团队已经把对状态的处理机制提交给 JCP,并希望通过我们的努力,使 JSF 能够获得一个更广泛的认可。
笔者无意去做什么“JSF的鼓吹者”,如果本文能够给“JSF 怀疑论者”带来一点启示,则已经是笔者最大的欣慰了。
在下一篇文章中,我们将介绍 AOM 2.0 中最令人激动的特性,一种全新的编程语言:ELite!
感觉AOM还是把事情搞复杂了
既然客户端使用了Ext,那么把浏览器看作一个完整的应用,在客户端保存状态信息,服务端只保存基本的会话状态(如授权信息)即可。完全可以不要用JSF这么重量的技术了,也不会有viewState之类的烦恼。
“感觉AOM还是把事情搞复杂了”,是这样的吗?
你完全可以用ext-js,但你愿意直接赤裸裸的用ext-js吗? 你愿意直接面对ext-js的那一堆javascript吗? 先不论aom是否遵循jsf,至少aom将ext-js包了一层,让你很轻松的使用ext-js。 难道这对用户来说不是一种进步?
优化的力度不够,也不稳定
在我的测试页面中设置transient="true"后,出现js异常,按钮事件无法正常执行。
同时使用HttpWatch监测了一下,发现效果不明显,没有优化时是4KB,优化后是3KB。
其实有一些Action根本就不需要取数据的,也不需要状态维护,能不能让这种Action直接在Browser执行? 如点击“新增”打开一个表单,其实不用维护任何状态,可以预先生成表单也可以动态加载表单的内容。
OperaMask Demos 优化了吗?
在OperaMask Demos的在线演示中,点击左边的菜单树,不管是展开还是收缩,每次都会发送接收大量的数据。 在IE6: send 9151 received 8727
在FireFox下是36KB?怎么可能是36KB? 整个菜单的数据?
<input type="hidden" name="javax.faces.ViewState" id="javax.faces.ViewState"
@DataModel(id="grid")
updater.reload();
JSF标准的实用性
这个问题很重要,JSF规范一开始就完全背离现实实用性,变成了SUN学究派的理论产物,如果不是其比较灵活的扩展性,恐怕早就被历史遗忘,但是依然有人肯原谅SUN的错误,并且坚守立场,帮助推动JSF的发展,难能可贵