如本文所用,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="width:100%;height:100%;margin:0px;overflow:hidden;"> <div id="roomRemark"></div> <div style="position:relative;width:99%;height:100%;overflow:auto;display:none;margin-left:5px;" id="chatMsg"></div> </body> </html> </span>
五、实现效果创建人tester4进入后,输入聊天内容后,退出。
聊天室好友tester1进入并发言
文中代码部分參考了Spring WebSocket教程(一)和Spring WebSocket教程(二)。
版权声明:本文博主原创文章。博客,未经同意不得转载。