(续
JSF 2.0阅读笔记:视图状态 (二))
在StateHelper的javadoc上明确声明:
引用
Define a
Map-like contract that makes it easier for components to implement
PartialStateHolder. Each UIComponent in the view will return an implementation of this interface from its
UIComponent.getStateHelper() method.
可以看出,这是一个“Map-like”的通过key-value方式访问的接口,它不仅仅是组件属性的底层储存结构,还为组件的增量状态(PartialState)提供支持。组件上提供getStateHelper方法,用于返回本组件持有的StateHelper实例。我们观察任意一个API基础组件,例如UIInput,可以发现其属性(例如说,immediate属性)是这样实现的:
public boolean isImmediate() {
return (Boolean) getStateHelper().eval(PropertyKeys.immediate, false);
}
public void setImmediate(boolean immediate) {
getStateHelper().put(PropertyKeys.immediate, immediate);
}
作为比较,不妨回头来看看JSF1.2 API中的UIInput:
/**
* <p>The immediate flag.</p>
*/
private boolean immediate = false;
private boolean immediateSet = false;
public boolean isImmediate() {
if (this.immediateSet) {
return (this.immediate);
}
ValueExpression ve = getValueExpression("immediate");
if (ve != null) {
try {
return (Boolean.TRUE.equals(ve.getValue(getFacesContext().getELContext())));
}
catch (ELException e) {
throw new FacesException(e);
}
} else {
return (this.immediate);
}
}
public void setImmediate(boolean immediate) {
// if the immediate value is changing.
if (immediate != this.immediate) {
this.immediate = immediate;
}
this.immediateSet = true;
}
可以看出使用强类型对象域(JSF1.2)与弱类型属性集合(JSF2.0)作为底层储存结构所带来的明显区别。同样,在JSF2.0中,具体组件的saveState/restoreState方法实现也变得相对简单,以UIInput为例:
private Object[] values;
public Object saveState(FacesContext context) {
if (context == null) {
throw new NullPointerException();
}
if (values == null) {
values = new Object[4];
}
values[0] = super.saveState(context); //将间接调用到UIComponentBase.saveState方法
values[1] = emptyStringIsNull;
values[2] = validateEmptyFields;
values[3] = ((validators != null) ? validators.saveState(context) : null);
return (values);
}
可以看出,现在具体组件的saveState/restoreState方法中,只需要负责保存一些内部对象域的状态。而数目众多的组件属性,则无须在这里显式保存。如果具体组件中没有内部对象域需要保存,甚至可以不覆盖saveState/restoreState方法,例如UICommand组件。因为针对StateHelper的save/restore逻辑,写在了API组件的公共基类UIComponentBase(自定义组件亦可从这里开始继承)中。反观JSF1.2中的UIInput:
public Object saveState(FacesContext context) {
if (values == null) {
values = new Object[14];
}
values[0] = super.saveState(context);
values[1] = localValueSet ? Boolean.TRUE : Boolean.FALSE;
values[2] = required ? Boolean.TRUE : Boolean.FALSE;
values[3] = requiredSet ? Boolean.TRUE : Boolean.FALSE;
values[4] = requiredMessage;
values[5] = requiredMessageSet ? Boolean.TRUE : Boolean.FALSE;
values[6] = converterMessage;
values[7] = converterMessageSet ? Boolean.TRUE : Boolean.FALSE;
values[8] = validatorMessage;
values[9] = validatorMessageSet ? Boolean.TRUE : Boolean.FALSE;
values[10] = this.valid ? Boolean.TRUE : Boolean.FALSE;
values[11] = immediate ? Boolean.TRUE : Boolean.FALSE;
values[12] = immediateSet ? Boolean.TRUE : Boolean.FALSE;
values[13] = saveAttachedState(context, validators);
return (values);
}
高下立见。
4.2 增量视图状态
JSF2.0中的增量视图状态处理的核心,是由StateManagementStrategy的实现类、PartialStateHolder接口、StateHelper的实现类三者配合组成的。
首先,在StateManagementStrategy的接口javadoc上,明确规定了增量式收集与恢复视图状态的总体思路:
saveState方法:
引用
public abstract Object saveView(FacesContext context)
Return the state of the current view in an Object that implements Serializable. The default implementation must perform the following algorithm or its semantic equivalent.
1. If the UIViewRoot of the current view is marked transient, return null immediately.
2. Traverse the view and verify that each of the client ids are unique. Throw IllegalStateException if more than one client id are the same.
3. Visit the tree using UIComponent.visitTree(javax.faces.component.visit.VisitContext, javax.faces.component.visit.VisitCallback). For each node, call StateHolder.saveState(javax.faces.context.FacesContext),
saving the returned Object in a way such that it can be restored given only its client id. Special care must be taken to
handle the case of components that were added or deleted programmatically during this lifecycle traversal, rather than by the VDL.
The implementation must ensure that the StateHolder.saveState(javax.faces.context.FacesContext) method is called for each node in the tree.
The data structure used to save the state obtained by executing the above algorithm must be Serializable, and all of the elements within the data structure must also be Serializable.
Parameters:
context - the FacesContext for this request.
Since:
2.0
restoreState方法:
引用
public abstract UIViewRoot restoreView(FacesContext context,
String viewId,
String renderKitId)
Restore the state of the view with information in the request. The default implementation must perform the following algorithm or its semantic equivalent.
1.
Build the view from the markup. For all components in the view that do not have an explicitly assigned id in the markup,
the values of those ids must be the same as on an initial request for this view. This view will not contain any components programmatically added during the previous lifecycle run, and it will contain components that were programmatically deleted on the previous lifecycle run. Both of these cases must be handled.
2. Call ResponseStateManager.getState(javax.faces.context.FacesContext, java.lang.String) to obtain the data structure returned from the previous call to saveView(javax.faces.context.FacesContext).
3. Visit the tree using UIComponent.visitTree(javax.faces.component.visit.VisitContext, javax.faces.component.visit.VisitCallback).
For each node, call StateHolder.restoreState(javax.faces.context.FacesContext, java.lang.Object), passing the state saved corresponding to the current client id.
4. Ensure that any programmatically deleted components are removed.
5. Ensure any programmatically added components are added.
The implementation must ensure that the StateHolder.restoreState(javax.faces.context.FacesContext, java.lang.Object) method is called for each node in the tree, except for those that were programmatically deleted on the previous run through the lifecycle.
Parameters:
context - the FacesContext for this request
viewId - the view identifier for which the state should be restored
renderKitId - the render kit id for this state.
Since:
2.0
简而言之,API规定了,在收集视图状态时,不管具体算法如何,但必须保证以下4点:1.调用组件上的saveState方法获取组件状态;2.视图状态的组织形式必须保证通过组件的clientId能唯一查找到该组件的状态信息;3.对于运行时动态添加和删除的组件(修改了组件树结构),要在视图状态中记录下来;4.最终形成的视图状态数据结构必须可序列化。
在恢复视图状态时,首先要通过原始页面(markup)构建出原始状态的组件树,特别地,组件树的结构和组件id必须与首次请求所创建的组件树完全一致。然后,遍历组件树,通过组件的clientId在视图状态中提取出该组件的状态信息,并调用组件上的restoreState方法恢复组件状态。再然后,删去在视图状态中记录的被动态删除了的组件。最后,添加在视图状态中记录的被动态添加的组件。
这样,StateManagementStrategy定义了在组件层面的增量视图状态处理方案。首先,它保证了组件状态的相互独立性,如果一个组件相对于原始状态没有变更,只要它的clientId不出现在视图状态中即可。其次,它保证了组件树结构的动态改动被独立记录和恢复。
而组件属性层面的增量状态,则是由PartialStateHolder与StateHelper共同提供的。组件的公共基类UIComponent实现了PartialStateHolder接口,该接口为组件提供了以下接口方法:
void markInitialState();
boolean initialStateMarked();
void clearInitialState();
它的设计思路是,实现这个接口的组件被分为两个状态,一个叫初始态(InitialState),另一个规范中没命名,不妨称之为增量态。JSF引擎应该保证,在根据视图声明语言创建组件实例、注入声明语言中定义的初始属性值时,组件处于初始态。在这之后,组件属性可能被动态改动之前,JSF引擎调用组件上的markInitialState方法,使组件进入增量态。处于增量态的组件,将记录自身属性的变动,从而允许在收集增量视图状态时,只包含差异属性。在RI中,大部分组件的markInitialState方法在Facelets解析过程中,组件被加入组件树并注入属性值后立刻调用:
package com.sun.faces.facelets.tag.jsf;
public class ComponentTagHandlerDelegateImpl extends TagHandlerDelegate {
...
public void apply(FaceletContext ctx, UIComponent parent) throws IOException {
...
c = this.createComponent(ctx);
...
// add to the tree afterwards
// this allows children to determine if it's
// been part of the tree or not yet
addComponentToView(ctx, parent, c, componentFound);
popComponentFromEL(ctx, c, ccStackManager, compcompPushed);
if (shouldMarkInitialState(ctx.getFacesContext())) {
c.markInitialState();
}
}
...
}
当组件进入增量态后,记录差异属性的责任就落在属性值的储存结构 —— StateHelper —— 头上了。这从StateHelper的默认实现类ComponentStateHelper可以清楚地看出:
class ComponentStateHelper implements StateHelper {
...
private UIComponent component;
private Map<Serializable, Object> deltaMap;
private Map<Serializable, Object> defaultMap;
...
public Object put(Serializable key, Object value) {
if(component.initialStateMarked() || value instanceof PartialStateHolder) {
Object retVal = deltaMap.put(key, value);
if(retVal==null) {
return defaultMap.put(key,value);
}
else {
defaultMap.put(key,value);
return retVal;
}
}
else {
return defaultMap.put(key,value);
}
}
...
public Object saveState(FacesContext context) {
if (context == null) {
throw new NullPointerException();
}
if(component.initialStateMarked()) {
return saveMap(context, deltaMap);
}
else {
return saveMap(context, defaultMap);
}
}
ComponentStateHelper持有两个Map,defaultMap与deltaMap。从put方法的代码可以看出,无论任何时候,新值都会被保存到defaultMap中。但当所属组件处于增量态时,新值将被同时保存到deltaMap中。在收集视图状态时,saveState方法被调用,如果组件处于增量态,则只返回deltaMap中的差异属性状态。如果对于个别组件需要保存完全状态,只需要在进入收集视图状态之前先调用组件上的clearInitialState(),让组件回到初始态,则返回defaultMap中的完全组件状态。
这种通过put方法监视组件属性变化的方式有一个比较大的缺陷,就是无法监视属性值内部的变化。最常见的场景是,如果属性值是一个集合类型,对集合元素的改动是不会作为增量变动被记录下来的。为此,JSF2.0提供了一个折中方案,在StateHelper接口上针对Map和List提供了专门的方法
Object put(Serializable key, String mapKey, Object value);
void add(Serializable key, Object value);
Object remove(Serializable key, Object valueOrKey);
其中,三参数的put方法针对类型为Map的属性,调用该方法将获取到一个名称为key,类型是Map<String, Object>的属性,如果没有该属性则创建之。并在其中加入键值为mapKey,值为value的元素。同理,双参数的add与remove方法则针对类型为List的属性。以上三个方法均应保证对增量视图状态的处理。当然,这只是个解决大部分场景的不完全解决方案,比如说我要有个类型是Set的属性,就没招了,只能扩展StateHelper接口来实现。感觉上API上带了这三个方法主要还是起到示范作用。