引入spring-websocket包
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${websocket.version}</version>
</dependency>
</dependencies>
1.创建聊天记录信实体类MessageLog
package com.bjhy.ven.domain;
import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import org.springframework.format.annotation.DateTimeFormat;
/**
* 聊天记录信息表
* @author xiaowen
*
*/
@Entity
@Table(name = "t_message")
public class MessageLog {
//id
@Id
private String id;
//发送的消息
private String msg;
// 发送者
@ManyToOne
@JoinColumn(name = "fromId")
private SysUser from;
//接收者
@ManyToOne
@JoinColumn(name = "toId")
private SysUser to;
//发送时间
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
private Date createDate;
/**
* @return the id
*/
public String getId() {
return id;
}
/**
* @param id the id to set
*/
public void setId(String id) {
this.id = id;
}
/**
* @return the msg
*/
public String getMsg() {
return msg;
}
/**
* @param msg the msg to set
*/
public void setMsg(String msg) {
this.msg = msg;
}
/**
* @return the from
*/
public SysUser getFrom() {
return from;
}
/**
* @param from the from to set
*/
public void setFrom(SysUser from) {
this.from = from;
}
/**
* @return the to
*/
public SysUser getTo() {
return to;
}
/**
* @param to the to to set
*/
public void setTo(SysUser to) {
this.to = to;
}
/**
* @return the createDate
*/
public Date getCreateDate() {
return createDate;
}
/**
* @param createDate the createDate to set
*/
public void setCreateDate(Date createDate) {
this.createDate = createDate;
}
}
2.创建消息服务接口MessageService
package com.bjhy.ven.service;
import com.bjhy.ven.biz.commons.service.BizCommonService;
import com.bjhy.ven.domain.MessageLog;
public interface MessageService extends BizCommonService<MessageLog, String>{
/**
* 保存message
*/
public void saveAndSendMessage(String from,String to,String message);
}
BizCommonService是公共的业务处理接口
package com.bjhy.ven.biz.commons.service;
import java.io.Serializable;
import java.util.List;
import org.springframework.data.domain.Sort;
import com.bjhy.platform.commons.pager.Condition;
import com.bjhy.platform.commons.pager.PageBean;
/**
*
* 公用业务接口
* @author wbw
*
* @param <T> 实体对象
* @param <PK> 实体对象id
*/
public interface BizCommonService<T,PK extends Serializable> {
/**
* 根据对象的ID查询对象信息
* @param id
* 对象ID
* @return
* 返回查询的对象
*/
public T findById(PK id);
/**
* 查询所有数据
*/
public List<T> findAll(Sort sort);
/**
* 根据条件查询数据
* @param conditions 条件对象
*/
public List<T> findByCondition(Condition... conditions);
/**
* 保存一个对象
* @param entity
* 保存的对象
*/
public PK save(T entity);
/**
* 修改一个对象
* @param entity
* 修改的对象
*/
public void update(T entity);
/**
* 保存或者修改一个对象
* @param entity
* 保存或者修改的对象
* @return
* 返回保存或者修改的对象的ID
*/
public PK saveOrUpdate(T entity);
/**
* 根据ID删除一个或者多个对象
* @param ids
* 删除的对象ID数组
* @throws Exception
*/
@SuppressWarnings("unchecked")
public void deleteById(PK... ids);
/**
* 分页查询
* @param pageBean 分页对象
*/
void pageQuery(PageBean pageBean);
}
3.实现消息服务接口MessageServiceImpl
package com.bjhy.ven.service.impl;
import java.util.Date;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.bjhy.platform.persist.dao.CommonRepository;
import com.bjhy.ven.biz.commons.service.impl.AbstractBizCommonService;
import com.bjhy.ven.dao.MesasgeRepository;
import com.bjhy.ven.domain.MessageLog;
import com.bjhy.ven.domain.SysUser;
import com.bjhy.ven.service.MessageService;
import com.bjhy.ven.service.SysUserService;
@Service
@Transactional
public class MessageServiceImpl extends AbstractBizCommonService<MessageLog, String> implements MessageService {
// 基础查询ql
private static final String BASE_FIELD_QUERY = "select m.id,m.msg,m.createDate,fromer.userName,toer.userName from MessageLog m left join m.from fromer left join m.to toer where 1=1 ";
@Autowired
private MesasgeRepository mesasgeRepository;
@Autowired
private SysUserService userService;
@Autowired
private SimpMessagingTemplate template;
@Override
protected CommonRepository<MessageLog, String> getRepository() {
return mesasgeRepository;
}
@Override
protected String getPageQl() {
return BASE_FIELD_QUERY;
}
@Override
public void saveAndSendMessage(String from, String to, String message) {
String convertMessage ="{from:'"+from+"',to:'"+to+"',message:'"+message+"'}";
template.convertAndSend("/topic/"+to,convertMessage);
//判断非空
SysUser userFrom = userService.findByUserName(from);
SysUser userTo = userService.findByUserName(from);
MessageLog msg = new MessageLog();
msg.setFrom(userFrom);
msg.setTo(userTo);
msg.setMsg(message);
msg.setCreateDate(new Date());
this.save(msg);
}
}
AbstractBizCommonService抽象的公共业务实现接口
package com.bjhy.ven.biz.commons.service.impl;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import com.bjhy.platform.commons.exception.PlatformException;
import com.bjhy.platform.commons.pager.Condition;
import com.bjhy.platform.commons.pager.Order;
import com.bjhy.platform.commons.pager.PageBean;
import com.bjhy.platform.persist.dao.CommonRepository;
import com.bjhy.platform.util.BeanUtils;
import com.bjhy.ven.biz.commons.service.BizCommonService;
/**
* 公用业务接口抽象实现类
* @author wbw
*/
@Component
@Transactional
public abstract class AbstractBizCommonService<T,PK extends Serializable> implements BizCommonService<T, PK>{
@SuppressWarnings("unchecked")
protected Class<T> entityClass = (Class<T>)((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[0];
/**
* 返回实现repository接口的实例对象
*/
protected abstract CommonRepository<T, PK> getRepository();
/**
* 返回分页ql语句
*/
protected abstract String getPageQl();
@Override
public T findById(PK id) {
return getRepository().findOne(id);
}
@Override
public List<T> findAll(Sort sort){
return getRepository().findAll(sort);
}
@SuppressWarnings("unchecked")
@Override
public List<T> findByCondition(Condition... conditions){
return (List<T>)getRepository().doList(getPageQl(), Arrays.asList(conditions), new ArrayList<Order>(), false);
}
@SuppressWarnings("unchecked")
@Override
public PK save(T entity) {
PK id = null;
try {
//判断是否有创建时间字段
try {
Field createDateField = entity.getClass().getDeclaredField("createDate");
if(createDateField != null){
createDateField.setAccessible(true);
createDateField.set(entity, new Date());
}
} catch (NoSuchFieldException e) {}
getRepository().store(entity);
Field field = entity.getClass().getDeclaredField("id");
field.setAccessible(true);
id = (PK)field.get(entity);
}catch (Exception e) {
e.printStackTrace();
throw new PlatformException(e.getMessage());
}
return id;
}
@SuppressWarnings("unchecked")
@Override
public void update(T entity) {
try {
Field field = entity.getClass().getDeclaredField("id");
field.setAccessible(true);
PK id = (PK)field.get(entity);
T sourceEntity = getRepository().findOne(id);
BeanUtils.copyNotNullProperties(entity, sourceEntity);
getRepository().update(sourceEntity);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
@Override
public PK saveOrUpdate(T entity) {
try {
Field field = entity.getClass().getDeclaredField("id");
field.setAccessible(true);
PK id = (PK)field.get(entity);
if(StringUtils.isEmpty(id)){
return save(entity);
}else{
update(entity);
return id;
}
} catch (Exception e) {
e.printStackTrace();
throw new PlatformException(e.getMessage());
}
}
@SuppressWarnings("unchecked")
@Override
public void deleteById(PK... ids) {
for (PK pk : ids) {
getRepository().delete(pk);
}
}
@Override
public void pageQuery(PageBean pageBean) {
getRepository().doPager(pageBean, getPageQl());
}
}
4.创建消息dao,MesasgeRepository 这里的orm框架使用的是Spring Data JPA
package com.bjhy.ven.dao;
import com.bjhy.platform.persist.dao.CommonRepository;
import com.bjhy.ven.domain.MessageLog;
public interface MesasgeRepository extends CommonRepository<MessageLog, String> {
}
CommonRepository是自定义的repository方法接口
package com.bjhy.platform.persist.dao;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.NoRepositoryBean;
import com.bjhy.platform.commons.pager.Condition;
import com.bjhy.platform.commons.pager.Order;
import com.bjhy.platform.commons.pager.PageBean;
/**
*
* 自定义repository的方法接口
*/
@NoRepositoryBean
public interface CommonRepository<T, ID extends Serializable> extends JpaRepository<T, ID>{
/**
* 保存对象<br/>
* 注意:如果对象id是字符串,并且没有赋值,该方法将自动设置为uuid值
* @param item
* 持久对象,或者对象集合
* @throws Exception
*/
public void store(Object... item);
/**
* 更新对象数据
*
* @param item
* 持久对象,或者对象集合
* @throws Exception
*/
public void update(Object... item);
/**
* 执行ql语句
* @param qlString 基于jpa标准的ql语句
* @param values ql中的?参数值,单个参数值或者多个参数值
* @return 返回执行后受影响的数据个数
*/
public int executeUpdate(String qlString, Object... values);
/**
* 执行ql语句
* @param qlString 基于jpa标准的ql语句
* @param params key表示ql中参数变量名,value表示该参数变量值
* @return 返回执行后受影响的数据个数
*/
public int executeUpdate(String qlString, Map<String, Object> params);
/**
* 执行ql语句,可以是更新或者删除操作
* @param qlString 基于jpa标准的ql语句
* @param values ql中的?参数值
* @return 返回执行后受影响的数据个数
* @throws Exception
*/
public int executeUpdate(String qlString, List<Object> values);
/**
* 结合提供的分页信息,获取指定条件下的数据对象
* @param pageBean 分页信息
* @param qlString 基于jpa标准的ql语句
*/
public void doPager(PageBean pageBean, String qlString);
/**
* 结合提供的分页信息,获取指定条件下的数据对象
* @param pageBean 分页信息
* @param qlString 基于jpa标准的ql语句
* @param cacheable 是否启用缓存查询
*/
public void doPager(PageBean pageBean,String qlString,boolean cacheable);
/**
* 结合提供的分页信息,获取指定条件下的数据对象
* @param pageBean 分页信息
* @param qlString 基于jpa标准的ql语句
* @param params key表示ql中参数变量名,value表示该参数变量值
*/
public void doPager(PageBean pageBean, String qlString,
Map<String, Object> params);
/**
* 结合提供的分页信息,获取指定条件下的数据对象
* @param pageBean 分页信息
* @param qlString 基于jpa标准的ql语句
* @param values ql中的?参数值
*/
public void doPager(PageBean pageBean, String qlString, List<Object> values);
/**
* 结合提供的分页信息,获取指定条件下的数据对象
* @param pageBean 分页信息
* @param qlString 基于jpa标准的ql语句
* @param values ql中的?参数值
*/
public void doPager(PageBean pageBean, String qlString, Object... values);
/**
* 批量删除数据对象
* @param entityClass
* @param primaryKeyValues
* @return
*/
public int batchDeleteByQl(Class<?> entityClass,Object...primaryKeyValues);
/**
* 批量删除数据对象
* @param entityClass
* @param pKeyVals 主键是字符串形式的
* @return
* @throws Exception
*/
public int batchDeleteByQl(Class<?> entityClass,String...pKeyVals);
/**
* @param qlString 查询hql语句
* @param values hql参数值
* @param conditions 查询条件
* @param orders 排序条件
* @return
*/
public List<?> doList(String qlString, List<Object> values, List<Condition> conditions, List<Order> orders, boolean sqlable);
public List<?> doList(String qlString, List<Condition> conditions, List<Order> orders, boolean sqlable);
}
此处省略该接口的实现类CommonRepositoryImpl
5.消息服务前端控制器MessageController
package com.bjhy.ven.controller.messager;
import java.util.List;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.bjhy.ven.domain.SysUser;
import com.bjhy.ven.service.MessageService;
/**
* webSocket 发送接收消息controller
* @author xiaowen
*
*/
import com.bjhy.ven.service.SysUserService;
@Controller
@RequestMapping("/message")
public class MessageController {
@Autowired
private MessageService messageService;
@Autowired
private SysUserService sysUserService;
private static final String MESSAGE_INDEX="message/message";
private static final String STAUTS="ok";
/**
* 请求首页
* @return
*/
@RequestMapping("/index")
public String index(){
return MESSAGE_INDEX;
}
/**
* 发送消息
* @param from 发送消息者
* @param to 接受消息者
* @param message 消息
* @return
*/
@RequestMapping("/sendMessage")
public @ResponseBody String recieveMessage(String from,String to,String message){
messageService.saveAndSendMessage(from, to, message);
return STAUTS;
}
/**
* 获取在线用户
* @return
*/
@RequestMapping("/onlineUsers")
public @ResponseBody List<SysUser> onlineUsers(HttpSession session){
String sessionUserName = (String) session.getAttribute("username");
return sysUserService.findUserByUserName(sessionUserName);
}
}
6.创建聊天页面message.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>聊天</title>
${static_context}${appJs}${jquery}${toast}${bootstrap}${socketJS}
<script type="text/javascript" src="${request.contextPath}/js/message/message.js"></script>
</head>
<body>
<input type="hidden" value="${username}" id="username" />
<input type="hidden" id="to"/>
<div style="margin-bottom:20px"></div>
<div class="container">
<div class="row">
<!-- 在线用户列表 -->
<div class="col-md-3">
<div class="list-group">
<ul class="list-group" id="online">
<li class="list-group-item active">在线用户</li>
</ul>
</div>
</div>
<!-- 聊天窗体-->
<div class="col-md-9" id="chatwindows">
</div>
</div>
<!-- 发送消息文本 -->
<div class="row" style="display:none" id="send_text">
<div class="col-md-2"></div>
<div class="col-md-1"></div>
<div class="col-md-9" >
<textarea id="messgeContext" class="form-control" rows="3" placeholder="按下回车键发送消息...."></textarea>
</div>
</div>
<!-- 发送按钮 -->
<div class="row" style="display:none" id="execute_send">
<div class="col-md-2"></div>
<div class="col-md-4">
</div>
<div class="col-md-5" style="margin-top:5px;">
<button type="button" class="btn btn-danger btn_lg" id="btn_close" >关闭</button>
<button type="button" class="btn btn-danger btn_lg" id="btn_send" >发送</button>
</div>
</div>
</div>
</body>
</html>
特别注意此处需要用到socket.js、stomp.js
页面的呈现效果
6.创建message.js处理消息动作
var messageArr = {};
$(function(){
mseeage.init();
bindEvent.init();
});
var bindEvent = {
//初始化
init :function(){
this.sendEvent();
},
//渲染视图
viewEvent: function(){
$(".list-group-item").not(":eq(0)").click(function(){
var name = $(this).attr("value");
$(".panel-primary").hide();
$("#"+name).show();
$("#to").val(name);
messageArr[name] = null;
$(".list-group-item[value='" + name + "']").find("span").html("");
$("#send_text").show();
$("#execute_send").show();
});
},
//发送消息
sendEvent : function(){
//发送
$('#btn_send').click(function(){
mseeage.snedMessage();
});
//关闭
$('#btn_close').click(function(){
$(".panel-primary").remove();
$("#send_text").hide();
$("#execute_send").hide();
});
//enter事件
$(window).keydown(function(event){
if(event.keyCode ==13){
mseeage.snedMessage();
}
});
}
}
var mseeage ={
init : function(){
this.initOnlineUsers();
this.initSocket();
},
//初始化在线用户
initOnlineUsers : function(){
$.ajax({
url:contextPath + "/message/onlineUsers",
success : function(data){
var $html =""
var $content = "";
for (var i = 0; i < data.length; i++) {
var userName = data[i].userName;
var alias = data[i].alias;
$html +=" <li class='list-group-item' value='"+alias+"'>"+userName+"<span class='badge'></span></li>"
$content +="<div class='panel panel-primary' style='height:400px;display:none' id='"+alias+"'>" +
" <div class='panel-heading'>聊天栏--<span>"+userName+"</span></div>" +
"<div id='tolk' class='panel-body'></div></div>";
}
$('#chatwindows').append($content);
$("#online").not("li:eq(0)").append($html);
//绑定点击事件
bindEvent.viewEvent();
}
})
},
//初始化
initSocket : function(){
var userName = $("#username").val();
var socket = new SockJS(contextPath + "/" + wsEndPoint);
var stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
stompClient.subscribe('/topic/'+userName, function(greeting){
var result=eval("("+greeting.body+")");
console.log(result);
var msg_="<br>"+result.from+"说:"+result.message;
$("#"+result.from).find("div:eq(1)").append(msg_);
if($('#to').val() != result.from){
if(messageArr[result.from]){
messageArr[result.from]++;
}else{
messageArr[result.from] = 1;
}
$(".list-group-item[value='" + result.from + "']").find("span").html(messageArr[result.from]);
}
});
});
},
//发送消息
snedMessage : function(){
var messge = $("#messgeContext").val();
var from = $("#username").val();
var to = $("#to").val();
if(messge.trim().length>0 && $("#to").val()){
var data = {
from : from,
to :to,
message : messge
}
$.ajax({
url:contextPath + "/message/sendMessage",
data: data,
success : function(data){
$("#messgeContext").val("");
}
})
}else{
toastr.warning("请输入消息!");
}
}
};
关于websocket配置
1.注册socket.js、stomp.js资源,实现ServletContextAware接口,在页面上通过${socketJs}就可以引用js
package com.bjhy.platform.websocket.client;
import javax.servlet.ServletContext;
import org.springframework.stereotype.Component;
import org.springframework.web.context.ServletContextAware;
@Component
public class RegistrySocketJS implements ServletContextAware{
public final static String GLOBAL_ENDPOINT = "platform-ws";
private final static String SOCKET_JS = "socketJS";
@Override
public void setServletContext(ServletContext servletContext) {
String contextPath = servletContext.getContextPath();
servletContext.setAttribute(SOCKET_JS,buildSocketJSResource(contextPath));
}
private String buildSocketJSResource(String contextPath){
String socketGloablJS = "<script type=\"text/javascript\" >var wsEndPoint = '" + GLOBAL_ENDPOINT + "';</script>";
String socketJS = "<script type=\"text/javascript\" src=\"" + contextPath + "/websocket/js/sockjs.min.js\"></script>";
String stompJS = "<script type=\"text/javascript\" src=\"" + contextPath + "/websocket/js/stomp.min.js\"></script>";
return socketGloablJS + socketJS + stompJS;
}
}
2.配置websocket消息中间件
package com.bjhy.platform.websocket.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/platform-ws").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
registry.enableSimpleBroker("/topic", "/quene");
}
}
这个可以单独抽离成一个子模块paltform-websocket如下图:
7.测试