一.背景
目前管理的一个应用系统中,原有的消息机制是通过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/>