原文:
http://www.abigdreamer.com/mywork/webqq-update-online-reverse-ajax-implementation-off-the-assembly-line-simulation.html
本blog已转移到造梦师http://www.abigdreamer.com,欢迎大家常去我的blog看看!
说明:本次更新只是模拟了一下人员的上下线,并没有采用真是的数据(我一台电脑开那么多浏览器测试的话有点受不了,谁叫电脑烂呢,呵呵) 。另外需要注意的就是,需要用这个新的dwr.jar覆盖掉原来的那个dwr.jar,要不会出问题的!
有朋友问我怎样用反向AJAX来实现在线人员列表的更新,提到DWR允许javascript访问服务器端的Java方法,这使得AJAX使用起来会比较容易,而在DWR2.0里面添加了一个非常强大的功能——反向AJAX,也就是说,服务器端的Java方法可以取得浏览器端的Web上下文,并可以调用javascript的方法,将服务器端的数据异步地传输给浏览器。本文将通过一个demo展示这种特性。这个demo实现了类似股票交易系统中实时更新数据的功能,事实上是通过发布-订阅模式去实现的。也就是说,客户端订阅一个主题,服务器端通过一个线程向订阅这个主题的浏览器定时、异步地发送数据,从而实现了这种实时更新的功能。
我们知道,客户端浏览器可以随时连接到web服务器,并向服务器请求资源,而服务器却没有这种能力,它不能主动地于客户端浏览器建立连接,并主动地将数据发送给浏览器。DWR支持3种从服务器端发送数据给客户端的方式:
1、轮询。客户端在每个时间周期内向服务器发送请求,看看服务器端有没有数据更新,如果有,就向服务器请求数据。
2、Comet:基于HTTP长连接的服务器推动方式。客户端向服务器发送请求后,服务器将数据通过response发送给客户端,但并不会将此response关闭,而是一直通过response将最新的数据发送给客户端浏览器,直到客户端浏览器关闭。
3、PiggyBack(回传)。服务器端将最新的数据排成队列,然后等待客户端下一次请求,接收到请求后就将等待更新的数据发给客户端。
这3种方式各有优劣,而DWR可以同时支持轮询和Comet。
首先,我们要让DWR程序支持反向AJAX。只需要在web.xml中添加如下配置:
<servlet>
<servlet-name>dwr-invoker</servlet-name>
<servlet-class>org.directwebremoting.servlet.DwrServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>activeReverseAjaxEnabled</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>initApplicationScopeCreatorsAtStartup</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>maxWaitAfterWrite</param-name>
<param-value>100</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dwr-invoker</servlet-name>
<url-pattern>/dwr/*</url-pattern>
</servlet-mapping>
将初始化参数activeReverseAjaxEnabled设置为true表示启动反向AJAX。另外,添加这个功能的核心之处在于服务器端的发布者——Publisher.java,在这个类里面,首先通过org.directwebremoting.WebContext来获得访问这个应用的Web上下文:
WebContext webContext = WebContextFactory.get();
ServletContext servletContext = webContext.getServletContext();
serverContext = ServerContextFactory.get(servletContext);
webContext.getScriptSessionsByPage("");
这里主要通过WebContext类获得DWR应用的WEB上下文,用ServletContext获得DWRServlet的上下文,以及通过WEB上下文获取访问本应用的客户端浏览器的ScriptSession。通过ScriptSession,可以在服务器端向客户端浏览器发出执行js方法的指令,并把服务器端数据传送给js方法,具体的用法如下:
Collection sessions = serverContext
.getScriptSessionsByPage("/webQQ/webQQ.jsp");
ScriptProxy proxy = new ScriptProxy(sessions);
WebQQUser user = users.getNextUserStatu();
proxy.addFunctionCall("publishUserStatus", user);
这段代码首先通过getScriptSessionsByPage方法获得所有访问/webQQ/webQQ.jsp这个资源的客户端浏览器的ScriptSession,并为这些session建立代理(ScriptProxy),通过这个代理,让客户端执行publishUserStatus的js方法。其中addFunctionCall就是向客户端发送执行js方法的服务器端方法,第一个参数是js方法的签名,后面的都是js方法的参数。user是要发布的即时数据。user这个对象是随机生成的(见org.darkness.test.webqq. WebQQUsers类),Publish.java这个类启动了一个线程(worker),这个线程不断地生成user的数据,并发布给客户端。
以下是html页面的核心部分的代码:
var listeners = [];
function subscribeUserStatus(callBackHandler){
listeners.push(callBackHandler);
}
function publishUserStatus(user){
for(var i=0;i<listeners.length;i++){
listeners[i].fun.call(listeners[i].scope,user);
}
}
subscribeUserStatus({
fun:function(user){
try{
var rootChildNodes = Ext.getCmp('im-tree').root.childNodes;
for(var i=0;i<rootChildNodes.length;i++){
var childNodes = rootChildNodes[i].childNodes;
for(var j=0;j<childNodes.length;j++){
if(childNodes[j].id == user.account){
if(user.isShowOnline == 0){
childNodes[j].getUI().getIconEl().src = 'images/user_delete.gif';
var tempChild = childNodes[j];
// appendChild就会将其放置到最尾端,不需要再remove了
//rootChildNodes[i].removeChild(tempChild);
rootChildNodes[i].appendChild(tempChild);
}else{
childNodes[j].getUI().getIconEl().src = 'images/user.gif';
// 将刚上线的人员移动到最顶端
var firstChild = rootChildNodes[i].firstChild;
var tempChild = childNodes[j];
rootChildNodes[i].insertBefore(tempChild, firstChild);
}
// 本来想用TreeNode自带的排序方法的,可试过了,总是没反应
//rootChildNodes[i].sort(function(nodeA, nodeB){
// return nodeA.getUI().getIconEl().src > nodeB.getUI().getIconEl().src;
//}, rootChildNodes[i]);
return;
}
}
}
}catch(e){
}
},
scope:this
});
dwr.engine.setActiveReverseAjax(true);
这一块代码主要是将改变在线或下线的用户的图标。
subscribeUserStatus方法订阅在线人员列表的数据(这些数据由服务器端的Java方法进行发布)。其中通过一个匿名回调函数,将取得改变的数据在页面显示出来(即人员的上、下线)。该回调方法非常简单,只是将TreeNode组件中的图标改变了一下,实现了实时提醒用户上下线的功能。
另外,服务器端还有一个监听器PublisherServletContextListener,这是为了在适当的时候关闭发布者的线程。这个监听器要结合其他两个DWR的监听器使用,只需在web.xml里面声明就行了:
<listener>
<listener-class>org.directwebremoting.servlet.EfficientShutdownServletContextAttributeListener</listener-class>
</listener>
<listener>
<listener-class>org.directwebremoting.servlet.EfficientShutdownServletContextListener</listener-class>
</listener>
<listener>
<listener-class> org.darkness.webqq.web.servlet.PublisherServletContextListener</listener-class>
</listener>
最后,看一下dwr的映射关系dwr.xml:
<dwr>
<allow>
<create creator="new" javascript="Publisher" scope="application">
<param name="class" value="org.darkness.webqq.web.servlet.Publisher"/>
</create>
<convert converter="bean" match="org.darkness.webqq.model.WebQQUser"/>
<!-- this is a bad idea for live, but can be useful in testing -->
<convert converter="exception" match="java.lang.Exception"/>
<convert converter="bean" match="java.lang.StackTraceElement"/>
</allow>
</dwr>
注意<convert converter="bean" match="org.darkness.webqq.model.WebQQUser"/>这个配置,dwr允许将自定义的Java类型与js对象进行相互转换,但要声明转换器。
以下是程序运行的结果: