将 PhaseListener 方法用于 JavaServer Faces 技术及 AJAX

作者:Mark Basler
状态:在 Early Access 版本阶段

问题描述

Java Enterprise Edition 5 平台包括 JavaServer Faces (JSF) 技术,该技术提供了成熟的可扩展的用户界面组件模型。通过使用此模型的设计,应用程序开发者可通过扩展 JSF 中包含的标准组件来方便地创建定制组件,并在应用程序中重用这些组件。结合使用 AJAX 和 JSF 的策略有很多种。本文主要侧重于如何使用 PhaseListener 方法在 JSF 应用程序中加入 AJAX 功能。有关访问静态和动态资源的各种方法的详细信息,请参见“访问静态和动态资源”。有关其他以及相关的解决方案,请参见“结合使用 JSF 和 AJAX”。

解决方案

如果开发者要在其 JSF 组件中加入 AJAX 支持,他们可以从多个策略中进行选择。一种解决此问题的方法是,使用 PhaseListener 截取并实现定制组件的 AJAX 请求,并且(可选)提供组件资源。

我们将讨论使用 PhaseListener 将 AJAX 支持集成到 JSF 应用程序的策略。这需要创建一个定制组件以生成在客户端上执行 AJAX 交互所需的 Javascript,并且需要使用 PhaseListener 形式的另一个服务器端组件来处理 AJAX 请求。

这种方法可用于以下情况:

这种方法自身也存在一些问题,如下所示:

由于这些原因,我们目前建议的方法是使用第三方库(“访问静态和动态资源”文章中详细介绍了这种方法)。我们仍在研究一些新出现的技术,当我们的建议发生变化时,将会对这些文章进行更新。

AJAX 请求流


图 1. 提交 AJAX 请求时执行的步骤


1) mouseover 事件处理程序触发用于填充和发送 XMLHttpRequest 的 Javascript 函数。URL 包含上下文根目录 (bp-jsf-example)、FacesServlet 映射 (faces)、用于通知 CompBPhaseListener 处理请求的名称空间敏感标志 (bpui_compb_action)、在 faces-config.xml 中注册的受管 Bean 名称 (CompBBean)、要调用的受管 Bean 的方法 (processMethod) 以及填充的与请求关联的查询字符串。例如:

    /bp-jsf-example/faces/bpui_compb_action/CompBBean/processMethod?itemId=test1A

2) 根据请求调用 CompBPhaseListener(已经在 faces-config.xml 文件中进行了注册),然后在根目录 ID(URL 中 "/faces" Servlet 映射后面的部分)中搜索标志 "bpui_compb_action"。如果找到了该标志,则会将根目录 ID 解析为其管理 Bean 名称和方法名称,并将其修改为 javax.el.MethodExpression 形式,然后调用受管 Bean 的方法。例如:

    #{CompBBean.processMethod}

3) 由于受管 Bean 在 faces-config.xml 中设置了一个受管属性 (itemId),因此,将在来自请求的管理 Bean 实例中自动设置它的值。然后,管理 Bean 从 FacesContext 中检索 HttpServletResponse,并为与请求一起发送来的 itemId 构造相应的响应。

4) 将 XML 响应发回到浏览器,并调用回调函数。

5) 分析该响应,填充弹出式组件的值并显示弹出式组件。


创建定制 AJAX 组件

要开始设计在应用程序中使用的定制组件,您应该了解一下 JavaEE 5 教程中的“Creating Custom UI Components(创建定制 UI 组件)”部分。要进行下一步以及设计供其他人重用的定制组件,您需要考虑一些重要事项。以下是对这些事项的简要介绍:

创建定制 JSF 1.2 组件时,所需的对象集包括一个标记库定义文件、一个 JSF 配置文件(通常名为 faces-config.xml)、一个标记处理程序类和一个呈现器类。通常还会开发一个定制的组件类,但并非在所有情况下都需要它们。以下部分是使用 bp-jsf-example 样例应用程序的 "CompB" 定制组件作为编码示例的参考。

服务器端代码:

JSF 定制呈现器 (com.sun.javaee.blueprints.components.ui.example.CompBRenderer)

在呈现通用工件时,一种跟踪是否已呈现这些工件的方法是,在请求映射中设置标志。呈现器代码检查该标志是否已存在,如果不存在,它将设置该标志并呈现通用工件。如果该标志已存在,则不会重新呈现这些工件。请注意,此组件要求将 FacesServlet 映射到 "/faces/*" URI。如果要分发此简单组件以供其他人使用,则应该在组件的用户文档中说明这种依赖关系。

Map requestMap=context.getExternalContext().getRequestMap();
Boolean scriptRendered=(Boolean)requestMap.get(RENDERED_SCRIPT_KEY);

// Check to see if resource already rendered
if (scriptRendered != null && scriptRendered.equals(Boolean.TRUE)) {
return;
}

// put flag in requestMap to indicate already rendered
requestMap.put(RENDERED_SCRIPT_KEY, Boolean.TRUE);

String contextRoot=context.getExternalContext().getRequestContextPath();

// Render markup tag for the javascript file
writer.startElement("script", component);
writer.writeAttribute("type", "text/javascript", null);
writer.writeAttribute("src", contextRoot + "/faces/jsf-example/compB/compB.js", null);
writer.endElement("script");
writer.write("\n");

由于我们的组件是用于显示详细信息的弹出式组件,因此,我们需要呈现某个标记,以用于区分出实际所显示的部分。可以使用多种方法来完成此操作,一种方法是使用模板文件,读入该文件并将标记替换为维护名称空间约定所需的值。此处,将模板文本的 "%%%ID%%%" 标记替换为组件 ID 以防止组件的多个实例发生冲突。组件开发者也可以使用 ResponseWriter 的 startElement 和 endElement 方法,直接在呈现器中对模板进行编码。如果要使用工具管理组件(以便在 IDE 中以可视方式使用),则这可能是最佳的方法,因为每个 startElement 调用都与组件相关联。这样,工具便可以更好地跟踪属于各个组件的标记。

	<div class="bpui_compB_popTop">
<div class="bpui_compB_cornerTL"><div class="bpui_compB_cornerTR">
<center><font color="white"><b><span id="%%%ID%%%_title">title</span></b></font></center>
</div></div>
</div>
<div class="bpui_compB_popMid">
<table border="0" style="width: 100%">
<tr>
<td>
<table border="0" bgcolor="#ffffff" cellpadding="5" cellspacing="5">
<tr>
<td><span id="%%%ID%%%_message">Value</span></td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<div class="bpui_compB_popBot">
<div class="bpui_compB_cornerBL"><div class="bpui_compB_cornerBR">
</div></div>
</div>


JSF 定制 PhaseListener (com.sun.javaee.blueprints.components.ui.example.CompBPhaseListener)
我们的示例中的 PhaseListener 可实现不同类型的请求。如果根目录 ID 中包含 "APP_KEY",并且它包含的后缀与此 PhaseListener 用于提供的资源类型相匹配,则它提供静态资源。PhaseListener 还会将包含相应 "ACTION_KEY" 的 AJAX 请求转发给在根目录 ID 中指定的受管 Bean 的方法。

public void afterPhase(PhaseEvent event) {
FacesContext context=event.getFacesContext();
String rootId=context.getViewRoot().getViewId();

int iPos=rootId.indexOf(APP_KEY);
int iPosx=rootId.indexOf(ACTION_KEY);

// see what suffix is used for mapping to content type
if (rootId.endsWith(SCRIPT_SUFFIX) && iPos > -1) {
handleResourceRequest(event, PATH_PREFIX + rootId, "text/javascript");
} else if (rootId.endsWith(CSS_SUFFIX) && iPos > -1) {
handleResourceRequest(event, PATH_PREFIX + rootId, "text/css");
} else if (rootId.endsWith(GIF_SUFFIX) && iPos > -1) {
handleResourceRequest(event, PATH_PREFIX + rootId, "image/gif");
} else if (rootId.endsWith(JPG_SUFFIX) && iPos > -1) {
handleResourceRequest(event, PATH_PREFIX + rootId, "image/jpeg");
} else if (rootId.endsWith(PNG_SUFFIX) && iPos > -1) {
handleResourceRequest(event, PATH_PREFIX + rootId, "image/x-png");
} else if (iPosx > -1) {
// action to invoke through a deferred method expression
String methodx="#{" + rootId.substring(iPos + ACTION_KEY.length() + 1) + "}";
methodx=methodx.replace('/','.');

try {
Class[] argTypes = { PhaseEvent.class};
// create method expression
MethodExpression mex=context.getApplication().getExpressionFactory().createMethodExpression(context.getELContext(),
methodx, null, argTypes);
Object[] args = { event };
// invoke method of managed bean
mex.invoke(context.getELContext(), args);
} catch (Exception e) {
// Just log exception
e.printStackTrace();
}
}
}


JSF 受管 Bean (com.sun.javaee.blueprints.components.ui.example.CompBBean)
在 受管 Bean 中,将从 PhaseEvent 中提取 HttpServletReponse,然后构造并返回 XML 响应。由于我们的示例要求返回 XML,因此,将相应的上下文类型设置为 "text/xml"。我们还设置了状态代码,因为 AJAX 回调函数也会查找响应状态 "200",然后才会显示详细数据。请注意,我们设置了响应标头以指示不应缓存此请求/响应集。如果未设置这些标头值,某些浏览器将缓存结果,在随后包含相同 URL 的请求中,浏览器将自动多次提供相同的响应,根本不执行新的查询。在某些情况下,这可能是有益的,但对于轮询 AJAX 组件,是不希望出现缓存行为的。还要注意,调用了 FacesContext 的 responseComplete 方法以指示应跳过 JSF 生命周期的其余部分。

	public void processMethod(PhaseEvent event) {
HttpServletResponse response=null;
try {
FacesContext context=event.getFacesContext();
response=(HttpServletResponse)context.getExternalContext().getResponse();
StringBuffer sb=new StringBuffer();
response.setContentType("text/xml;charset=UTF-8");
response.setStatus(200);
// need to set no cache or IE will not make future requests when same URL used.
response.setHeader("Pragma", "No-Cache");
response.setHeader("Cache-Control", "no-cache,no-store,max-age=0");
response.setDateHeader("Expires", 1);
sb.append("<response>");
// custom response code goes here
...
sb.append("</response>");
response.getWriter().write(sb.toString());
} catch (IOException iox) {
iox.printStackTrace();
} finally {
try {
response.getWriter().flush();
} catch (Exception ee) {}
// set response complete so JSF lifecycle is not continued
event.getFacesContext().responseComplete();
}
}


组件的 faces-config.xml 文件
下面列出了在 faces-config.xml 文件中注册且与 "CompB" 组件有关的的工件。其中包括使用标准组件系列的定制呈现器、实现静态请求并转发动态请求的 PhaseListener 以及创建 AJAX 请求响应的受管 Bean。请注意,受管 Bean 包含一个受管属性,它是在每个请求后通过属性增变方法预置的。如果请求不包含 itemId 参数,则会将 itemId 设置为 Null。
	<!-- register the custom renderer for the standard component family -->
<render-kit>
<renderer>
<component-family>javax.faces.Output</component-family>
<renderer-type>CompB</renderer-type>
<renderer-class>com.sun.javaee.blueprints.components.ui.example.CompBRenderer</renderer-class>
</renderer>
</render-kit>

<!-- register components custom phaselistener -->
<lifecycle>
<phase-listener>com.sun.javaee.blueprints.components.ui.example.CompBPhaseListener</phase-listener>
</lifecycle>

<!-- register the custom managed bean, with a managed property (the requests itemId parameter -->
<managed-bean>
<managed-bean-name>CompBBean</managed-bean-name>
<managed-bean-class>com.sun.javaee.blueprints.components.ui.example.CompBBean</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
<managed-property>
<property-name>itemId</property-name>
<value>#{param.itemId}</value>
</managed-property>
</managed-bean>




客户端代码:


CompB 定制组件的标记将位于 JSP 页中,下面详细介绍了该标记:

	<ui:compB id="pop1" url="faces/bpui_compb_action/CompBBean/processMethod?itemId="/>

        <a href="#" onmouseover="bpui.compB.showPopup('pop1', event, 'test1A')" 
onmouseout="bpui.compB.hidePopup('pop1')" style="cursor: pointer"><b>Mouse over link to see popup (test1A)</b></a><br/>
<small><i>(With a JSF Managed Bean fulfilling the AJAX Request)</i></small><br/><br/>

在填充的定制标记通过 FacesServlet 被呈现后,产生的 HTML 标记和 Javascript 代码应如下所示:
	<div id="pop1" class="bpui_compB_popup">            
<div class="bpui_compB_popTop">
<div class="bpui_compB_cornerTL"><div class="bpui_compB_cornerTR">
<center><font color="white"><b><span id="pop1_title">title</span></b></font></center>
</div></div>
</div>
<div class="bpui_compB_popMid">
<table border="0" style="width: 100%">
<tr>
<td>
<table border="0" bgcolor="#ffffff" cellpadding="5" cellspacing="5">
<tr>
<td><span id="pop1_message">Value</span></td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<div class="bpui_compB_popBot">
<div class="bpui_compB_cornerBL"><div class="bpui_compB_cornerBR">
</div></div>
</div>
</div>

<script type="text/javascript">
bpui.compB['pop1']=new bpui.compB.createPopup('pop1', '/bp-jsf-example/faces/bpui_compb_action/CompBBean/processMethod?itemId=');
</script>
请注意,Javascript 代码创建新的 bpui.compB.createPopup 对象并将结果存储在 bpui.compB 名称空间的 "pop1" 属性下面。此对象是一个 Javascript 关闭,通过 bpui.compB.showPopup 函数触发弹出式组件时将检索此对象(如下所述)。


Javascript 函数:
呈现 CompB 组件时,将调用以下 Javascript 函数,它启动弹出式组件对象关闭以保留 AJAX 回调函数以及组件的标识符和 URL。回调函数进行检查以确保获得有效的响应,分析返回的 XML 数据,然后填充并显示弹出式组件。
	// create a closure to maintain the component's id and AJAX url with callback function
bpui.compB.createPopup=function(componentId, urlx) {
this.componentId=componentId;
this.urlx=urlx;

this.ajaxReturnFunction=function() {
// make sure response is ready
if (bpui.compB.req.readyState == 4) {
// make sure it is a valid response
if (bpui.compB.req.status == 200) {
// populate the popup with the info from the response
var resultx=bpui.compB.req.responseXML.getElementsByTagName("response")[0];
document.getElementById(componentId + "_title").innerHTML=resultx.getElementsByTagName("title")[0].childNodes[0].nodeValue;
document.getElementById(componentId + "_message").innerHTML=resultx.getElementsByTagName("message")[0].childNodes[0].nodeValue;;
// show popup with the newly populated information
document.getElementById(componentId).style.visibility='visible';
} else if (bpui.compB.req.status == 204){
// error, just show alert
alert("204 returned from AJAX call");
}
}
}
}



触发弹出式组件的对象使用以下 Javascript 函数来放置弹出式组件,并设置超时值以触发用于发送 AJAX 请求的函数。

	bpui.compB.showPopup=function(popupx, eventx, itemId) {
// Position popup base on event and set timeout so popup isn't flashing all the time
var xx=0;
var yy=0;
if (!eventx) var eventx=window.event;
if (eventx.pageX || eventx.pageY){
xx=eventx.pageX;
yy=eventx.pageY;
} else if (eventx.clientX || eventx.clientY) {
xx=eventx.clientX + document.body.scrollLeft;
yy=eventx.clientY + document.body.scrollTop;
}
document.getElementById(popupx).style.left= (xx + 3) + "px";
document.getElementById(popupx).style.top=yy + "px";
// make sure the popup doesn't show all the time, need to mouseover for at least 1 second.
bpui.compB.timeout=setTimeout("bpui.compB.showPopupInternal('" + popupx + "', '" + itemId + "')", 1000);
}


以下 Javascript 函数启动 XMLHttpRequest,按标识符检索弹出式组件 Javascript 对象,将 AJAX URL 与 itemId 连接在一起,设置特定弹出式组件的 AJAX 回调函数并发送请求。
	bpui.compB.showPopupInternal=function(popupx, itemId) {
// initialize the AJAX request
bpui.compB.req=bpui.compB.initRequest();
// retrieve the correct popup object that is being shown
popObject=bpui.compB[popupx];

// concatenate the itemId value to the URI
url=popObject.urlx + escape(itemId);
// set the correct popup's callback function
bpui.compB.req.onreadystatechange = popObject.ajaxReturnFunction;
bpui.compB.req.open("GET", url, true);
// send the request
bpui.compB.req.send(null);
}



在不呈现完整视图的情况下处理 AJAX 请求:

上面使用 FacesServlet 来简化提供静态资源(Javascript 文件和层叠样式表)以及委托动态资源(AJAX 请求)的过程。一种替代方法是使用 HTTP POST 将实际视图状态提交回服务器(通过 AJAX 请求)。这样做需要支持 AJAX 的组件终止正常的页面处理以防止为 AJAX 请求呈现整个 JSP 页。这是在 FacesContext 上使用 ResponseComplete 方法以跳过其余阶段来实现的。体系结构中的另一个不同之处是,PhaseListener 只负责呈现静态资源,而呈现器处理 AJAX 请求以产生 XML 响应。

在确定视图状态与每个请求(相对于 AJAX 请求数量)的关联程度时,应该考虑到性能问题。尽管这种方法与 JSF 生命周期更紧密地耦合在一起,从而可随时访问 Faces 对象,但是,与没有与视图状态紧密耦合在一起的方法相比,这种紧密耦合可能会造成性能下降。


参考资料


© Sun Microsystems 2006。Java BluePrints Solutions Catalog 中的所有内容受版权保护,未经 Sun Microsystems 的明确书面许可,不得在其他产品中发布。