前言
最近專案中有個私信功能,需要用到websocket,於是在網上找找資料並在實踐中總結了一點經驗分享給大家。
問題
在操作之前我先拋一個問題:
SpringBoot專案整合 webSocket,當客戶端與伺服器端建立連線的時候,發現 server物件並未注入而是為 null。
產生原因:spring管理的都是單例(singleton),和 websocket (多物件)相沖突。
詳細解釋:專案啟動時初始化,會初始化 websocket (非使用者連線的),spring 同時會為其注入 service,該物件的 service 不是 null,被成功注入。但是,由於 spring 預設管理的是單例,所以只會注入一次 service。當客戶端與伺服器端進行連線時,伺服器端又會建立一個新的 websocket 物件,這時問題出現了:spring 管理的都是單例,不會給第二個 websocket 物件注入 service,所以導致只要是使用者連線建立的 websocket 物件,都不能再注入了。
像 controller 裡面有 service, service 裡面有 dao。因為 controller,service ,dao 都有是單例,所以注入時不會報 null。但是 websocket 不是單例,所以使用spring注入一次後,後面的物件就不會再注入了,會報NullException。
下面會講解決辦法。
操作
1、引入websocket依賴包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2、配置websocket
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
// webSocket通道
// 指定處理器和路徑,如:http://www.baidu.com/service-name/websocket?uid=xxxx
webSocketHandlerRegistry.addHandler(new WebSocketHandler(), "/websocket")
// // 指定自定義攔截器
.addInterceptors(new WebSocketInterceptor())
// 允許跨域
.setAllowedOrigins("*");
}
}
3、新增獲取websocket地址中的引數類
public class WebSocketInterceptor implements HandshakeInterceptor {
/**
* handler處理前呼叫,attributes屬性最終在WebSocketSession裡,可能通過webSocketSession.getAttributes().get(key值)獲得
*/
@Override
public boolean beforeHandshake(org.springframework.http.server.ServerHttpRequest request, ServerHttpResponse serverHttpResponse, org.springframework.web.socket.WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest serverHttpRequest = (ServletServerHttpRequest) request;
// 獲取請求路徑攜帶的引數
String uid = serverHttpRequest.getServletRequest().getParameter("uid");
map.put("uid", uid);
return true;
} else {
return false;
}
}
@Override
public void afterHandshake(org.springframework.http.server.ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, org.springframework.web.socket.WebSocketHandler webSocketHandler, Exception e) {
}
}
4、新增解決server物件@Autowired注入為null的類
@Component
public class SpringContext implements ApplicationContextAware {
/**
* 列印日誌
*/
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 獲取上下文物件
*/
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContext.applicationContext = applicationContext;
logger.info("set applicationContext");
}
/**
* 獲取 applicationContext
*
* @return
*/
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
/**
* 通過 name 獲取 bean 物件
*
* @param name
* @return
*/
public static Object getBean(String name) {
return getApplicationContext().getBean(name);
}
/**
* 通過 class 獲取 bean 物件
*
* @param clazz
* @param <T>
* @return
*/
public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}
/**
* 通過 name,clazz 獲取指定的 bean 物件
*
* @param name
* @param clazz
* @param <T>
* @return
*/
public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
}
5、新增websocket接收傳送訊息類
@Component
public class WebSocketHandler extends AbstractWebSocketHandler {
private static Logger log = LoggerFactory.getLogger(WebSocketHandler.class);
public AccountFeignClient getAccountFeignClient() {
return SpringContext.getBean(AccountFeignClient.class);
}
public NotifyMailboxService getNotifyMailboxService() {
return SpringContext.getBean(NotifyMailboxService.class);
}
public NotifyMailboxMessageService getNotifyMailboxMessageService() {
return SpringContext.getBean(NotifyMailboxMessageService.class);
}
/**
* 儲存sessionId和webSocketSession
* 需要注意的是,webSocketSession沒有提供無參構造,不能進行序列化,也就不能通過redis儲存
* 在分散式系統中,要想別的辦法實現webSocketSession共享
*/
private static Map<String, WebSocketSession> sessionMap = new ConcurrentHashMap<>();
private static Map<String, String> userMap = new ConcurrentHashMap<>();
/**
* webSocket連線建立後呼叫
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) {
// 獲取引數
String uid = String.valueOf(session.getAttributes().get("uid"));
String sessionId = session.getId();
log.info("init websocket uid={},sessionId={}", uid, sessionId);
userMap.put(uid, sessionId);
sessionMap.put(sessionId, session);
}
/**
* 前端傳送訊息到後臺
* 接收到訊息會呼叫
*/
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
//A使用者傳送前端訊息到後臺,後臺要儲存A訊息,並且向B使用者推送訊息
if (message instanceof TextMessage) {
log.info("message={}", message);
} else if (message instanceof BinaryMessage) {
} else if (message instanceof PongMessage) {
} else {
log.info("Unexpected WebSocket message type: " + message);
}
String uid = String.valueOf(session.getAttributes().get("uid"));
String messages = (String) message.getPayload();
ObjectMapper mapper = new ObjectMapper();
HashMap<String, Object> map = mapper.readValue(messages, HashMap.class);
String _uid = (String) map.get("uid");
// String _dialogId = (String) map.get("dialogId");
String _friendId = (String) map.get("friendId");
String _message = (String) map.get("message");
String sessionId = session.getId();
log.info("sessionId={},uid={},_uid={},_friendId={},_message={}", sessionId, uid, _uid, _friendId, _message);
if (!StringUtils.hasLength(sessionId) || !StringUtils.hasLength(_uid) || !StringUtils.hasLength(_friendId)) {
log.info("sessionId&_uid&_friendId不能為空");
session.sendMessage(new TextMessage("error:sessionId&_uid&_friendId不能為空"));
return;
}
String dialogId = pushMessage(_uid, _friendId, _message);
if (dialogId != null) {
TextMessage textMessage = new TextMessage("dialogId:" + dialogId);
// 向自己的ws推送訊息
session.sendMessage(textMessage);
String sessionIdForFriend = userMap.get(_friendId);
log.info("sessionIdForFriend={}", sessionIdForFriend);
if (StringUtils.hasLength(sessionIdForFriend)) {
WebSocketSession friendSession = sessionMap.get(sessionIdForFriend);
if (friendSession != null && friendSession.isOpen())
// 向朋友推送訊息
friendSession.sendMessage(textMessage);
}
}
}
/**
* 連線出錯會呼叫
*/
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) {
String uid = String.valueOf(session.getAttributes().get("uid"));
String sessionId = session.getId();
log.info("CLOSED uid= ={},sessionId={}", uid, sessionId);
sessionMap.remove(sessionId);
}
/**
* 連線關閉會呼叫
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
String uid = String.valueOf(session.getAttributes().get("uid"));
String sessionId = session.getId();
log.info("CLOSED uid= ={},sessionId={}", uid, sessionId);
sessionMap.remove(sessionId);
}
@Override
public boolean supportsPartialMessages() {
return false;
}
/**
* 後臺傳送訊息到前端
* 封裝方法傳送訊息到客戶端
*/
public static void sendMessage(String uid, String dialogId) {
log.info("傳送訊息到:");
}
/**
* @param uid
* @param friendId
* @param message
* @return
*/
private String pushMessage(String uid, String friendId, String message) {
log.info("uid={},friendId={},message={}", uid, friendId, message);
NotifyMailboxService notifyMailboxService = getNotifyMailboxService();
NotifyMailboxMessageService notifyMailboxMessageService = getNotifyMailboxMessageService();
try {
NotifyMailbox notifyMailbox = notifyMailboxService.queryBy(uid, friendId);
} catch (Exception e) {
log.info("exception msg={}", e.getMessage());
return null;
}
}
}
websocket前端狀態碼readyState
0 CONNECTING 連線尚未建立
1 OPEN WebSocket的連結已經建立
2 CLOSING 連線正在關閉
3 CLOSED 連線已經關閉或不可用
總結
1、server物件並未注入而是為 null,所以要通過新增上面的SpringContext類,並通過下面這種方式引用
public AccountFeignClient getAccountFeignClient() {
return SpringContext.getBean(AccountFeignClient.class);
}
public NotifyMailboxService getNotifyMailboxService() {
return SpringContext.getBean(NotifyMailboxService.class);
}
public NotifyMailboxMessageService getNotifyMailboxMessageService() {
return SpringContext.getBean(NotifyMailboxMessageService.class);
}
2、websocket的連線和關閉的session對應問題,使用上面程式碼就沒問題,否則會出現連線上的問題。
3、WebSocketInterceptor
會獲取https://www.baidu.com/service-name/websocket?uid=xxx
中的uid並注入到session中,因此WebSocketHandler
類才能獲取到session中的uid引數。