1. 程式人生 > 其它 >Spring - Vue+Spring Boot實現WebSocket定時訊息推送

Spring - Vue+Spring Boot實現WebSocket定時訊息推送

Vue+Spring Boot實現WebSocket定時訊息推送

要實現本篇訊息推送功能,首先要準備好:一個vue專案,一個已經整合Quartz框架的Spring Boot專案。

後端配置

首先在pom中新增webSocket依賴:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </
dependency>

然後建立一個 MySpringConfigurator。這個類的作用是端點配置類,用在接下來我們的Web Socket配置中。

從下面程式碼可以看出,如果不建立這個類,Web Socket註冊的Bean預設是由ServerEndpointConfig自己管理的,這個類的作用就是把Web Socket相關Bean也交給Spring去管理:

public class MySpringConfigurator extends ServerEndpointConfig.Configurator implements ApplicationContextAware {
 
    
private static volatile BeanFactory context; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { MySpringConfigurator.context = applicationContext; } @Override public <T> T getEndpointInstance(Class<T> clazz) throws
InstantiationException { return context.getBean(clazz); } }

建立真正的Web Socket配置類:

@Configuration
public class WebSocketConfig {

    /**
     * 注入ServerEndpointExporter,
     * 這個bean會自動註冊使用了@ServerEndpoint註解宣告的Websocket endpoint
     *
     * @return ServerEndpointExporter
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

    /**
     * 註冊自定義的配置類
     *
     * @return MySpringConfigurator
     */
    @Bean
    public MySpringConfigurator mySpringConfigurator() {
        return new MySpringConfigurator();
    }
}

然後是Web Socket的真正實現類。因為我沒找到ws傳輸header的解決方案,所以只能在連線的時候,用url param去鑑權,如果鑑權失敗就關閉連線:

@Slf4j
@Component
//此註解相當於設定訪問URL
@ServerEndpoint(value = "/websocket/alarm/{userId}/{token}", configurator = MySpringConfigurator.class)
public class AlarmWebSocket {

    /**
     * 鑑權業務邏輯類
     */
    @Autowired
    private ShiroService shiroService;

    // 連線會話,通過它來和客戶端互動
    private Session session;
    // 儲存websocket連線
    public static final CopyOnWriteArraySet<AlarmWebSocket> ALARM_WEB_SOCKETS = new CopyOnWriteArraySet<>();
    // 儲存使用者和session的對應關係
    private static final Map<String, Session> sessionPool = new HashMap<>();

    /**
     * 連線成功回撥
     * 因為web socket沒有請求頭,所以需要在連線成功的時候做一次鑑權
     * 如果鑑權失敗,斷開連線
     *
     * @param session session
     * @param userId  使用者id
     */
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "userId") String userId, @PathParam(value = "token") String token)
            throws IOException {
        // 根據accessToken,查詢使用者資訊
        SysUserTokenEntity tokenEntity = shiroService.queryByToken(token);
        // token失效
        if (tokenEntity == null
                || tokenEntity.getExpireTime().getTime() < System.currentTimeMillis()
                || !userId.equalsIgnoreCase(tokenEntity.getUserId())) {
            // 自定義websocket關閉原因
            CloseReason closeReason = new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "鑑權失敗!");
            session.close(closeReason);
            throw new IncorrectCredentialsException("token失效,請重新登入");
        }
        this.session = session;
        ALARM_WEB_SOCKETS.add(this);
        sessionPool.put(userId, session);
        log.info("【websocket訊息】有新的連線,總數為:{}", ALARM_WEB_SOCKETS.size());
    }

    /**
     * 連線關閉回撥
     */
    @OnClose
    public void onClose() {
        ALARM_WEB_SOCKETS.remove(this);
        log.info("【websocket訊息】連線斷開,總數為:{}", ALARM_WEB_SOCKETS.size());
    }

    /**
     * 收到資訊的回撥
     *
     * @param message 收到的資訊
     */
    @OnMessage
    public void onMessage(String message) {
        log.info("【websocket訊息】收到客戶端訊息:{}", message);
    }

    /**
     * 廣播訊息
     *
     * @param message 訊息內容
     */
    public void sendAllMessage(String message) {
        for (AlarmWebSocket alarmWebSocket : ALARM_WEB_SOCKETS) {
            log.info("【websocket訊息】廣播訊息:{}", message);
            try {
                Session session1 = alarmWebSocket.session;
                session1.getAsyncRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 傳送一對一訊息
     *
     * @param userId  對端userId
     * @param message 訊息內容
     */
    public void sendTextMessage(String userId, String message) {
        Session session = sessionPool.get(userId);
        if (session != null) {
            try {
                session.getBasicRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

最後就是定時推送任務的編寫了,因為集成了 Quartz 框架,所以後臺只需要寫一個定時任務Bean就好了。沒有整合的朋友們可以用@Scheduled 註解定義定時任務來代替:

@Slf4j
@Component("alarmTask")
public class AlarmTask implements ITask {
    @Autowired
    private AlarmWebSocket alarmWebSocket;
    @Override
    public void run(String params) {
        // 如果有連線則查詢報警訊息並推送
        if (AlarmWebSocket.ALARM_WEB_SOCKETS.size() > 0) {
            alarmWebSocket.sendAllMessage("需要推送的資料");
        }
        log.info("alarmTask定時任務正在執行");
    }
}

注:我在開發的過程中遇到了一個問題,AlarmWebSocket裡的session.getBasicRemote().sendText(message); 這句程式碼,如果我使用 sendText 方法就可以傳送訊息,用sendObject 就不能。期待以後有時間了研究一下。

前端配置

前端配置倒也簡單了,都是制式的東西,程式碼如下:

    // 初始化websocket
    initWebSocket: function () {
      // 建立websocket連線,傳入鑑權引數
      let userId = Vue.cookie.get('userId');
      let token = Vue.cookie.get('token');
// 這裡的url要換成你自己的url
this.websock = new WebSocket(window.SITE_CONFIG.wsUrl + "/websocket/alarm/" + userId + "/" + token); // 配置回撥方法 this.websock.onopen = this.websocketOnOpen; this.websock.onerror = this.websocketOnError; this.websock.onmessage = this.websocketOnMessage; this.websock.onclose = this.websocketClose; }, // 連線成功回撥 websocketOnOpen: function () { console.log("WebSocket連線成功"); }, // 錯誤回撥 websocketOnError: function (e) { console.log("WebSocket連線發生錯誤"); console.log(e); }, // 收到訊息回撥 websocketOnMessage: function (e) { let obj = JSON.parse(e.data); ...業務邏輯 }, // 連線關閉回撥 websocketClose: function (e) { console.log("WebSocket連線成功"); }

然後在頁面生命週期函式裡面呼叫方法:

  created() {
    // 連線websocket
    this.initWebSocket();
  },
  destroyed() {
    this.websocketClose();
  },

到這裡一個定時訊息推送功能就全部完成了。