最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Spring Boot WebSocket 兩種集成方式詳解

 更新時(shí)間:2026年05月22日 10:17:25   作者:zhaojiakang99  
WebSocket 是實(shí)現(xiàn)服務(wù)器主動(dòng)推送、實(shí)時(shí)通信的利器,常見于聊天室、消息通知、實(shí)時(shí)監(jiān)控大屏等場景,本文詳細(xì)介紹了SpringBoot集成WebSocket的兩種方式,感興趣的朋友跟隨小編一起看看吧

一次說清楚:原生 @ServerEndpoint 與 Spring 整合 WebSocketHandler,配置差異、踩坑全記錄

前言

WebSocket 是實(shí)現(xiàn)服務(wù)器主動(dòng)推送、實(shí)時(shí)通信的利器,常見于聊天室、消息通知、實(shí)時(shí)監(jiān)控大屏等場景。Spring Boot 集成 WebSocket 有兩條路,很多人在這里摔跟頭,原因只有一個(gè):把兩套配置混用了

本文會(huì)講清楚:

  • 兩種方式各自的工作原理
  • 各自的完整配置步驟
  • 最容易踩的坑(以及為什么會(huì)踩)
  • 選型建議

一、兩種方式的本質(zhì)區(qū)別

維度原生 JSR-356(@ServerEndpoint)Spring 整合(WebSocketHandler)
規(guī)范來源Java EE 標(biāo)準(zhǔn),javax.websocketSpring 框架封裝,org.springframework.web.socket
底層容器由 Servlet 容器(Tomcat/Jetty)直接管理由 Spring DispatcherServlet 統(tǒng)一管理
實(shí)例生命周期每個(gè)連接 new 一個(gè)新實(shí)例單例 Handler 處理所有連接
與 Spring 集成需要額外橋接(ServerEndpointExporter)原生支持,Bean 注入無障礙
適用場景輕量、快速上手需要 Spring 生態(tài)深度整合

二、方式一:原生 JSR-356(@ServerEndpoint)

2.1 原理

JSR-356 是 Java EE 標(biāo)準(zhǔn)的 WebSocket API。Spring Boot 內(nèi)嵌的 Tomcat 本身就支持這套規(guī)范,但 Spring 容器默認(rèn)不會(huì)掃描 @ServerEndpoint 注解的類。

ServerEndpointExporter 的作用就是充當(dāng)"橋梁"——它在 Spring 啟動(dòng)時(shí),把所有被 @ServerEndpoint 標(biāo)注的類手動(dòng)注冊到底層 Servlet 容器的 WebSocket 運(yùn)行時(shí)中。

Spring容器啟動(dòng)
    └── ServerEndpointExporter.afterPropertiesSet()
            └── 掃描 @ServerEndpoint 類
                    └── 注冊到 ServerContainer(Tomcat WebSocket 運(yùn)行時(shí))

2.2 依賴

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

2.3 第一步:WebSocket 配置類

@Configuration
public class WebSocketConfig {
    /**
     * 向 Spring 容器注冊 ServerEndpointExporter
     * 它會(huì)在應(yīng)用啟動(dòng)后,將所有 @ServerEndpoint 注解的類注冊到底層 Servlet 容器
     * 注意:使用外部容器(如獨(dú)立部署的 Tomcat)時(shí),不需要注冊此 Bean,
     *       外部容器會(huì)自行完成注冊
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

 禁忌:這個(gè)配置類不能@EnableWebSocket,也不能實(shí)現(xiàn) WebSocketConfigurer。否則兩套機(jī)制沖突,啟動(dòng)時(shí)會(huì)拋出類轉(zhuǎn)換異常(ClassCastException)。

2.4 第二步:WebSocket 服務(wù)端點(diǎn)

@Component  // ① 必須交給 Spring 容器,才能在內(nèi)部注入 Service 等 Bean
@ServerEndpoint("/ws/chat/{roomId}")  // ② 定義 WebSocket 連接路徑
public class ChatWebSocketServer {
    // ③ 核心踩坑點(diǎn):@ServerEndpoint 每個(gè)連接都會(huì) new 一個(gè)新實(shí)例
    //    因此不能用普通的 @Autowired 字段注入,必須用 static 字段 + setter 注入
    private static MessageService messageService;
    @Autowired
    public void setMessageService(MessageService messageService) {
        ChatWebSocketServer.messageService = messageService;
    }
    // ④ 線程安全:用 ConcurrentHashMap 管理所有在線 Session
    private static final ConcurrentHashMap<String, Session> SESSION_MAP
            = new ConcurrentHashMap<>();
    private Session session;
    private String userId;
    /**
     * 連接建立成功時(shí)觸發(fā)
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("roomId") String roomId) {
        this.session = session;
        this.userId = session.getId();
        SESSION_MAP.put(userId, session);
        System.out.printf("用戶 [%s] 加入房間 [%s],當(dāng)前在線人數(shù):%d%n",
                userId, roomId, SESSION_MAP.size());
    }
    /**
     * 收到客戶端消息時(shí)觸發(fā)
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.printf("收到用戶 [%s] 的消息:%s%n", userId, message);
        // 調(diào)用業(yè)務(wù) Service 處理消息(static 注入,可正常使用)
        messageService.saveMessage(userId, message);
        // 廣播給所有在線用戶
        broadcastMessage(userId + ": " + message);
    }
    /**
     * 連接關(guān)閉時(shí)觸發(fā)
     */
    @OnClose
    public void onClose() {
        SESSION_MAP.remove(userId);
        System.out.printf("用戶 [%s] 斷開連接,當(dāng)前在線人數(shù):%d%n",
                userId, SESSION_MAP.size());
    }
    /**
     * 發(fā)生錯(cuò)誤時(shí)觸發(fā)
     */
    @OnError
    public void onError(Session session, Throwable error) {
        System.err.printf("用戶 [%s] 發(fā)生錯(cuò)誤:%s%n", userId, error.getMessage());
        error.printStackTrace();
    }
    /**
     * 廣播消息給所有在線用戶
     */
    private void broadcastMessage(String message) {
        SESSION_MAP.values().forEach(s -> {
            try {
                if (s.isOpen()) {
                    s.getBasicRemote().sendText(message);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }
    /**
     * 向指定用戶發(fā)送消息(可供外部調(diào)用)
     */
    public static void sendMessageToUser(String userId, String message) {
        Session session = SESSION_MAP.get(userId);
        if (session != null && session.isOpen()) {
            try {
                session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

三、方式二:Spring 整合 WebSocket(WebSocketHandler)

3.1 原理

這套方案是 Spring 自己封裝的 WebSocket 抽象,通過 WebSocketConfigurer 將處理器注冊進(jìn) Spring 的 WebSocket 路由體系,請(qǐng)求由 DispatcherServlet 統(tǒng)一入口分發(fā)。

HTTP 請(qǐng)求升級(jí)為 WebSocket
    └── DispatcherServlet
            └── WebSocketHandlerMapping(路徑路由)
                    └── 你的 WebSocketHandler(處理具體邏輯)

因?yàn)槿淘?Spring 生態(tài)內(nèi),Bean 注入、攔截器、權(quán)限校驗(yàn)都可以無縫對(duì)接。

3.2 第一步:WebSocket 配置類

@Configuration
@EnableWebSocket  // ① 開啟 Spring WebSocket 支持
public class WebSocketConfig implements WebSocketConfigurer {  // ② 實(shí)現(xiàn)此接口
    @Autowired
    private ChatWebSocketHandler chatWebSocketHandler;
    @Autowired
    private WebSocketAuthInterceptor authInterceptor;
    /**
     * ③ 重寫此方法,將處理器注冊到指定路徑
     */
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry
            .addHandler(chatWebSocketHandler, "/ws/chat")  // 注冊處理器和路徑
            .addInterceptors(authInterceptor)               // 可添加握手?jǐn)r截器
            .setAllowedOrigins("*");                        // 跨域配置
    }
}

3.3 第二步:握手?jǐn)r截器(可選但推薦)

握手?jǐn)r截器在 WebSocket 連接建立之前執(zhí)行,常用于身份驗(yàn)證、權(quán)限校驗(yàn)、將用戶信息存入 Session attributes。

@Component
public class WebSocketAuthInterceptor implements HandshakeInterceptor {
    /**
     * WebSocket 握手前執(zhí)行:返回 false 則拒絕連接
     */
    @Override
    public boolean beforeHandshake(ServerHttpRequest request,
                                   ServerHttpResponse response,
                                   WebSocketHandler wsHandler,
                                   Map<String, Object> attributes) throws Exception {
        // 從請(qǐng)求參數(shù)或 Header 中獲取 Token,驗(yàn)證用戶身份
        String token = ((ServletServerHttpRequest) request)
                .getServletRequest().getParameter("token");
        if (token == null || !isValidToken(token)) {
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return false;  // 拒絕握手
        }
        // 將用戶信息存入 attributes,后續(xù) Handler 中可以取到
        attributes.put("userId", parseUserId(token));
        return true;
    }
    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
                               WebSocketHandler wsHandler, Exception exception) {
        // 握手后執(zhí)行,一般留空
    }
    private boolean isValidToken(String token) {
        // 實(shí)際項(xiàng)目中調(diào)用 JWT 解析或 Redis 校驗(yàn)
        return token.startsWith("valid_");
    }
    private String parseUserId(String token) {
        return token.replace("valid_", "");
    }
}

3.4 第三步:WebSocket 處理器

@Component  // ① 交給 Spring 容器管理,正常 @Autowired 注入無任何問題
public class ChatWebSocketHandler extends TextWebSocketHandler {  // ② 繼承此類處理文本消息
    @Autowired
    private MessageService messageService;  // ③ 單例 Handler,直接 @Autowired 完全沒問題
    // 維護(hù)在線 Session 的線程安全 Map
    private static final ConcurrentHashMap<String, WebSocketSession> SESSION_MAP
            = new ConcurrentHashMap<>();
    /**
     * 連接建立成功時(shí)觸發(fā)
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        String userId = (String) session.getAttributes().get("userId");
        SESSION_MAP.put(userId, session);
        System.out.printf("用戶 [%s] 已連接,當(dāng)前在線:%d%n", userId, SESSION_MAP.size());
    }
    /**
     * 收到文本消息時(shí)觸發(fā)
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message)
            throws Exception {
        String userId = (String) session.getAttributes().get("userId");
        String payload = message.getPayload();
        System.out.printf("收到 [%s] 的消息:%s%n", userId, payload);
        // 調(diào)用業(yè)務(wù) Service
        messageService.saveMessage(userId, payload);
        // 廣播消息
        broadcastMessage(userId + ": " + payload);
    }
    /**
     * 連接關(guān)閉時(shí)觸發(fā)
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status)
            throws Exception {
        String userId = (String) session.getAttributes().get("userId");
        SESSION_MAP.remove(userId);
        System.out.printf("用戶 [%s] 已斷開,當(dāng)前在線:%d%n", userId, SESSION_MAP.size());
    }
    /**
     * 傳輸異常時(shí)觸發(fā)
     */
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception)
            throws Exception {
        System.err.println("傳輸錯(cuò)誤:" + exception.getMessage());
        session.close(CloseStatus.SERVER_ERROR);
    }
    private void broadcastMessage(String message) {
        SESSION_MAP.values().forEach(s -> {
            try {
                if (s.isOpen()) {
                    s.sendMessage(new TextMessage(message));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }
}

四、最容易踩的坑,逐一拆解

坑一:兩套配置混用 → ClassCastException

錯(cuò)誤場景:配置類同時(shí)寫了 ServerEndpointExporter Bean 又實(shí)現(xiàn)了 WebSocketConfigurer。

報(bào)錯(cuò)特征

java.lang.ClassCastException: class X cannot be cast to class Y

根因:原生方式繞過 Spring MVC 直接對(duì)接 Servlet 容器;Spring 整合方式走 DispatcherServlet 體系。兩套路由機(jī)制同時(shí)工作,處理同一個(gè) WebSocket 請(qǐng)求時(shí)類型不匹配,直接炸。

解法:二選一,堅(jiān)決不混用。

坑二:原生方式 @Autowired 注入為 null

錯(cuò)誤場景

@ServerEndpoint("/ws/chat")
@Component
public class ChatServer {
    @Autowired
    private UserService userService;  // 運(yùn)行時(shí)是 null!
    @OnMessage
    public void onMessage(String msg) {
        userService.doSomething(msg);  // NullPointerException
    }
}

根因@ServerEndpoint 類由 Servlet 容器管理實(shí)例化,每來一個(gè)連接就 new 一個(gè)新對(duì)象。Spring 只管理它在自己容器里的那一個(gè)原型實(shí)例,Servlet 容器 new 出來的新實(shí)例 Spring 不認(rèn)識(shí),自然也不會(huì)注入。

解法:static 字段 + setter 注入(Spring 注入的那一個(gè)實(shí)例執(zhí)行 setter,寫入 static 字段,所有實(shí)例共享):

@ServerEndpoint("/ws/chat")
@Component
public class ChatServer {
    private static UserService userService;
    @Autowired  // Spring 對(duì)它管理的那個(gè)實(shí)例執(zhí)行此方法,寫入 static 字段
    public void setUserService(UserService userService) {
        ChatServer.userService = userService;
    }
}

坑三:跨域配置不生效

  • 原生方式跨域:在 @ServerEndpoint 注解本身無跨域配置項(xiàng),需要在 Nginx 層或 Filter 層處理
  • Spring 整合方式:直接在 addHandler(...).setAllowedOrigins("*") 配置,簡潔明了

坑四:外部 Tomcat 部署時(shí)不需要ServerEndpointExporter

用嵌入式 Tomcat(spring-boot:run 或打 jar 包)時(shí)需要注冊 ServerEndpointExporter;打 war 包部署到外部 Tomcat 時(shí),外部容器會(huì)自己掃描 @ServerEndpoint,再注冊 ServerEndpointExporter 反而會(huì)報(bào)錯(cuò)。

// 嵌入式容器:需要
// 外部容器:刪掉這個(gè) Bean
@Bean
public ServerEndpointExporter serverEndpointExporter() {
    return new ServerEndpointExporter();
}

五、包路徑對(duì)照表

兩種方式的核心類來自完全不同的包,混用時(shí) IDE 的自動(dòng)補(bǔ)全會(huì)"幫你犯錯(cuò)",務(wù)必留意:

功能原生 JSR-356Spring 整合
端點(diǎn)/處理器注解javax.websocket.@ServerEndpoint實(shí)現(xiàn) org.springframework.web.socket.WebSocketHandler
連接建立@OnOpenafterConnectionEstablished()
接收消息@OnMessagehandleTextMessage()
連接關(guān)閉@OnCloseafterConnectionClosed()
錯(cuò)誤處理@OnErrorhandleTransportError()
Session 類型javax.websocket.Sessionorg.springframework.web.socket.WebSocketSession
消息類型String / ByteBufferTextMessage / BinaryMessage

六、前端連接示例

無論哪種后端方式,前端連接寫法完全一樣:

// 原生方式路徑示例
const ws1 = new WebSocket('ws://localhost:8080/ws/chat/room123');
// Spring 整合方式路徑示例(附帶 token 參數(shù)用于握手?jǐn)r截器驗(yàn)證)
const ws2 = new WebSocket('ws://localhost:8080/ws/chat?token=valid_user001');
ws2.onopen = () => {
    console.log('連接已建立');
    ws2.send(JSON.stringify({ type: 'chat', content: 'Hello!' }));
};
ws2.onmessage = (event) => {
    console.log('收到消息:', event.data);
};
ws2.onclose = (event) => {
    console.log('連接已關(guān)閉,code:', event.code);
};
ws2.onerror = (error) => {
    console.error('連接錯(cuò)誤:', error);
};

七、選型建議

需要權(quán)限校驗(yàn)、Session 管理、與 Spring Security 集成?
    └── 選 Spring 整合方式(WebSocketHandler)
快速實(shí)現(xiàn)、團(tuán)隊(duì)熟悉 Java EE 規(guī)范、無復(fù)雜 Spring 生態(tài)依賴?
    └── 選原生方式(@ServerEndpoint)
需要打 war 包部署到外部 Tomcat?
    └── 兩種都行,但原生方式記得去掉 ServerEndpointExporter Bean
追求更強(qiáng)的消息抽象(發(fā)布訂閱、廣播頻道)?
    └── 考慮 Spring WebSocket + STOMP 協(xié)議(本文未涉及,可作進(jìn)階方向)

總結(jié)

原生 @ServerEndpointSpring WebSocketHandler
配置類@Configuration + ServerEndpointExporter Bean@Configuration + @EnableWebSocket + 實(shí)現(xiàn) WebSocketConfigurer
處理器@ServerEndpoint + @Component實(shí)現(xiàn) WebSocketHandler + @Component
Bean 注入必須用 static 字段 + setter 注入直接 @Autowired,無限制
實(shí)例模型每連接一個(gè)新實(shí)例全局單例
互相混用嚴(yán)禁,直接報(bào)錯(cuò)嚴(yán)禁,直接報(bào)錯(cuò)

記住一句話:選定一套,配全套,絕不混搭。

到此這篇關(guān)于Spring Boot WebSocket 兩種集成方式詳解的文章就介紹到這了,更多相關(guān)Spring Boot WebSocket集成內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • JavaWeb中struts2實(shí)現(xiàn)文件上傳下載功能實(shí)例解析

    JavaWeb中struts2實(shí)現(xiàn)文件上傳下載功能實(shí)例解析

    這篇文章主要介紹了JavaWeb中struts2文件上傳下載功能的實(shí)現(xiàn),在Web應(yīng)用系統(tǒng)開發(fā)中,文件上傳和下載功能是非常常用的功能,需要的朋友可以參考下
    2016-05-05
  • springboot實(shí)現(xiàn)異步調(diào)用@Async的示例

    springboot實(shí)現(xiàn)異步調(diào)用@Async的示例

    這篇文章主要介紹了springboot實(shí)現(xiàn)異步調(diào)用@Async的示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-12-12
  • Java實(shí)現(xiàn)自動(dòng)獲取法定節(jié)假日詳細(xì)代碼

    Java實(shí)現(xiàn)自動(dòng)獲取法定節(jié)假日詳細(xì)代碼

    這篇文章主要給大家介紹了關(guān)于Java實(shí)現(xiàn)自動(dòng)獲取法定節(jié)假日的相關(guān)資料,獲取并處理節(jié)假日數(shù)據(jù)是一個(gè)常見需求,特別是在需要安排任務(wù)調(diào)度、假期通知等功能的場景中,需要的朋友可以參考下
    2024-05-05
  • Java 基礎(chǔ)之內(nèi)部類詳解及實(shí)例

    Java 基礎(chǔ)之內(nèi)部類詳解及實(shí)例

    這篇文章主要介紹了 Java 基礎(chǔ)之內(nèi)部類詳解及實(shí)例的相關(guān)資料,需要的朋友可以參考下
    2017-03-03
  • Spring Boot Swagger2使用方法過程解析

    Spring Boot Swagger2使用方法過程解析

    這篇文章主要介紹了Spring Boot Swagger2使用方法過程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-08-08
  • spring基礎(chǔ)系列之JavaConfig配置詳解

    spring基礎(chǔ)系列之JavaConfig配置詳解

    本篇文章主要介紹了spring基礎(chǔ)系列之JavaConfig配置詳解,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2017-07-07
  • SpringBoot 動(dòng)態(tài)配置Profile環(huán)境的方式

    SpringBoot 動(dòng)態(tài)配置Profile環(huán)境的方式

    這篇文章主要介紹了SpringBoot 動(dòng)態(tài)配置Profile環(huán)境的方式,本文通過圖文實(shí)例相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-10-10
  • Java如何判斷一個(gè)空對(duì)象的常見方法

    Java如何判斷一個(gè)空對(duì)象的常見方法

    在Java中判斷對(duì)象是否為空是一項(xiàng)重要的編程技巧,可以有效防止空指針異常的發(fā)生,下面這篇文章主要給大家介紹了關(guān)于利用Java如何判斷一個(gè)空對(duì)象的相關(guān)資料,需要的朋友可以參考下
    2024-01-01
  • Spring?Boot與Redis的緩存一致性問題解決

    Spring?Boot與Redis的緩存一致性問題解決

    在使用緩存時(shí),緩存一致性問題是一個(gè)常見的挑戰(zhàn),本文主要介紹了Spring?Boot與Redis的緩存一致性問題,具有一定的參考價(jià)值,感興趣的可以了解一下
    2024-07-07
  • hadoop 單機(jī)安裝配置教程

    hadoop 單機(jī)安裝配置教程

    單機(jī)安裝主要用于程序邏輯調(diào)試。安裝步驟基本通分布式安裝,包括環(huán)境變量,主要Hadoop配置文件,SSH配置等,需要的朋友可以參考下
    2012-11-11

最新評(píng)論

湘西| 酒泉市| 东乡县| 南川市| 山丹县| 廊坊市| 廊坊市| 宿松县| 松江区| 阿图什市| 灵寿县| 麟游县| 荔浦县| 凤城市| 淳安县| 马公市| 绥化市| 平舆县| 毕节市| 抚顺市| 沈阳市| 那曲县| 右玉县| 信丰县| 定州市| 锡林郭勒盟| 青神县| 黔西县| 潢川县| 太湖县| 迭部县| 邻水| 涡阳县| 都匀市| 鹰潭市| 西畴县| 南部县| 六枝特区| 盖州市| 泰宁县| 民权县|