一.背景
目前管理的一个应用系统中,原有的消息机制是通过ajax轮询来进行的,一方面效率不高,再一个消息产生和消费的时候,系统通知也会有延迟,造成用户体验并不是很好。基于这一背景,对应用系统的消息通知机制进行了改造,使用websocket来实时进行消息的通知。
spring和spring mvc环境的搭建就不讲了,这里主要讲怎样把spring websocket整合到spring mvc web工程中,并解决遇到的问题。
二. 获取spring websocket依赖包
这里,使用maven来进行依赖包的管理,只需要在pom.xml里添加以下依赖包配置信息即可:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
三. 服务端Websocket程序编写
编写MyWebSocketConfig实现WebSocketConfigurer接口:
package cn.xxx.rmwlgzpt.websocket;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
/**
* websocket配置
*
* @author huangyuanmu
* @date 2018年6月13日 下午3:35:35
* @version 1.0
*/
@Configuration
@EnableWebMvc
@EnableWebSocket
public class MyWebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketHandler(),"/websocket").addInterceptors(new MyWebSocketHandlerInterceptor());
registry.addHandler(webSocketHandler(), "/sockjs").addInterceptors(new MyWebSocketHandlerInterceptor()).withSockJS();
}
@Bean
public WebSocketHandler webSocketHandler() {
return new MyWebSocketHandler();
}
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
return container;
}
}
因为并不是所有的浏览器都实现了websocket,所以这里也设置拦截器,对socketjs进行支持。
编写MyWebSocketHandlerInterceptor拦截器,处理websocket session相关事项:
package cn.xxx.rmwlgzpt.websocket;
import java.util.Map;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import cn.xxx.util.common.CommonUtil;
/**
* 拦截器
*
* @author huangyuanmu
* @date 2018年6月13日 下午3:53:42
* @version 1.0
*/
public class MyWebSocketHandlerInterceptor extends HttpSessionHandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
String user = CommonUtil.getSpringSecurityUser();
if(CommonUtil.isNotEmpty(user)) {
attributes.put("WEBSOCKET_USERNAME", user);
}
return super.beforeHandshake(request, response, wsHandler, attributes);
}
}
编写websocket消息处理类MyWebSocketHandler:
package cn.xxx.rmwlgzpt.websocket;
import java.io.IOException;
import java.util.ArrayList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
/**
* 消息处理
*
* @author huangyuanmu
* @date 2018年6月13日 下午3:37:36
* @version 1.0
*/
public class MyWebSocketHandler extends TextWebSocketHandler {
private final static Logger log = LoggerFactory.getLogger(MyWebSocketHandler.class);
private static final ArrayList<WebSocketSession> users;
static {
users = new ArrayList<WebSocketSession>();
}
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
users.add(session);
}
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
users.remove(session);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
super.handleTextMessage(session, message);
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
if (session.isOpen()) {
session.close();
}
users.remove(session);
}
@Override
public boolean supportsPartialMessages() {
return false;
}
/**
* 发送消息给某个用户
*
* @author huangyuanmu
* @date 2018年6月13日 下午4:03:58
* @param userName
* @param message
*/
public void sendMessageToUser(String userName, TextMessage message) {
for (WebSocketSession user : users) {
if (user.getAttributes().get("WEBSOCKET_USERNAME").equals(userName)) {
try {
if (user.isOpen()) {
user.sendMessage(message);
}
} catch (IOException e) {
log.error("发送消息出错!", e);
}
break;
}
}
}
}
因为在我的应用场景下,只需要websocket服务端发送消息,所以仅实现了发送消息的代码。接收消息的代码略去。
应用场景中,进行消息发送:
package cn.xxx.rmwlgzpt.web.shell.service;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.annotation.Resource;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import cn.tohot.psyco.web.base.domain.SqlObj;
import cn.tohot.psyco.web.base.service.CommonCrudService;
import cn.tohot.rmwlgzpt.websocket.MyWebSocketHandler;
import cn.tohot.swim.comm.PageData;
import cn.tohot.swim.spring.dao.SqlDAO;
import cn.tohot.util.common.CommonUtil;
import cn.tohot.util.exception.LazyAppException;
/**
* 消息查询的服务类
*
* @author huangyuanmu
* @date 2015年9月22日 上午10:06:31
* @version 1.0
*/
@Service
public class SysAlertService extends CommonCrudService {
@Bean
public MyWebSocketHandler webSocketHandler() {
return new MyWebSocketHandler();
}
/**
* 发消息通知客户端
*
* @author huangyuanmu
* @date 2018年6月13日 下午4:46:59
*/
private void notifyClient() {
// 发websocket消息通知客户端
webSocketHandler().sendMessageToUser(CommonUtil.getSpringSecurityUser(), new TextMessage("Alerts updated."));
}
/** * 产生消息 * * @author huangyuanmu * @date 2015年11月7日 下午4:13:53 * @param title * 消息标题 * @param content * 消息内容 * @param bizType * 消息业务类型 * @param bizId * 消息业务id * @param receivers * 消息的接受者 * @param type * 消息类型 */public void generateAlertSave(String title, String content, String bizType, String bizId, List receivers, String type) {
// 业务代码略
// 发消息通知客户端
notifyClient();
}
/**
* 处理消息
*
* @author huangyuanmu
* @date 2015年11月7日 下午4:29:51
* @param bizType
* 消息业务类型
* @param bizId
* 业务id
* @param userId
* 用户id
*/
public void dealAlertSave(String bizType, String bizId, String userId) {
// 业务代码略
// 发消息通知客户端
notifyClient();
}
}
四. 客户端代码编写
var options = {};
options.url = "desktop";
options.serviceName = "desktopService";
options.methodName = "getWsUrl";
options.isMask = false;
options.func = function(data) {
var websocket = null;
if ('WebSocket' in window) {
websocket = new WebSocket(data.wsUrl);
}
else if ('MozWebSocket' in window) {
websocket = new MozWebSocket(data.wsUrl);
} else {
websocket = new SockJS(data.wsJsUrl);
}
websocket.onopen = onOpen;
websocket.onmessage = onMessage;
websocket.onerror = onError;
websocket.onclose = onClose;
function onOpen(openEvt) {}
// 接收websocket消息,更新系统消息显示
function onMessage(evt) {
$.head.loadAlerts();
}
function onError() {}
function onClose() {}
}
$.serviceCall(options);
因为业务和技术上做了一定的封装,我也没有必要将全部代码列出,所以这里的代码看起来不是那么清晰,但是大体流程是清楚的,可供在spring mvc和spring websocket的整合过程中参考。
五. 写在最后
按照以上流程,应该就可以实现spring websocket的成功整合了,但还有最后一个问题需要解决。在我的整合过程中,发生了ajax调用中文乱码的问题,话费了不少时间。究其原因,关键的地方在于要在spring mvc的配置文件中增加一些配置:
<bean
class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="messageConverters">
<list>
<ref bean="stringHttpMessageConverter" />
</list>
</property>
</bean>
<bean id="stringHttpMessageConverter"
class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/plain;charset=UTF-8</value>
</list>
</property>
</bean>
<!-- 增加上边的配置 -->
<context:component-scan base-package="cn.xxx.rmwlgzpt.web">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service" />
</context:component-scan>
<context:component-scan base-package="cn.xxx.rmwlgzpt.websocket"/>
<task:annotation-driven/>