踩坑篇之WebSocket實現類中無法使用@Autowired注入物件

JanYork_小簡發表於2023-03-17
大家好,我是小簡,今天我又大意了,在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端點服務類。

我打算使用RedisBitmap來做連線人數統計。

空指標?

@Resource
public RedisUtil redisUtil;

我直接注入我封裝的Redis工具類,然後自信滿滿的開始測試。

結果.....

???

居然空指標???什麼情況?

我是百思難得其解呀,因為這個類本身也是一個Bean,使用了@Component註解。

尋找答案

我開始使用萬能的瀏覽器搜尋。

於是在一番搜尋後,在CSDN東拼西湊,綜合找到以下答案:

首先,使用了@ServerEndpoint註解的類中使用@Resource@Autowired注入都會失敗,並且報出空指標異常。

原因是WebSocket服務是執行緒安全的,那麼當我們去發起一個ws連線時,就會建立一個端點物件。

那麼問題就在這了,根據CSDN上的說明,WebSocket服務是多物件的,不是單例的。

而我們的SpringBean預設就是單例的,在非單例類中注入一個單例的Bean是衝突的。

而且我雖然使用@Component註解了這個類,但是WebSocket的端點仍然不是單例的,這個是必須的,端點服務不可能單例。

來自CSDN

@Autowired註解注入物件是在啟動的時候就把物件注入,而不是在使用A物件時才把A需要的B物件注入到A中。
WebSocket在剛剛有說到,有連線時才例項化物件,而且有多個連線就有多個。

如何解決?

知道原因還不好解決嗎?我們開發的適合,基本上很常見的遇到要在非Bean的類中使用Bean,因為不被Spring容器所管理的類中是無法注入Bean物件的,所以我們需要去使用一個上下文類,在一開始就將Spring中所有的Bean靜態化到上下文類中。

如何實現?

定義一個類,實現ApplicationContextAware介面:

public class SpringBeanContext implements ApplicationContextAware

不過需要注意的是!這個類也必須要是Bean,不如無法獲取到SpringApplicationContext

@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物件,然後測試連線成功了。

擴充套件知識

注意!我這裡有坑,別踩著了,我測試的適合資料還是寫入失敗了,我這裡是想將SocketSession丟到Redis裡面實現分散式環境物件共享(小小的嘗試)。

 bean.socketCache(uid, session);

顯然是不行的,序列化會報錯,因為:

看他的原始碼,他沒有去實現Serializable介面,是不能被序列化的!

好了,此文結束,下一篇小簡來講將分散式環境下WebSocket的同步問題吧!

相關文章