概述
最近项目需要一个主动推送的功能,果断百度、google发现网上主要有两种实现方式:
一种是使用HTML5的webSockect,这个需要Tomcat7以上才支持,而且需要客户端的IE浏览器都支持HTML5。所以被我们果断的放弃的了。
另一种是使用dwr实现,dwr是一个JS与服务端Java类交互的Ajax框架。可以做到后台调用Java类方法的同时前台JS方法执行,前台JS方法执行的同时后台Java类的对应方法被执行。
基本实现思路:
既然涉及到推送,那么我们肯定要明确推送的发起点以及推送的目标点(每个用户都要有明确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页面中查看。
该文件默认存放路径在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类的方法:
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); } } }