JSF 2.0阅读笔记:视图状态 (一)
最近终于抽空阅读了JSR314与相关的API代码,把一些心得整理一下。
JSF 2.0阅读笔记:视图状态
什么是视图状态在JSF中,一个页面,或者叫视图(View),在服务器端是通过一棵组件树来描述的。组件树中的每一个节点,对应着视图上的每一个组件,是组件在服务器端的内部表述模型,称为组件类实例。JSF请求处理生命周期中的所有行为,包括校验、转换、事件分发、渲染等等,都是围绕组件树进行的。任何时候,只要把组件树完整地建立起来,我们就拥有了它所表述的视图在请求处理生命周期中所需要的全部信息。这些信息,就称为视图状态信息。通常来说,视图状态信息包括了组件树的结构信息,组件中每一个组件的属性值,以及所有附加到这些组件上的其他对象(例如转换器和校验器等等)。而我们编写页面(xhtml或jsp)的行为,事实上是在定义组件树的初始状态。在JSF1.2规范中,官方只规定了一种编写页面的方式,jsp。在JSF2.0中,对定义页面初始状态的方式进行了抽象,支持多种页面定义方式,每一种方式称为一种视图声明语言(ViewDeclarationLanguage,或称VDL)。
在对页面的首次请求中,JSF引擎根据VDL的描述建立起初始状态的组件树。在后续的处理与postback处理中,应用系统会对视图状态进行修改。例如修改组件属性值,动态增删组件等等。由于http协议是无状态的,每次请求完毕后与这次请求相关的状态都会丢失。JSF引擎就承担起在请求处理生命周期的起始阶段把上一次请求的组件树恢复重建起来的职责。显然,实现这一目的最直接的做法是把整棵组件树保存在session中。但是,出于性能与服务器资源占用的综合考虑,JSF的实现框架通常会把恢复组件树的必要信息进行抽取和编码,以某种形式保存在session中或传递到客户端。在JSF规范中,就把这些信息称为视图状态。请注意“视图状态”这个术语的多重含义,在请求处理的语境中,它指组件树的当前状态。在恢复视图的语境中,它指“以某种形式传递到下一次请求,JSF引擎能根据它来重建组件树的必要信息”。至于这些“必要信息”保存的形式和传递的方式,根据具体实现不同而不同,可以是保存在session中组件树,也可以是经过序列化和BASE64编码的一串字符串。为了避免混淆,在下文中“视图状态”统一采用后一种含义。对于前一种含义,采用“组件树当前状态”来表述。
JSF规范和API把生产视图状态的过程划分为采集与编码两个阶段。采集的逻辑通过继承javax.faces.application.StateManager抽象类来实现,负责遍历组件树,把组件树结构(包括组件对象的类型信息,和组件对象之间的层次关系)、每个组件的状态信息和附加对象信息抽取出来,构造出一个可序列化的对象数组。编码阶段(针对把视图状态传递到客户端的情况),通过继承javax.faces.render.ResponseStateManager抽象类来实现,负责把上述的对象数组转换为某种可在网络上传输的(BASE64编码),安全的(加密)编码形式,并向响应流中输出。同理,在根据视图状态恢复组件树的过程中,也存在解码和恢复两个阶段,同样由上述两个抽象类的实现类来负责。当然,其中的实现细节都是JSF框架的实现者才需要关心的事,组件开发人员和应用开发人员都无需关心。
JSF 2.0之前的视图状态在JSF 1.2或更早版本中,视图状态一直是一个尾大不掉的难题。
首先,组件类上存在大量的强类型的属性对象域,使得单个组件的视图状态尺寸虚高。比如说,组件有一个布尔型的属性叫immediate,那么组件开发者就会在这个组件类中加入一个private boolean immediate对象域。同时,由于要跟踪这个属性是否被显式地设置过,还要加入一个private boolean isImmediateSet对象域来记录。这样一来,即使在某个页面中我可能从来都没使用过这个属性,但这些对象域的值最终都会被加入到视图状态中去。可想而知,对于一个复杂的组件体系模型,单个组件上的属性数目很可能达到上百个,而事实上每次使用的只不过是其中三五个,将会使整个视图状态尺寸无谓地增大。需要注意的是,JSF规范中并没有规定要使用强类型对象域的方式来保存组件属性,但JSF1.2的官方API中采用了这种做法,导致组件开发者不得不效仿。
其次,组件开发者必须为每个组件类实现saveState与restoreState方法,来采集与恢复强类型对象域的状态。组件类上存在的大量强类型对象域,有的甚至没有作为JavaBean属性对外公开,是无法统一采集与恢复的。因此JSF规范为组件类准备了saveState与restoreState接口方法,组件开发者必须在每个组件类上覆盖这两个方法,依次采集与恢复各个需要保存状态的对象域。这为组件开发者带来了大量琐碎的重复工作。一些JSF实现采用了自动生成代码的方法来避开这个问题,但在开发组件的过程中需要人手去触发代码生成过程,不免令人不爽。
第三,视图状态尺寸极易失控,使应用开发者陷入两难处境。由于上述第一点原因,结构稍为复杂一点的页面往往会产生尺寸巨大的视图状态信息。更令人崩溃的是,JSF规范针对这种状况给出了一种火上浇油,掩耳盗铃式的“解决方案”:您可以选择把视图状态保存在服务器端,用内存换带宽;也可以选择把它传递到客户端,在下一次请求再提交上来,用带宽换内存。这下好了,应用程序员都变成了揭不开锅的小媳妇,小心翼翼地算着手上有几角几分,到底要带宽呢,还是要内存。要带宽,就意味着服务器资源吃紧,强并发用户数降低。要内存,就意味着明确表示56k modem与3G不得入内,也意味着需要尽量减少ajax提交频率。手心是肉,手背也是肉,你要割哪面?我曾尝试过加入gzip压缩方案来减少视图状态尺寸,但发现效果并不理想。虽然确实能把视图状态尺寸缩小到60%左右,但又把CPU扯进来了,无非是把两难选择变成了三难选择。治标不如治本,归根结底,从本质上减少视图状态尺寸才是正路。
第四,视图状态必须存在。事实上,并不是所有页面都需要更改视图状态,那么对于那些一直保持初始状态的视图,是否可以不保存视图状态,而改为在每次请求处理从原始页面重新构建组件树呢?答案是不行!JSF1.2规范只支持JSP这一种页面语言,而JSP本身是无比开放的,应用开发者倾向于在JSP中直接嵌入业务脚本,并且预期这些脚本只会在首次请求时才运行。在每次postback(特别是ajax postback)中都执行这些脚本显然不是应用开发者想要的。而JSF作为一个开发框架,与其禁止程序员在JSP中嵌入业务逻辑,造成概念上的混乱,还不如接受性能“稍慢”的现状。Facelets框架的出现改变了这种状况,为基于初始视图状态的“优化视图状态”提供了理论上的可能。Facelets页面只关心组件树的初始状态描述,无法嵌入业务逻辑(时髦的说法是没有side effect)。JSF的实现者可以在任何有需要的时候重新解析页面,获取到一份初始视图状态。
(待续)