大家好,我是小簡,今天我又大意了,在WebSocket
這個類上踩坑了。
接下來我講講我踩坑的經歷吧!
package cn.donglifeng.shop.socket.endpoin;
import cn.donglifeng.shop.common.context.SpringBeanContext;
import cn.donglifeng.shop.common.redis.RedisUtil;
import cn.donglifeng.shop.socket.config.WebSocketConfiguration;
import cn.donglifeng.shop.socket.util.WebSocketEndpointTool;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author JanYork
* @date 2023/3/14 11:36
* @description WebSocket服務端點
*/
@ServerEndpoint(value = "/websocket/{uid}",configurator = WebSocketConfiguration.class)
@Component
@Slf4j
public class WebSocketEndpoint {
@Resource
public RedisUtil redisUtil;
/**
* 連線建立成功呼叫的方法
*
* @param session 可選的引數。session為與某個客戶端的連線會話,需要透過它來給客戶端傳送資料
* @param uid 使用者id
*/
@OnOpen
public void onOpen(Session session, @PathParam("uid") String uid) {
try {
redisUtil.socketOnline(Long.parseLong(uid));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 收到客戶端訊息後呼叫的方法
*/
@OnMessage
public void onMessage(String message) {
if (StringUtils.hasLength(message)) {
//TODO 業務邏輯
} else {
}
}
/**
* 連線錯誤呼叫的方法
*
* @param error 錯誤資訊
*/
@OnError
public void onError(Throwable error) {
error.printStackTrace();
}
/**
* 連線關閉呼叫的方法
*
* @param session 會話
* @param uid 使用者id
*/
@OnClose
public void onClose(Session session, @PathParam("uid") String uid) {
}
/**
* @return 線上人數
*/
public AtomicInteger getOnlineCount() {
return new AtomicInteger(redisUtil.countSocketOnline().intValue());
}
}
上面是一個很簡單的WebSocket
端點服務類。
我打算使用Redis
的Bitmap
來做連線人數統計。
空指標?
@Resource
public RedisUtil redisUtil;
我直接注入我封裝的Redis
工具類,然後自信滿滿的開始測試。
結果.....
???
居然空指標???什麼情況?
我是百思難得其解呀,因為這個類本身也是一個Bean
,使用了@Component
註解。
尋找答案
我開始使用萬能的瀏覽器搜尋。
於是在一番搜尋後,在CSDN
東拼西湊,綜合找到以下答案:
首先,使用了@ServerEndpoint
註解的類中使用@Resource
或@Autowired
注入都會失敗,並且報出空指標異常。
原因是WebSocket
服務是執行緒安全的,那麼當我們去發起一個ws
連線時,就會建立一個端點物件。
那麼問題就在這了,根據CSDN
上的說明,WebSocket
服務是多物件的,不是單例的。
而我們的Spring
的Bean
預設就是單例的,在非單例類中注入一個單例的Bean
是衝突的。
而且我雖然使用@Component
註解了這個類,但是WebSocket
的端點仍然不是單例的,這個是必須的,端點服務不可能單例。
來自
CSDN
:
@Autowired
註解注入物件是在啟動的時候就把物件注入,而不是在使用A物件時才把A需要的B物件注入到A中。
而WebSocket
在剛剛有說到,有連線時才例項化物件,而且有多個連線就有多個。
如何解決?
知道原因還不好解決嗎?我們開發的適合,基本上很常見的遇到要在非Bean
的類中使用Bean
,因為不被Spring容器所管理的類中是無法注入Bean
物件的,所以我們需要去使用一個上下文類,在一開始就將Spring
中所有的Bean
靜態化到上下文類中。
如何實現?
定義一個類,實現ApplicationContextAware
介面:
public class SpringBeanContext implements ApplicationContextAware
不過需要注意的是!這個類也必須要是Bean
,不如無法獲取到Spring
的ApplicationContext
。
@Component
public class SpringBeanContext implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
}
重寫他的setApplicationContext
方法,將ApplicationContext
賦值給本類靜態的屬性。
此時,當我們啟動程式,Spring
中的Bean
物件就全部會被context
獲取到。
然後我們還需要寫從上下文中獲取Bean
的方法,我就直接丟程式碼了:
package cn.donglifeng.shop.common.context;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
/**
* @author JanYork
* @date 2023/3/8 9:33
* @description SpringBean上下文
*/
@Component
public class SpringBeanContext implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
/**
* 獲取上下文
*
* @return 上下文物件
*/
public static ApplicationContext getContext() {
return context;
}
/**
* 根據beanName獲取bean
*
* @param beanName bean名稱
* @return bean物件
*/
public Object getBean(String beanName) {
return context.getBean(beanName);
}
/**
* 根據beanName和型別獲取bean
*
* @param beanName bean名稱
* @param clazz bean型別
* @param <T> bean型別
* @return bean物件
*/
public <T> T getBean(String beanName, Class<T> clazz) {
return context.getBean(beanName, clazz);
}
/**
* 根據型別獲取bean
*
* @param clazz bean型別
* @param <T> bean型別
* @return bean物件
*/
public <T> T getBean(Class<T> clazz) {
return context.getBean(clazz);
}
}
解決效果
/**
* 連線建立成功呼叫的方法
*
* @param session 可選的引數。session為與某個客戶端的連線會話,需要透過它來給客戶端傳送資料
* @param uid 使用者id
*/
@OnOpen
public void onOpen(Session session, @PathParam("uid") String uid) {
try {
RedisUtil bean = SpringBeanContext.getContext().getBean(RedisUtil.class);
bean.socketCache(uid, session);
} catch (Exception e) {
e.printStackTrace();
}
}
這裡我透過上下文類去獲取到Bean
物件,然後測試連線成功了。
擴充套件知識
注意!我這裡有坑,別踩著了,我測試的適合資料還是寫入失敗了,我這裡是想將Socket
的Session
丟到Redis
裡面實現分散式環境物件共享(小小的嘗試)。
bean.socketCache(uid, session);
顯然是不行的,序列化會報錯,因為:
看他的原始碼,他沒有去實現Serializable
介面,是不能被序列化的!
好了,此文結束,下一篇小簡來講將分散式環境下WebSocket
的同步問題吧!