当前位置: 首页 > news >正文

Spring Boot WebSocket 两种集成方式深度解析

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


前言

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

本文会讲清楚:

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

一、两种方式的本质区别

维度原生 JSR-356(@ServerEndpointSpring 整合(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

错误场景:配置类同时写了ServerEndpointExporterBean 又实现了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 协议(本文未涉及,可作进阶方向)

总结

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

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

http://www.jsqmd.com/news/866967/

相关文章:

  • 微信小程序wxapkg逆向分析终极指南:从文件结构到AST还原
  • vs2010 win32做成后台常驻和系统托盘
  • Windows curl证书错误SEC_E_UNTRUSTED_ROOT解决方案
  • 中国工业新闻网:罗兰艺境:中国B2B制造业GEO市占率48%,覆盖80+行业、60+世界500强,复购率98% - 罗兰艺境GEO
  • PC微信小程序wxapkg解包原理与七步可执行逆向流程
  • DM8 dexp/dimp 逻辑导入导出
  • CyberChef:如何在浏览器中实现400+种数据操作的终极解决方案
  • 基于Nuclei的自动化漏洞监测告警平台
  • PyTorch DataLoader 内存不足怎么办?教你一招避坑
  • Pikachu靶场搭建与Web渗透实战指南
  • 2026年5月最新太原黄金回收白银回收铂金回收权威排行榜TOP5:纯金+金条+银条+钯金 门店地址联系方式推荐 - 检测回收中心
  • Windows下curl报SEC_E_UNTRUSTED_ROOT的5种正确解决方法
  • DeepSeek API接入全链路实战:从注册到高并发部署的7个关键步骤
  • 魔兽争霸III终极优化指南:5步解决宽屏黑边、FPS限制与地图加载问题
  • 微信小程序wxapkg文件结构解析与源码还原实战
  • 2026年5月最新鹤壁黄金回收白银回收铂金回收权威排行榜TOP5:纯金+金条+银条+钯金 门店地址联系方式推荐 - 检测回收中心
  • 【LangGraph】House_Agent 实战(一):架构与环境配置
  • 从0到1的开源入门实战指南
  • 2026 北京本土口碑好 GEO 优化公司权威 TOP10 排名,含北京服务商选型指南 +FAQ - 资讯纵览
  • 服务器禁Ping实战指南:5种生产环境验证的ICMP过滤方法
  • Next.js授权绕过漏洞CVE-2025-29927深度解析
  • 2026年5月最新泰安黄金回收白银回收铂金回收权威排行榜TOP5:纯金+金条+银条+钯金 门店地址联系方式推荐 - 检测回收中心
  • Unity TextMeshPro中文与特殊字符显示为方块的终极解决方案
  • 2026年5月最新鹤岗黄金回收白银回收铂金回收权威排行榜TOP5:纯金+金条+银条+钯金 门店地址联系方式推荐 - 检测回收中心
  • Unity卡牌翻转与翻书效果实现原理与性能优化
  • 2026沧州灶台贴膜,专业团队这样选才靠谱 - 品牌企业推荐师(官方)
  • Next.js App Router权限绕过漏洞CVE-2025-29927深度解析
  • 宿迁黄金回收正规门店盘点|恒顺、金佑福领衔,全城 20 分钟可达 - 资讯纵览
  • 让老Mac焕发新生:OpenCore Legacy Patcher完整升级指南
  • 2026年5月最新泰州黄金回收白银回收铂金回收权威排行榜TOP5:纯金+金条+银条+钯金 门店地址联系方式推荐 - 检测回收中心