如本文所用,Spring4和websocket要构建web聊天室,根据框架SpringMVC+Spring+Hibernate的Maven项目,后台使用spring websocket进行消息转发和聊天消息缓存。client使用socket.js和stomp.js来进行消息订阅和消息发送。具体实现见以下代码。
首先在pom.xml中加入对spring websocket的相关依赖包。
一、加入websocket依赖
<span style="font-size:14px;"> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-websocket</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-messaging</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>3.9</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.3.0</version> </dependency> </span>
当中<springframework.version>4.0.3.RELEASE</springframework.version>。
由于spring4以上才支持WebSocket。
2、配置Spring WebSocket
该配置能够在Spring MVC的配置文件配置,也能够使用注解方式配置。本文使用注解@Configuration方式进行配置。
<span style="font-size:14px;">package com.test.chat.controller; import java.util.List; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.simp.config.ChannelRegistration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer{ @Override public void registerStompEndpoints(StompEndpointRegistry registry) { //加入这个Endpoint。这样在网页中就能够通过websocket连接上服务了 registry.addEndpoint("/webchat").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry config) { System.out.println("server启动成功"); //这里设置的simple broker是指能够订阅的地址,也就是server能够发送的地址 config.enableSimpleBroker("/userChat","/initChat","/initFushionChart","/updateChart","/videoChat"); config.setApplicationDestinationPrefixes("/app"); } @Override public void configureClientInboundChannel(ChannelRegistration channelRegistration) { } @Override public void configureClientOutboundChannel(ChannelRegistration channelRegistration) { } @Override public void configureWebSocketTransport( WebSocketTransportRegistration registry) { // TODO Auto-generated method stub System.out.println("registry:"+registry); } @Override public boolean configureMessageConverters( List<MessageConverter> messageConverters) { // TODO Auto-generated method stub System.out.println("messageConverters:"+messageConverters); return true; } } </span>
要使配置文件生效,需在Spring的文件里可以扫描到该文件所在的包。即配置<context:component-scan base-package="com.test.**.controller" />
3、聊天内容的实体对象和后台关键代码
<span style="font-size:14px;">package com.test.chat.model; public class ChatMessage { //房间号 private String roomid; //username private String userName; //机构名 private String deptName; //当前系统时间 private String curTime; //聊天内容 private String chatContent; //是否是系统消息 private String isSysMsg; public String getIsSysMsg() { return isSysMsg; } public void setIsSysMsg(String isSysMsg) { this.isSysMsg = isSysMsg; } public String getRoomid() { return roomid; } public void setRoomid(String roomid) { this.roomid = roomid; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getDeptName() { return deptName; } public void setDeptName(String deptName) { this.deptName = deptName; } public String getCurTime() { return curTime; } public void setCurTime(String curTime) { this.curTime = curTime; } public String getChatContent() { return chatContent; } public void setChatContent(String chatContent) { this.chatContent = chatContent; } } </span>
后台关键处理代码,用于转发消息并缓存聊天记录
<span style="font-size:14px;">package com.test.chat.controller; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.Date; import java.util.HashMap; import java.util.Map; import javax.annotation.Resource; import javax.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.handler.annotation.DestinationVariable; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.messaging.simp.annotation.SubscribeMapping; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import com.alibaba.fastjson.JSONObject; import com.test.chat.model.ChatMessage; import com.test.chat.model.LimitQueue; import com.test.chat.model.VideoMessage; import com.test.framework.common.SessionContainer; import com.test.framework.service.GenericService; import com.test.framework.utils.DateUtil; @Controller public class UserChatController { //每一个聊天室缓存最大聊天信息条数,该值由SpringMVC的配置文件注入,超过该值将清理出缓存 private int MAX_CHAT_HISTORY; public void setMAX_CHAT_HISTORY(int MAX_CHAT_HISTORY) { this.MAX_CHAT_HISTORY = MAX_CHAT_HISTORY; } @Resource private GenericService genericService; // 用于转发数据 sendTo private SimpMessagingTemplate template; //消息缓存列表 private Map<String, Object> msgCache = new HashMap<String, Object>(); @Autowired public UserChatController(SimpMessagingTemplate t) { template = t; } /** * WebSocket聊天的对应接收方法和转发方法 * client通过app/userChat调用该方法,并将处理的消息发送client订阅的地址 * @param userChat 关于用户聊天的各个信息 */ @MessageMapping("/userChat") public void userChat(ChatMessage chatMessage) { // 找到须要发送的地址(client订阅地址) String dest = "/userChat/chat" + chatMessage.getRoomid(); // 获取缓存,并将用户最新的聊天记录存储到缓存中 Object cache = msgCache.get(chatMessage.getRoomid()); try { chatMessage.setRoomid(URLDecoder.decode(chatMessage.getRoomid(),"utf-8")); chatMessage.setUserName(URLDecoder.decode(chatMessage.getUserName(), "utf-8")); chatMessage.setDeptName(URLDecoder.decode(chatMessage.getDeptName(), "utf-8")); chatMessage.setChatContent(URLDecoder.decode(chatMessage.getChatContent(), "utf-8")); chatMessage.setIsSysMsg(URLDecoder.decode(chatMessage.getIsSysMsg(),"utf-8")); chatMessage.setCurTime(DateUtil.format(new Date(),DateUtil.formatStr_yyyyMMddHHmmss)); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } // 发送用户的聊天记录 this.template.convertAndSend(dest, chatMessage); ((LimitQueue<ChatMessage>) cache).offer(chatMessage); } @SubscribeMapping("/initChat/{roomid}") public LimitQueue<ChatMessage> initChatRoom(@DestinationVariable String roomid) { System.out.print("-------新用户进入聊天室------"); LimitQueue<ChatMessage> chatlist = new LimitQueue<ChatMessage>(MAX_CHAT_HISTORY); // 发送用户的聊天记录 if (!msgCache.containsKey(roomid)) { // 从来没有人进入聊天空间 msgCache.put(roomid, chatlist); } else { chatlist = (LimitQueue<ChatMessage>) msgCache.get(roomid); } return chatlist; } } </span>在Spring的配置文件里注入MAX_CHAT_HISTRORY
<span style="font-size:14px;"> <bean id="userChatController" class="com.test.chat.controller.UserChatController"> <property name="MAX_CHAT_HISTORY" value="20"/> </bean> </span>
当中缓存队列LimitQueue的实现为:
<span style="font-size:14px;">package com.test.chat.model; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; import java.util.Queue; public class LimitQueue<E> implements Queue<E> { private int limit; private Queue<E> queue; public LimitQueue(int limit) { this.limit = limit; this.queue = new LinkedList<E>(); } @Override public int size() { return queue.size(); } @Override public boolean isEmpty() { return queue.isEmpty(); } @Override public boolean contains(Object o) { return queue.contains(o); } @Override public Iterator<E> iterator() { return queue.iterator(); } @Override public Object[] toArray() { return queue.toArray(); } @Override public <T> T[] toArray(T[] a) { return queue.toArray(a); } @Override public boolean add(E e) { return queue.add(e); } @Override public boolean remove(Object o) { return queue.remove(0); } @Override public boolean containsAll(Collection<?> c) { return queue.containsAll(c); } @Override public boolean addAll(Collection<? extends E> c) { return queue.addAll(c); } @Override public boolean removeAll(Collection<?> c) { return queue.removeAll(c); } @Override public boolean retainAll(Collection<?> c) { return queue.retainAll(c); } @Override public void clear() { queue.clear(); } @Override public boolean offer(E e) { if (queue.size() >= limit) { queue.poll(); } return queue.offer(e); } @Override public E remove() { return queue.remove(); } @Override public E poll() { return queue.poll(); } @Override public E element() { return queue.element(); } @Override public E peek() { return queue.peek(); } public int getLimit() { return this.limit; } } </span>
四、前台聊天室的实现(前台界面使用dhtmlx控件)
<span style="font-size:14px;"><%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>聊天室管理</title> <meta http-equiv="content-type" content="text/html; charset=utf-8" /> <link rel="stylesheet" type="text/css" href="/dhtmlx/dhtmlxEditor/codebase/skins/dhtmlxeditor_dhx_skyblue.css"> <script src="/common/js/lib-base.js" type="text/javascript"></script> <script src="/dhtmlx/dhtmlxEditor/codebase/dhtmlxeditor.js" type="text/javascript"></script> <!-- <script src="/dhtmlx/dhtmlxEditor/codebase/ext/dhtmlxeditor_ext.js" type="text/javascript"></script> --> <!-- web chat 引入相关脚本 --> <script src="/common/js/websocket/sockjs-0.3.4.min.js" type="text/javascript"></script> <script src="/common/js/websocket/stomp.js" type="text/javascript"></script> <!----------------end-------------------> <script> var chatLayout; var roomid="${roomid}"; var roomName=null; var friendTree=null; var userid=null; var username=null; var deptSortName=null; var editor=null; $(function(){ commonForm.initForm(); chatLayout= new dhtmlXLayoutObject(document.body, "3J"); ajaxPost("/chatroom/findById",{"id":roomid},function(data,status){ chatLayout.cells("a").setText("<img src='/images/Pub/User.gif' width='16px' height='16px' align='absmiddle' style='margin-right:5px'>"+data.roomName); roomName=data.roomName; $("#roomRemark").html(data.remark); }) ; chatLayout.cells("a").hideHeader(); chatLayout.cells("a").attachObject("chatMsg"); chatLayout.cells("c").setHeight(150); chatLayout.cells("c").hideHeader(); chatLayout.setAutoSize("a;c","a;b"); chatLayout.cells("b").setWidth(180); var friendLayout=chatLayout.cells("b").attachLayout("2E"); friendTree=friendLayout.cells("b").attachTree(); friendLayout.cells("a").setText("<img src='/images/Pub/User.gif' width='16px' height='16px' align='absmiddle' style='margin-right:5px'>群公告"); friendLayout.cells("a").attachObject("roomRemark"); friendLayout.cells("a").setHeight(100); friendLayout.cells("b").setText("<img src='/images/Pub/User.gif' width='16px' height='16px' align='absmiddle' style='margin-right:5px'>好友列表"); friendLayout.setAutoSize("a;b","b"); //载入好友列表树 ajaxPost("/auth/getCurUser",null,function(data,status){ userid=data.id; username=data.name; deptSortName=data.deptSortName; }) loadChatFriend(); //载入聊天 var talkLayout=chatLayout.cells("c").attachLayout("2E"); talkLayout.cells("a").hideHeader(); talkLayout.cells("b").hideHeader(); talkLayout.cells("b").setHeight(29); //dhtmlx.image_path="/dhtmlx/dhtmlxEditor/codebase/imgs/"; editor=talkLayout.cells("a").attachEditor(); var toolbar=talkLayout.cells("b").attachToolbar(); toolbar.setIconsPath("/images/Pub/"); var tbindex=0; toolbar.addSeparator("sep1", tbindex++); toolbar.addSpacer("sep1"); toolbar.addButton("closeChat", tbindex++, "关闭", "delete.png","delte.png"); toolbar.addSeparator("sep2",tbindex++); toolbar.addButton("videoChat", tbindex++, "视频", "FrameReLogin.gif","FrameReLogin.gif"); toolbar.addSeparator("sep3",tbindex++); toolbar.addButton("sendMessage", tbindex++, "发送", "redo.gif","redo.gif"); toolbar.attachEvent("onclick",function(tid){ switch(tid){ case "sendMessage": if(editor.getContent()=="" ) return; sendMessage("0"); editor.setContent(""); break; case "closeChat": sendMessage("1","离开"); parent.dhxWins.window("chatWin").close(); break; case "videoChat": top.openWindow("/video/openVideoChat?五、实现效果roomid="+roomid,"videoWin","聊天室【"+roomName+"】",650,550,false,false,true); break; default: break; } }) }); function loadChatFriend(){ friendTree.setSkin('dhx_skyblue'); friendTree.setImagePath("/dhtmlx/dhtmlxTree/codebase/imgs/csh_dhx_skyblue/"); ajaxPost("/chatroom/getChatFriends",{"roomid":roomid},function(data,status){ friendTree.deleteChildItems(friendTree.rootId); $.each(data,function(index,item){ var id=item.user.id; var deptName=item.user.corg.shortName; var userName=item.user.name; var isCreator=item.isCreator; friendTree.insertNewItem(friendTree.rootId,id,deptName+"--"+userName+(isCreator=="1"?
"(群主)":""),0,0,0,0,""); if(userid==id) friendTree.setItemColor(id,"red",""); }) }) } //---------------------------------------聊天室关键代码(websocket)--------------------------------------- var stompClient=null;content=null; $(function(){ connect(); }) //connect the server function connect(){ var socket=new SockJS("/webchat"); stompClient=Stomp.over(socket); stompClient.connect('','',function(frame){ console.log('Connected: '+frame); //用户聊天订阅 //alert("hello: "+frame); stompClient.subscribe("/userChat/chat"+roomid,function(chat){ showChat(JSON.parse(chat.body)); }); //初始化 stompClient.subscribe("/app/initChat/"+roomid,function(initData){ //alert("初始化聊天室"); console.log(initData); content=JSON.parse(initData.body); //content=body.document.content; //alert(content+":"+content.document.content); content.forEach(function(item){ showChat(item); }); sendMessage("1","进入"); }); },function(){ connect(); }); } //显示聊天信息 function showChat(message){ var htmlMsg=decodeURIComponent(message.chatContent); var image="<img src='/images/Pub/User.gif' width='16px' height='16px' align='absmiddle'/>"; var userMsg=decodeURIComponent(message.deptName) +"--"+decodeURIComponent(message.userName)+" "+decodeURIComponent(message.curTime)+"</font>"; htmlMsg=userMsg+"<br/> "+htmlMsg; if(htmlMsg!="") { if($("#chatMsg").html()!=""){ if(message.isSysMsg=="1") $("#chatMsg").append("<br/><div style='text-align:center'><font color='gray'>"+htmlMsg+"</div>"); else $("#chatMsg").append("<br/>"+image+"<font color='blue'>"+htmlMsg); } else { if(message.isSysMsg=="1") $("#chatMsg").append("<div style='text-align:center'><font color='gray'>"+htmlMsg+"</div>"); else $("#chatMsg").append(image+"<font color='blue'>"+htmlMsg); } $("#chatMsg")[0].scrollTop=$("#chatMsg")[0].scrollHeight; } } function sendMessage(isSysMsg,textMsg){ var chatCont=editor.getContent(); if(isSysMsg=="1"){ chatCont="<font color='gray'>"+textMsg+"聊天室</font>"; } stompClient.send("/app/userChat",{},JSON.stringify({ 'roomid':encodeURIComponent(roomid), 'userName':encodeURIComponent(username), 'deptName':encodeURIComponent(deptSortName), 'chatContent':encodeURIComponent(chatCont), 'isSysMsg':encodeURIComponent(isSysMsg) })) } //--------------------------------------------------------------------------------------------------------------- </script> </head> <body style="100%;height:100%;margin:0px;overflow:hidden;"> <div id="roomRemark"></div> <div style="position:relative;99%;height:100%;overflow:auto;display:none;margin-left:5px;" id="chatMsg"></div> </body> </html> </span>
创建人tester4进入后,输入聊天内容后,退出。
聊天室好友tester1进入并发言
文中代码部分參考了Spring WebSocket教程(一)和Spring
WebSocket教程(二)。
版权声明:本文博主原创文章。博客,未经同意不得转载。