概述
最近项目需要一个主动推送的功能,果断百度、google发现网上主要有两种实现方式:
一种是使用HTML5的webSockect,这个需要Tomcat7以上才支持,而且需要客户端的IE浏览器都支持HTML5。所以被我们果断的放弃的了。
另一种是使用dwr实现,dwr是一个JS与服务端Java类交互的Ajax框架。可以做到后台调用Java类方法的同时前台JS方法执行,前台JS方法执行的同时后台Java类的对应方法被执行。
最后使用了DWR来实现后台向前台的数据推送。
基本实现思路:
既然涉及到推送,那么我们肯定要明确推送的发起点以及推送的目标点(每个用户都要有明确ID),并且保证当用户在不同页面跳转时,保证数据都能够推送到前台(会话状态)。在我们的系统中,主要发起点是用户A在前台点击某些操作触发的,推送的目标点是用户B。
Dwr推送的基本思想是将一个后台的Java类映射为一个前台的同名JS类,同时通过JS维持一个长连接,与每个页面产生一个scriptsession,该session保证了长连接的同时也保证了每个链接会话的不同状态。当前台调用java对应的JS类中的某个方法时,dwr调用后台的java类中的同名方法,后台java类的某个方法被调用的时候,前台的对应JS对象的方法也会被调用,也就实现了向前台推送。
实现方式:
1,引入DWR包
2,在web.xml文件中配置:
<listener>
<listener-class>
org.directwebremoting.servlet.DwrListener
</listener-class>
</listener>
<servlet>
<servlet-name>dwr-invoker</servlet-name>
<servlet-class>
org.directwebremoting.servlet.DwrServlet
</servlet-class>
<init-param>
<param-name>crossDomainSessionSecurity</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>allowScriptTagRemoting</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>classes</param-name>
<param-value>java.lang.Object</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>3000</param-value>
</init-param>
<init-param>
<param-name>logLevel</param-name>
<param-value>WARN</param-value>
</init-param>
<init-param>
<param-name>debug</param-name>
<param-value>true</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dwr-invoker</servlet-name>
<url-pattern>/dwr/*</url-pattern>
</servlet-mapping>
以上是在web.xml中的配置,具体配置信息的含义都可以在
http://directwebremoting.org/dwr/documentation/server/configuration/servlet/index.html页面中查看。
3,添加dwr.xml文件
该文件默认存放路径在WEB-INF路径下。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dwr PUBLIC "-//GetAhead Limited//DTD Direct Web Remoting 3.0//EN" "http://getahead.org/dwr/dwr30.dtd">
<!-- 指明暴露给前台JS的后台JAVA类,即将后台的JAVA类转换为前台的JS类,并且JAVA类中的public方法,
都会在JS类中生成同名的JS函数 -->
<dwr>
<allow>
<!-- creator是java类的创建者,有spring(dwr与spring集成的时候使用)、new(单独使用)等
javascript指创建的JS类的名称即生成的JS文件的名称,这里是MessagePush.js -->
<create creator="spring" javascript="MessagePush">
<!-- name值可以使class,配合creator=new使用.也可以是beanName,配合creator=spring使用
value的值根据不同的情况值不同,当name值为new,那么value的值就是目标Java类的全类名;
当name的值为beanName时,value的值就是spring实例化出的bean的id -->
<param name="beanName" value="pushUtil"></param>
</create>
<!-- creator是java类的创建者,有spring(dwr与spring集成的时候使用),javascript指创建的
JS类的名称即生成的JS文件的名称,这里是aclService.js -->
<create creator="spring" javascript="aclService">
<param name="beanName" value="aclManageService"></param>
</create>
<!--
<create creator="new" javascript="MessagePush">
<param name="class" value="com.changan.test.DWRTest"></param>
</create>
-->
</allow>
</dwr>
4,在spring中配置:
<bean id="pushUtil" class="com.changan.common.pushUtils.PushUtil"></bean>
5,在JS中引入如下JS文件,这些JS文件不是实际存在的,而是通过DWR的servlet根据dwr.xml文件配置自动生成的。
<script type="text/javascript" src="<%=basePath%>dwr/engine.js"></script>
<script type="text/javascript" src="<%=basePath%>dwr/util.js"></script>
<script type="text/javascript" src="<%=basePath%>dwr/interface/MessagePush.js"></script>
6,在前台页面调用后台java类的方法:
这里的这个方法是在onload方法中调用,以保证页面加载成功后就创建一个ScriptSession对象,来保持长连接通话
function onPageLoad(){
var userId = '${users.userId}';//这个userId是用来区分ScriptSession的
//dwr创建的JS对象MessagePush,对应dwr.xml中的定义,这里调用了后台方法
MessagePush.onPageLoad(userId);
}
7,定义被Java后台类调用的前台JS方法
function showMessage(msg){
alert(msg);
}
8,后台Java类:
- PushUtil类,暴露、转化为JS对象的Java类
/**
*
* @ClassName: PushUtil
* @Description: 推送的主要实现类,由Spring实例化,同时dwr.xml文件中配置.
* @author lixiaodai
* @date 2013-11-2 上午9:50:26
*
*/
public class PushUtil {
/**
*
* @Title: onPageLoad
* @Description: 前台页面创建的onload事件会调用这个方法的JS方法,其实和调用这个方法一样
* 该方法会在每次调用时创建一个脚本会话
* @param userId 不同session的回话标识
* @return void 返回类型
* @throws
*/
public void onPageLoad(String userId) {
//ScriptSession,DWR中提供的脚本会话对象,这个会话是储存在本地线程中的
ScriptSession scriptSession = WebContextFactory.get().getScriptSession();
//给每个脚本会话赋值一个属性,一般作为脚本会话的区别属性
scriptSession.setAttribute("userId", userId);
//初始化信息
initInfo();
}
//初始化方法
private void initInfo() {
//得到当前服务端的dwr容器
Container container = ServerContextFactory.get().getContainer();
//从dwr容器中得到脚本会话管理类
ScriptSessionManager manager = container.getBean(ScriptSessionManager.class);
//从脚本会话管理类中得到目前所有的脚本会话对象
Collection<ScriptSession> sessions = manager.getAllScriptSessions();
//得到当前访问用户的HttpSession
HttpSession httpSession = WebContextFactory.get().getSession();
//判断当前Http会话中是否已经有scriptSessionId属性
//如果有,则说明该HttpSession已经绑定了一个ScriptSession对象
//如果没有,则说明该HttpSession还没有绑定ScriptSession
if(httpSession.getAttribute("scriptSessionId")!=null){
//得到当前HttpSession中存放的scriptSessionId属性
int id = (Integer)httpSession.getAttribute("scriptSessionId");
//遍历所有的ScirptSession对象,尝试将所有ScirptSession的id不是HttpSession中存放的scriptSessionId
//的ScriptSession对象废止,注意:这里是废止不是立刻删除
for(ScriptSession session:sessions){
if(session.hashCode()!=id){
session.invalidate();
}
}
}
// System.out.println("after invalidate sessionId:"+httpSession.getId()+",scriptSessionCount:"+manager.getScriptSessionsByHttpSessionId(httpSession.getId()).size());
//得到会话监听对象
ScriptSessionListener listener = PushListener.getInstance();
//将监听对象添加到ScriptSessionManager管理类上
manager.addScriptSessionListener(listener);
}
//这个方法用来推送,也就是当调用这个方法的时候,前台的JS对应函数就会被触发
public static void sendMessageAuto(String userid,String message) {
//由于我们的推送是有目标的,所以需要目标ID以及要推送信息
Browser.withAllSessionsFiltered(new PushFilter(userid),new PushRunable(message));
}
}
- PushListener类
/**
*
* @ClassName: PushListener
* @Description: 会话监听器类,是一个单例类,这个类主要来当监听到ScriptSession创建,
* 那么就分别在新创建的ScriptSession和HttpSession两个不同级别的会话中
* 互相绑定对方的唯一标识
* @author lixiaodai
* @date 2013-11-7 上午9:56:57
*
*/
public class PushListener implements ScriptSessionListener{
private static PushListener listener;
private PushListener() {
}
public static synchronized PushListener getInstance(){
if(listener==null){
listener = new PushListener();
}
return listener;
}
/**
* 会话/长连接创建时调用的方法
*/
public void sessionCreated(ScriptSessionEvent ev) {
//得到当前的HttpSession类
HttpSession session = WebContextFactory.get().getSession();
//当前登录用户的用户ID
String userId = ((Users) session.getAttribute("users")).getUserId() + "";
//向新创建的ScriptSession中添加属性userId,来标识该ScriptSession对应的用户
ev.getSession().setAttribute("userId", userId);
//向HttpSession中设置新生成的ScriptSession对象的ID
session.setAttribute("scriptSessionId", ev.getSession().hashCode());
}
/**
* 会话(长连接)关闭时调用的方法
*/
public void sessionDestroyed(ScriptSessionEvent ev) {
//尝试废止该ScriptSession对象
ev.getSession().invalidate();
}
}
- PushFilter类
/**
*
* @ClassName: PushFilter
* @Description: 这个类用来过滤不同的SessionScript,保证我们要推送的数据能够准确推送到目标的
* ScriptSession中
* @author lixiaodai
* @date 2014-3-21 上午10:31:05
*
*/
public class PushFilter implements ScriptSessionFilter {
private static PushFilter filter;
private String userId;
public PushFilter() {
}
public PushFilter(String id){
this.userId = id;
}
/**
* 主要的过滤方法,根据我们的条件来过滤推送到哪个ScriptSession中
*/
@Override
public boolean match(ScriptSession session) {
//根据脚本中的userId属性来判断是否是要推送的目标脚本会话
if (session.getAttribute("userId") == null){
return false;
}else{
return (session.getAttribute("userId")).equals(userId);
}
}
}
- PushRunnable
/** * * @ClassName: PushRunable * @Description: 用来实际执行推送的类,这个类是一个线程,同时要设定目标推送的方法以及要推送的信息 * @author lixiaodai * @date 2014-3-21 上午10:47:19 * */ public class PushRunable implements Runnable { private String message; private ScriptBuffer script = new ScriptBuffer(); public PushRunable(){ } public PushRunable(String msg){ this.message = msg; } /** * 新启的线程,执行的业务 */ @Override public void run() { //要推送到的前台目标的JS方法以及该方法的参数 script.appendCall("showMessage", message); //这里得到的ScriptSession的集合是通过PushFilter过滤过的 Collection<ScriptSession> sessions = Browser.getTargetSessions(); for (ScriptSession scriptSession : sessions) { // System.out.println(scriptSession.getAttribute("userId")); scriptSession.addScript(script); } } }
并做了一点改进,敬请拍砖,3Q。