Spring Boot WebSocket 两种集成方式详解

 更新时间:2026年05月22日 10:17:25   作者:zhaojiakang99  
WebSocket 是实现服务器主动推送、实时通信的利器,常见于聊天室、消息通知、实时监控大屏等场景,本文详细介绍了SpringBoot集成WebSocket的两种方式,感兴趣的朋友跟随小编一起看看吧

一次说清楚:原生 @ServerEndpoint 与 Spring 整合 WebSocketHandler,配置差异、踩坑全记录

前言

WebSocket 是实现服务器主动推送、实时通信的利器,常见于聊天室、消息通知、实时监控大屏等场景。Spring Boot 集成 WebSocket 有两条路,很多人在这里摔跟头,原因只有一个:把两套配置混用了

本文会讲清楚:

  • 两种方式各自的工作原理
  • 各自的完整配置步骤
  • 最容易踩的坑(以及为什么会踩)
  • 选型建议

一、两种方式的本质区别

维度原生 JSR-356(@ServerEndpoint)Spring 整合(WebSocketHandler)
规范来源Java EE 标准,javax.websocketSpring 框架封装,org.springframework.web.socket
底层容器由 Servlet 容器(Tomcat/Jetty)直接管理由 Spring DispatcherServlet 统一管理
实例生命周期每个连接 new 一个新实例单例 Handler 处理所有连接
与 Spring 集成需要额外桥接(ServerEndpointExporter)原生支持,Bean 注入无障碍
适用场景轻量、快速上手需要 Spring 生态深度整合

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

2.1 原理

JSR-356 是 Java EE 标准的 WebSocket API。Spring Boot 内嵌的 Tomcat 本身就支持这套规范,但 Spring 容器默认不会扫描 @ServerEndpoint 注解的类。

ServerEndpointExporter 的作用就是充当"桥梁"——它在 Spring 启动时,把所有被 @ServerEndpoint 标注的类手动注册到底层 Servlet 容器的 WebSocket 运行时中。

Spring容器启动
    └── ServerEndpointExporter.afterPropertiesSet()
            └── 扫描 @ServerEndpoint 类
                    └── 注册到 ServerContainer(Tomcat WebSocket 运行时)

2.2 依赖

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

2.3 第一步:WebSocket 配置类

@Configuration
public class WebSocketConfig {
    /**
     * 向 Spring 容器注册 ServerEndpointExporter
     * 它会在应用启动后,将所有 @ServerEndpoint 注解的类注册到底层 Servlet 容器
     * 注意:使用外部容器(如独立部署的 Tomcat)时,不需要注册此 Bean,
     *       外部容器会自行完成注册
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

 禁忌:这个配置类不能@EnableWebSocket,也不能实现 WebSocketConfigurer。否则两套机制冲突,启动时会抛出类转换异常(ClassCastException)。

2.4 第二步:WebSocket 服务端点

@Component  // ① 必须交给 Spring 容器,才能在内部注入 Service 等 Bean
@ServerEndpoint("/ws/chat/{roomId}")  // ② 定义 WebSocket 连接路径
public class ChatWebSocketServer {
    // ③ 核心踩坑点:@ServerEndpoint 每个连接都会 new 一个新实例
    //    因此不能用普通的 @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;
    /**
     * 连接建立成功时触发
     */
    @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%n",
                userId, roomId, SESSION_MAP.size());
    }
    /**
     * 收到客户端消息时触发
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.printf("收到用户 [%s] 的消息:%s%n", userId, message);
        // 调用业务 Service 处理消息(static 注入,可正常使用)
        messageService.saveMessage(userId, message);
        // 广播给所有在线用户
        broadcastMessage(userId + ": " + message);
    }
    /**
     * 连接关闭时触发
     */
    @OnClose
    public void onClose() {
        SESSION_MAP.remove(userId);
        System.out.printf("用户 [%s] 断开连接,当前在线人数:%d%n",
                userId, SESSION_MAP.size());
    }
    /**
     * 发生错误时触发
     */
    @OnError
    public void onError(Session session, Throwable error) {
        System.err.printf("用户 [%s] 发生错误:%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();
            }
        });
    }
    /**
     * 向指定用户发送消息(可供外部调用)
     */
    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 将处理器注册进 Spring 的 WebSocket 路由体系,请求由 DispatcherServlet 统一入口分发。

HTTP 请求升级为 WebSocket
    └── DispatcherServlet
            └── WebSocketHandlerMapping(路径路由)
                    └── 你的 WebSocketHandler(处理具体逻辑)

因为全程在 Spring 生态内,Bean 注入、拦截器、权限校验都可以无缝对接。

3.2 第一步:WebSocket 配置类

@Configuration
@EnableWebSocket  // ① 开启 Spring WebSocket 支持
public class WebSocketConfig implements WebSocketConfigurer {  // ② 实现此接口
    @Autowired
    private ChatWebSocketHandler chatWebSocketHandler;
    @Autowired
    private WebSocketAuthInterceptor authInterceptor;
    /**
     * ③ 重写此方法,将处理器注册到指定路径
     */
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry
            .addHandler(chatWebSocketHandler, "/ws/chat")  // 注册处理器和路径
            .addInterceptors(authInterceptor)               // 可添加握手拦截器
            .setAllowedOrigins("*");                        // 跨域配置
    }
}

3.3 第二步:握手拦截器(可选但推荐)

握手拦截器在 WebSocket 连接建立之前执行,常用于身份验证、权限校验、将用户信息存入 Session attributes。

@Component
public class WebSocketAuthInterceptor implements HandshakeInterceptor {
    /**
     * WebSocket 握手前执行:返回 false 则拒绝连接
     */
    @Override
    public boolean beforeHandshake(ServerHttpRequest request,
                                   ServerHttpResponse response,
                                   WebSocketHandler wsHandler,
                                   Map<String, Object> attributes) throws Exception {
        // 从请求参数或 Header 中获取 Token,验证用户身份
        String token = ((ServletServerHttpRequest) request)
                .getServletRequest().getParameter("token");
        if (token == null || !isValidToken(token)) {
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return false;  // 拒绝握手
        }
        // 将用户信息存入 attributes,后续 Handler 中可以取到
        attributes.put("userId", parseUserId(token));
        return true;
    }
    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
                               WebSocketHandler wsHandler, Exception exception) {
        // 握手后执行,一般留空
    }
    private boolean isValidToken(String token) {
        // 实际项目中调用 JWT 解析或 Redis 校验
        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 完全没问题
    // 维护在线 Session 的线程安全 Map
    private static final ConcurrentHashMap<String, WebSocketSession> SESSION_MAP
            = new ConcurrentHashMap<>();
    /**
     * 连接建立成功时触发
     */
    @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%n", userId, SESSION_MAP.size());
    }
    /**
     * 收到文本消息时触发
     */
    @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);
        // 调用业务 Service
        messageService.saveMessage(userId, payload);
        // 广播消息
        broadcastMessage(userId + ": " + payload);
    }
    /**
     * 连接关闭时触发
     */
    @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%n", userId, SESSION_MAP.size());
    }
    /**
     * 传输异常时触发
     */
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception)
            throws Exception {
        System.err.println("传输错误:" + 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

错误场景:配置类同时写了 ServerEndpointExporter Bean 又实现了 WebSocketConfigurer

报错特征

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

根因:原生方式绕过 Spring MVC 直接对接 Servlet 容器;Spring 整合方式走 DispatcherServlet 体系。两套路由机制同时工作,处理同一个 WebSocket 请求时类型不匹配,直接炸。

解法:二选一,坚决不混用。

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

错误场景

@ServerEndpoint("/ws/chat")
@Component
public class ChatServer {
    @Autowired
    private UserService userService;  // 运行时是 null!
    @OnMessage
    public void onMessage(String msg) {
        userService.doSomething(msg);  // NullPointerException
    }
}

根因@ServerEndpoint 类由 Servlet 容器管理实例化,每来一个连接就 new 一个新对象。Spring 只管理它在自己容器里的那一个原型实例,Servlet 容器 new 出来的新实例 Spring 不认识,自然也不会注入。

解法:static 字段 + setter 注入(Spring 注入的那一个实例执行 setter,写入 static 字段,所有实例共享):

@ServerEndpoint("/ws/chat")
@Component
public class ChatServer {
    private static UserService userService;
    @Autowired  // Spring 对它管理的那个实例执行此方法,写入 static 字段
    public void setUserService(UserService userService) {
        ChatServer.userService = userService;
    }
}

坑三:跨域配置不生效

  • 原生方式跨域:在 @ServerEndpoint 注解本身无跨域配置项,需要在 Nginx 层或 Filter 层处理
  • Spring 整合方式:直接在 addHandler(...).setAllowedOrigins("*") 配置,简洁明了

坑四:外部 Tomcat 部署时不需要ServerEndpointExporter

用嵌入式 Tomcat(spring-boot:run 或打 jar 包)时需要注册 ServerEndpointExporter;打 war 包部署到外部 Tomcat 时,外部容器会自己扫描 @ServerEndpoint,再注册 ServerEndpointExporter 反而会报错。

// 嵌入式容器:需要
// 外部容器:删掉这个 Bean
@Bean
public ServerEndpointExporter serverEndpointExporter() {
    return new ServerEndpointExporter();
}

五、包路径对照表

两种方式的核心类来自完全不同的包,混用时 IDE 的自动补全会"帮你犯错",务必留意:

功能原生 JSR-356Spring 整合
端点/处理器注解javax.websocket.@ServerEndpoint实现 org.springframework.web.socket.WebSocketHandler
连接建立@OnOpenafterConnectionEstablished()
接收消息@OnMessagehandleTextMessage()
连接关闭@OnCloseafterConnectionClosed()
错误处理@OnErrorhandleTransportError()
Session 类型javax.websocket.Sessionorg.springframework.web.socket.WebSocketSession
消息类型String / ByteBufferTextMessage / BinaryMessage

六、前端连接示例

无论哪种后端方式,前端连接写法完全一样:

// 原生方式路径示例
const ws1 = new WebSocket('ws://localhost:8080/ws/chat/room123');
// Spring 整合方式路径示例(附带 token 参数用于握手拦截器验证)
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('连接已关闭,code:', event.code);
};
ws2.onerror = (error) => {
    console.error('连接错误:', error);
};

七、选型建议

需要权限校验、Session 管理、与 Spring Security 集成?
    └── 选 Spring 整合方式(WebSocketHandler)
快速实现、团队熟悉 Java EE 规范、无复杂 Spring 生态依赖?
    └── 选原生方式(@ServerEndpoint)
需要打 war 包部署到外部 Tomcat?
    └── 两种都行,但原生方式记得去掉 ServerEndpointExporter Bean
追求更强的消息抽象(发布订阅、广播频道)?
    └── 考虑 Spring WebSocket + STOMP 协议(本文未涉及,可作进阶方向)

总结

原生 @ServerEndpointSpring WebSocketHandler
配置类@Configuration + ServerEndpointExporter Bean@Configuration + @EnableWebSocket + 实现 WebSocketConfigurer
处理器@ServerEndpoint + @Component实现 WebSocketHandler + @Component
Bean 注入必须用 static 字段 + setter 注入直接 @Autowired,无限制
实例模型每连接一个新实例全局单例
互相混用严禁,直接报错严禁,直接报错

记住一句话:选定一套,配全套,绝不混搭。

到此这篇关于Spring Boot WebSocket 两种集成方式详解的文章就介绍到这了,更多相关Spring Boot WebSocket集成内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 给Java文件打成独立JAR包的详细步骤记录

    给Java文件打成独立JAR包的详细步骤记录

    这篇文章主要介绍了给Java文件打成独立JAR包的相关资料,文中将Java文件打包成独立的JAR包,包括非Maven和Maven项目的打包步骤,需要的朋友可以参考下
    2024-12-12
  • SpringBoot整合Echarts实现数据大屏

    SpringBoot整合Echarts实现数据大屏

    这篇文章给大家介绍了三步实现SpringBoot全局日志记录,整合Echarts实现数据大屏,文中通过代码示例给大家介绍的非常详细,具有一定的参考价值,需要的朋友可以参考下
    2024-03-03
  • Idea中Java项目如何修改项目名

    Idea中Java项目如何修改项目名

    这篇文章主要介绍了Idea中Java项目如何修改项目名问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-06-06
  • java通过控制鼠标实现屏幕广播的方法

    java通过控制鼠标实现屏幕广播的方法

    这篇文章主要介绍了java通过控制鼠标实现屏幕广播的方法,针对前面一篇Java屏幕共享功能进行了改进,实现了鼠标控制功能,具有一定的实用价值,需要的朋友可以参考下
    2014-12-12
  • SpringBoot项目加载配置文件的6种方式小结

    SpringBoot项目加载配置文件的6种方式小结

    这篇文章给大家总结了六种SpringBoot项目加载配置文件的方式,通过@value注入,通过@ConfigurationProperties注入,通过框架自带对象Environment实现属性动态注入,通过@PropertySource注解,yml外部文件,Java原生态方式注入这六种,需要的朋友可以参考下
    2023-09-09
  • Java整型数与网络字节序byte[]数组转换关系详解

    Java整型数与网络字节序byte[]数组转换关系详解

    这篇文章主要介绍了Java整型数与网络字节序byte[]数组转换关系,结合实例形式归纳整理了java整型数和网络字节序的byte[]之间转换的各种情况,需要的朋友可以参考下
    2017-08-08
  • Java 深入理解创建型设计模式之原型模式

    Java 深入理解创建型设计模式之原型模式

    原型(Prototype)模式的定义如下:用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。在这里,原型实例指定了要创建的对象的种类。用这种方式创建对象非常高效,根本无须知道对象创建的细节
    2022-02-02
  • 详解spring boot starter redis配置文件

    详解spring boot starter redis配置文件

    spring-boot-starter-Redis主要是通过配置RedisConnectionFactory中的相关参数去实现连接redis service。下面通过本文给大家介绍在spring boot的配置文件中redis的基本配置,需要的的朋友参考下
    2017-07-07
  • Java中字符串去重的特性介绍

    Java中字符串去重的特性介绍

    这篇文章主要介绍了Java中字符串去重的特性,是Java8中引入的一个新特性,至于是否真的用起来顺手就见仁见智了...需要的朋友可以参考下
    2015-07-07
  • SpringBoot快速整合通用Mapper的示例代码

    SpringBoot快速整合通用Mapper的示例代码

    后端业务开发,每个表都要用到单表的 增删改查 等通用方法,而配置了通用Mapper可以极大的方便使用Mybatis单表的增删改查操作,这篇文章主要介绍了SpringBoot快速整合通用Mapper,需要的朋友可以参考下
    2022-07-07

最新评论