大多数网站都有站内信,未读消息,今日要闻等消息的推送功能,就拿本站来说打开今日第一次csdn首页,立马会在右下角出现一个弹出窗口,就是下图这样的,你一定见过的:
很多无良网站都会有各种各样的浮动层,飘来飘去的很烦人,一不小心点到了,弹出令人更烦的无良广告。csdn当然不是无良网站,尽管有弹出层,但没有乱飞,并且也不是以广告为目的的,这样大家就不会抵制和排斥了。作为一个web开发者,我曾经略微思考过,如何实现这种功能呢?什么触发事件,如何响应的呢?正好用户要求加这么一个消息提醒功能,就借此深入研究了一下。
至于消息推送的功能,网上给的建议是使用dwr的消息推送,由于平时我们所做的操作都是基于请求--》处理--》响应模式的,那么没有请求直接响应可以吗?http协议似乎对此支持的并不好。我们常见的消息推送,短信、聊天工具、邮件,这些都是需要服务器来支持的,那么在javaEE web开发中,能不能实现无请求的消息推送呢?
遍差网上资料,发现有几个方案可以支持:1,dwr3实现;2,comet4j实现。这两个技术之前都没怎么接触过,如何选择呢?dwr技术毕竟更为成熟,网上的资料更多,可行性更强些。毕竟身边的人会的不多,客户那边要求挺紧,必须兼顾到各种情况。
客户要求是:当后台发布了一篇内容(报道、公告、新闻通知或其他)之后,前台已登录的用户在首页上可以看到消息提醒。
那么我就把问题分成几步来解决:
1,后台发布消息的时候要做登记,不能直接广播,要指定推送目标,那么就要做一个内容和接收人的对应关系,后台发布内容时作登记。
2,用户进入首页的时候,检测是否存在符合推送条件(有文章且允许)。
3,查出所有用户的未读信息列表,弹出提醒窗口。
4,用户点击某条信息后,把状态为设为已读。
这么几个步骤,就可以解决问题了。
首先说第一步:这一步似乎是最简单的,但却是最不容易思考的地方,由于项目本身对用户的管理是通过组来实现,在发布内容的时候也是列出的所有的组供用户选择,让用户勾选。我们可以在这个时候“趁火打劫”,在保存组的时候,不妨把组里面的用户也一一取到,放入我们的操作表中,操作表结构如下:
仅有两列,一列是内容id,一列是user_id,本想再弄一个标志位,后来觉得用户把信息读取一条信息之后就可以把本条记录直接删除了,留着也没有实际意义。
// 保存浏览会员组 if (viewGroupIds != null && viewGroupIds.length > 0) { for (Integer gid : viewGroupIds) { bean.addToGroups(cmsGroupMng.findById(gid)); } } // 保存浏览会员 String addToMsgAlert = request.getParameter("addToMsgAlert"); //前台选择了该消息,要做提醒 if(addToMsgAlert!=null&&Boolean.parseBoolean(addToMsgAlert)){ List<Integer> groupList = this.getUserViewGroupIds(bean); Integer[] user_viewGroupIds = null; if(groupList!=null&&groupList.size()>0){ user_viewGroupIds = groupList.toArray(new Integer[0]); } if (user_viewGroupIds != null && user_viewGroupIds.length > 0) { List<CmsUser> userList = this.getUserListByGroupIds(user_viewGroupIds==null?null:Arrays.asList(user_viewGroupIds)); for (CmsUser cmsUser : userList) { bean.addToUsers(cmsUser); } } }
@SuppressWarnings("unchecked") private List<CmsUser> getUserListByGroupIds(List<Integer> groupList){ List<CmsUser> userList = new ArrayList<CmsUser>(); if(groupList==null){ for (CmsGroup group : cmsGroupMng.getList()) { userList.addAll(this.cmsUserMng.getList(null,null,group.getId(),null)); } }else{ for (Integer groupId : groupList) { userList.addAll(this.cmsUserMng.getList(null,groupId,null)); } } return userList; }
hibernate配置文件:
<set name="viewUsers" table="jc_content_user_flag"> <cache usage="read-write"/> <key column="content_id"/> <many-to-many column="user_id" class="CmsUser"/> </set>
第二、三步:消息侦测和弹出窗口:
<script type="text/javascript" src="../dwr/engine.js"></script> <script type="text/javascript" src="../dwr/util.js"></script> <script type="text/javascript" src="../dwr/interface/pushMsg.js"></script>
<div style="display: block;" id="_popup_msg_container">
<div class="im_popupWindow" id="im_popupWindow_miniMsg" wistate="max" style="display: block; height: 244px;"> <h6 class="wi_draggable"> <a href="#" onclick="_message_tips_pop('down');return false;"> <img src="/${res}/jlczmh/alert/x.gif" class="founctionpic3" alt="colse" /> </a> </h6> <div class="wi_content" id="im_popupWindow_miniMsgContent"> <div class="kuai"> <div class="tuijian"> <div id="alertTitle"><h3>您有<span id="msgNum"></span>条消息未读</h3></div> <div id="msgList"> </div> <div id="queryAll"> <span style="float: left;"><span style="margin-top: 2px;">不在提醒</span><input type="checkBox" onclick="notAlert();" id=""/></span> <a id="queryAll_a" onclick="queryAllMsg();return false;" href="javascript:void(0);"> </a> </div> </div> </div> </div> </div> </div> <SCRIPT language=JavaScript type=text/javascript> function notAlert(){ _message_tips_pop('down'); var userId = $("#userId").val(); pushMsg.alertOrNot(userId); } function _message_node(id) { return document.getElementById(id) } function _message_child(p,c) { var pdom = _message_node(p); var nodes = pdom.childNodes; var cdom = false; for (var i = 0; i < nodes.length; i++) { if (nodes[i].tagName == c.toUpperCase()) { cdom = nodes[i]; break } } return cdom } function _message_tips_pop(act) { var MsgPop = _message_child("_popup_msg_container","div"); var MsgPop = null == MsgPop ? document.getElementById("_popup_msg_container") : MsgPop; var popH = parseInt(MsgPop.clientHeight); if (act == "up") { MsgPop.style.display = "block"; show = setInterval("_message_changeH('up')",2) } if (act == "down") { hide = setInterval("_message_changeH('down')",2) } } function _message_changeH(str) { var MsgPop = _message_child("_popup_msg_container","div"); var MsgPop = null == MsgPop ? document.getElementById("_popup_msg_container") : MsgPop; var popH = parseInt(MsgPop.clientHeight); if (str == "up") { if (popH <= 240) { MsgPop.style.height = (popH + 4).toString() + "px" } else { clearInterval(show) } } if (str == "down") { if (popH >= 8) { MsgPop.style.height = (popH - 4).toString() + "px" } else { MsgPop.style.display = "none"; clearInterval(hide) } } } var server = window.location.host; if (server.indexOf("local") < 0) { server = "www.jb51.net" } var p = "1=1"; if (window.location.search.indexOf("?") > -1) { var p = window.location.search.substring(1) } var MsgPop = _message_child("_popup_msg_container","div"); if (MsgPop) { MsgPop.style.display = "none"; MsgPop.style.height = "0px" } function click_push(){ pushMsg2();//先加载一次 //var time = 5000; //5*10秒 //window.setInterval('pushMsg()',time); } pushMsg2 = function(){ var userId = $("#userId").val(); if(userId!=""&&userId!=null&&userId!=undefined){ dwr.engine.setAsync(false);//设置同步 pushMsg.getMsg(userId,function(data){ var jsonData = eval("("+data+")"); //alert(jsonData); var len = jsonData.length; if(len>0){ //alert("您有"+len+"条新内容未读"); var max = 8; var titleLen = 13; var html = "<ul>" for(var i = 0;i < (len>max?max:len);i++){ var title = jsonData[i].title; if(title.length>titleLen) title = title.substring(0,titleLen)+"..."; var addr = "/"+jsonData[i].path+"/"+jsonData[i].id+".jhtml"; html+="<li><a href='"+addr+"' onclick='readMsg2("+jsonData[i].id+","+userId+");' title='"+jsonData[i].title+"'>["+jsonData[i].date+"] "+title+"</a></li>"; } html+="</ul>"; $("#msgList").html(html); $("#msgNum").html(len); if(len>max){ $("#queryAll_a").html("查看所有未读信息"); } _message_tips_pop("up"); } }); dwr.engine.setAsync(true);//设置异步 } } readMsg2 = function(contentId,userId){ dwr.engine.setAsync(false);//设置同步 pushMsg.readMsg(contentId,function(data){ //alert(data); }); dwr.engine.setAsync(true);//设置异步 } //setTimeout("_message_tips_pop('up')",100); queryAllMsg = function(){ document.location.href='/content/getAllUnReadMsg.jspx'; } </SCRIPT> <script type="text/javascript"> $(function(){ click_push(); }); </script>
dwr配置文件:
<?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"> <dwr> <allow> <create creator="spring" javascript="pushMsg"> <param name="beanName" value="pushMsg"/> </create> </allow> </dwr>creator=‘spring’是指需要的bean是使用spring配置的,名字(id)叫做 pushMsg。
java代码:
package cn.com.dwr; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.Map; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.springframework.jdbc.core.JdbcTemplate; public class PushMsg { private JdbcTemplate jdbcTemplate; private String userId; public JdbcTemplate getJdbcTemplate() { return jdbcTemplate; } public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } /** * 根据userId获取所有的未读信息 * @param userId * @return * @throws JSONException */ @SuppressWarnings("unchecked") public String getMsg(String userId) throws JSONException { JSONArray msgArr = new JSONArray(); JSONObject o; Map<String,Integer> contentIdMap = null; String path = ""; Integer contentId = null; String title = ""; String releaseDate = ""; this.userId = userId; List contentIdList = jdbcTemplate .queryForList("SELECT content_id FROM jc_content_user_flag jf,jc_user ju where jf.user_id=ju.user_id and ju.alertornot= '0' and ju.user_id = " + Integer.parseInt(userId)+" order by content_id desc"); for (int i = 0; i < contentIdList.size(); i++) { o = new JSONObject(); contentIdMap = (Map<String,Integer>) contentIdList.get(i); contentId = contentIdMap.get("content_id"); path = getPathByContentId(contentId); Map<String,Object> map = getTitleAndTimeByContentId(contentId); title = (String) map.get("title"); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); releaseDate = sdf.format((Date)map.get("release_date")); o.put("id",contentId); o.put("path",path); o.put("title",title); o.put("date",releaseDate); msgArr.put(o); } return msgArr.toString();// contentIds.toString(); } /** * 阅读过一条信息,将此信息从表中删除 * @param contentId * @return */ public String readMsg(String contentId){ String result = "false"; try { jdbcTemplate.update("DELETE FROM jc_content_user_flag WHERE content_id = "+Integer.parseInt(contentId)+" and user_id="+Integer.parseInt(userId)); result = "true"; } catch (Exception e) { e.printStackTrace(); } return result; } /** * 设置用户不再提醒消息 * @param userId */ public void alertOrNot(String userId){ try{ jdbcTemplate.update("update jc_user set alertornot = '1' where user_id="+userId); System.out.println(userId); }catch (Exception e) { e.printStackTrace(); } } /** * 设置用户提醒消息 * @param userId */ public void alert(String userId){ try{ jdbcTemplate.update("update jc_user set alertornot = '0' where user_id="+userId); System.out.println(userId); }catch (Exception e) { e.printStackTrace(); } } private Map<String,Object> getTitleAndTimeByContentId(Integer contentId) { Map<String,Object> map = jdbcTemplate .queryForMap("select title,release_date from jc_content_ext where content_id=" + contentId); return map; } private String getPathByContentId(Integer contentId) { Map<String,Object> map = jdbcTemplate .queryForMap("select channel_path from jc_channel cha,jc_content con where cha.channel_id=con.channel_id and con.content_id=" + contentId); return (String) map.get("channel_path"); } }
其中采用了jdbc模板来操作,并没有使用和系统相同的hibernate,一来由于业务量小,不用费事处理复杂配置文件。二来,对这种形式的查询我更为熟悉。
效果:
如果点击不在提醒,那么用户表中的标志位就起了作用,这里没有设解锁功能,一旦设置成了不再提醒,想恢复就需要修改数据库。这是功能不够完善,用户暂时没提这个功能,也就乐得不改动。
后台发布的时候:
红框是可浏览的会员组选择,绿框表示本条消息是否作为提醒的消息,有了这两个参数,操作起来就变得灵活多了。
如果未读消息过多,那么弹出层右下角会出现“查看所有未读消息”,这样就能看全了。
总结:本方案并没有彻底实现消息推送功能,依然是前台刷新,后台响应这种模式,消息的推送不是即时的。但考虑到用户最常去的页面就是首页,session超时也设为10分钟,本项目发布的消息也不是十分紧急,考虑上述情况,无需采用即时推送,如果用户经常在线,那么最多延迟十分钟,如果用户不经常在线,即时推送也没有意义。
本方案并没有完全发挥dwr消息推送的优势,这样的推送形式ajax似乎也办得到,效率未必比它差,有时间可以换一种形式来实现。