概述
最近項目需要一個主動推送的功能,果斷百度、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頁面中查看。<?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); } } }
並做了一點改進,敬請拍磚,3Q。