Spring Boot WebSocket实战:原生@ServerEndpoint与WebSocketHandler对比详解
1. 从HTTP到WebSocket为什么我们需要它如果你做过实时聊天、股票行情推送或者在线游戏肯定遇到过一个问题HTTP协议太“慢”了。这里的慢不是指数据传输速度而是指它的“一问一答”模式。客户端发个请求服务器回个响应然后连接就断了。下次客户端想知道服务器有没有新消息得再发起一次请求。这种模式我们称之为“轮询”Polling效率低下且浪费资源。想象一下你和朋友用对讲机聊天每说一句话都要按一下通话键说完松开等对方也按一下键才能回复。而WebSocket就像把对讲机换成了电话一旦接通双方可以随时说话线路一直保持畅通。这就是全双工通信。在Spring Boot项目中集成WebSocket就是为了建立这种“电话线路”让服务器能主动、实时地把数据“推”给客户端而不是等客户端来“拉”。Spring Boot为WebSocket提供了两种主流的集成方式这也是很多开发者初次接触时容易混淆的地方。一种是基于JSR-356标准Java API for WebSocket的原生ServerEndpoint注解另一种是Spring框架自己封装的、更“Spring风格”的WebSocketHandler方式。前者更接近底层标准配置灵活但需要自己管理一些Spring上下文后者与Spring MVC无缝集成能自动享受依赖注入等便利但抽象层次更高。我经历过从原生注解切换到Spring封装的完整过程也踩过不少两者混用或配置冲突的坑。这篇文章我就结合实战把这两种方式的核心原理、具体实现、适用场景以及那些官方文档不会告诉你的细节一次性讲透。2. 环境准备与项目初始化在开始敲代码之前我们需要先把舞台搭好。这里我假设你已经有基本的Spring Boot开发经验。2.1 依赖引入选对起步依赖创建一个标准的Spring Boot项目。关键就在于pom.xml中的依赖。对于WebSocketSpring Boot提供了一个非常方便的起步依赖。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency这个spring-boot-starter-websocket依赖内部已经包含了我们需要的核心库spring-websocket和spring-messaging。它不包含Tomcat等服务器自带的WebSocket实现javax.websocket-api因为Spring Boot的Web Starter里自带的Tomcat已经提供了。如果你使用的是Undertow或Jetty它们也有自己的WebSocket实现Spring Boot会自动适配。这里有个小细节如果你查看这个starter的依赖树会发现它引入了spring-boot-starter-web。这意味着一旦你引入了WebSocket starter你的应用默认就是一个Web应用。如果你只想做一个纯WebSocket服务没有REST API理论上可以排除web starter但实践中很少这么做因为管理起来更麻烦。2.2 核心配置类启用WebSocket支持依赖加好之后我们需要一个配置类来启用WebSocket功能。这是使用Spring封装方式必须的一步。import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; Configuration EnableWebSocket // 关键注解启用WebSocket支持 public class WebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { // 在这里注册你的WebSocket处理器WebSocketHandler // 并指定访问的端点路径比如 /ws // registry.addHandler(myHandler(), /ws).setAllowedOrigins(*); } }这个EnableWebSocket注解的作用是让Spring去查找实现了WebSocketConfigurer接口的Bean比如我们这个配置类然后调用其registerWebSocketHandlers方法来注册处理器。如果你用原生ServerEndpoint方式这个配置类不是必须的但通常我们还是会用它来做一些全局配置比如后面会讲到的拦截器。注意很多初学者会在这里卡住写好了ServerEndpoint的类但客户端连不上往往就是因为缺少了EnableWebSocket这个注解对于Spring封装方式或者没有进行正确的ServerEndpointExporter配置对于原生方式。我们接着往下看。3. 方式一使用原生ServerEndpoint注解JSR-356是Java EE 7引入的WebSocket标准API。Spring Boot可以很好地兼容它。这种方式更接近WebSocket协议本身对于从其他Java EE容器迁移过来的项目或者希望代码更“标准”的团队可能更熟悉。3.1 创建端点Endpoint类首先我们创建一个普通的Java类并用ServerEndpoint注解来标记它是一个WebSocket端点。import javax.websocket.*; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; ServerEndpoint(/native/chat) // 定义WebSocket的访问路径 Component // 必须声明为Spring的Bean public class NativeWebSocketEndpoint { // 用来保存每个客户端会话Session的对象键可以自定义比如用户ID private static ConcurrentHashMapString, Session sessionMap new ConcurrentHashMap(); /** * 连接建立成功时调用的方法 * param session 当前客户端的会话对象是与客户端通信的核心通道 */ OnOpen public void onOpen(Session session) { String sessionId session.getId(); sessionMap.put(sessionId, session); System.out.println(客户端连接建立会话ID: sessionId 当前在线数: sessionMap.size()); sendMessage(session, 服务器连接成功你的会话ID是 sessionId); } /** * 收到客户端消息时调用的方法 * param message 客户端发送的文本消息 * param session 发送消息的客户端会话 */ OnMessage public void onMessage(String message, Session session) { System.out.println(收到来自[ session.getId() ]的消息: message); // 这里可以实现业务逻辑比如广播、私聊等 broadcast(用户[ session.getId() ]说: message); } /** * 连接关闭时调用的方法 */ OnClose public void onClose(Session session) { String sessionId session.getId(); sessionMap.remove(sessionId); System.out.println(客户端连接关闭会话ID: sessionId 当前在线数: sessionMap.size()); } /** * 发生错误时调用的方法 */ OnError public void onError(Session session, Throwable error) { System.out.println(发生错误会话ID: session.getId()); error.printStackTrace(); } // 辅助方法向指定会话发送消息 private void sendMessage(Session session, String message) { try { // getBasicRemote()是同步发送getAsyncRemote()是异步发送 session.getBasicRemote().sendText(message); } catch (IOException e) { e.printStackTrace(); } } // 辅助方法广播消息给所有连接的客户端 private void broadcast(String message) { sessionMap.forEach((id, session) - { if (session.isOpen()) { sendMessage(session, message); } }); } }代码看起来挺直观的有几个关键点需要强调Component注解必不可少这是最容易踩的坑ServerEndpoint是JSR-356的标准注解Spring容器默认不会管理它。如果不加Component或Service等这个类不会被实例化为Spring Bean。后果就是Autowired注入会全部失败并且这个端点根本不会被注册。Session对象这是服务器与单个客户端通信的通道。每个连接都有一个独立的Session实例。通过它可以发送消息、关闭连接、获取连接属性等。会话管理我们用一个静态的ConcurrentHashMap来管理所有在线的Session。这是最简单的实现但在分布式环境下会失效因为HashMap是内存级的不同服务器实例间不共享。生产环境需要考虑用Redis等中间件来存储会话映射关系。OnOpen,OnMessage,OnClose,OnError这四个注解分别对应WebSocket生命周期的四个事件连接建立、收到消息、连接关闭、发生错误。方法签名参数是固定的不能随意更改。3.2 关键配置ServerEndpointExporter仅有上面的端点类还不够。我们需要显式地配置一个ServerEndpointExporterBean它的作用是将所有带有ServerEndpoint注解的类注册到WebSocket服务器中。import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; Configuration public class WebSocketConfig { /** * 这个Bean会自动注册使用了ServerEndpoint注解声明的WebSocket endpoint。 * 如果使用独立的Servlet容器而不是直接使用Spring Boot的内置容器 * 需要提供自己的ServerEndpointExporter。 */ Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }为什么需要这个Bean因为Spring Boot默认的WebSocket支持是基于Spring自己的抽象层WebSocketHandler的。ServerEndpointExporter是一个适配器它负责扫描Spring容器中的ServerEndpoint注解类并将它们“桥接”到底层服务器Tomcat/Jetty/Undertow的WebSocket实现上去。重要提示如果你使用的是Spring Boot的内置容器默认这个Bean是必须的。但如果你将应用打包成WAR包部署到外部的、完整的Servlet容器如独立的Tomcat那么容器本身会负责扫描和注册ServerEndpoint此时再提供这个Bean可能会导致重复注册错误。通常在Spring Boot项目中我们总是显式地声明它。3.3 原生方式的优缺点与注入难题优点标准基于JSR-356代码可移植性相对较好知识可以迁移到其他Java EE容器。直观注解清晰直接对应WebSocket协议事件易于理解。灵活对Session的控制更直接可以方便地获取底层连接信息。缺点与坑点依赖注入问题这是最大的坑。在ServerEndpoint类中每个客户端连接都会创建一个新的端点实例而不是复用单例Bean。这意味着如果你在类里用Autowired注入了一个Service这个注入只会发生一次在Spring创建Bean时。但是当OnOpen等方法被调用时你操作的实际上是另一个新创建的端点实例它的Service成员是null。解决方案需要通过SpringContextUtil这类静态工具类在方法内部手动从Spring容器中获取Bean。OnMessage public void onMessage(String message, Session session) { // 错误方式userService 为 null // userService.doSomething(); // 正确方式通过工具类获取 UserService userService SpringContextUtil.getBean(UserService.class); userService.doSomething(); }你需要事先创建一个工具类实现ApplicationContextAware接口来保存Spring上下文。不支持复杂的消息传递模式原生API对STOMP这种订阅-发布模式的支持较弱需要自己实现分发逻辑对于复杂的消息路由如根据目的地发送不够方便。与Spring生态集成较弱比如想用Spring Security来鉴权WebSocket连接在原生的ServerEndpoint上配置会比较麻烦。4. 方式二使用Spring封装的WebSocketHandler这是Spring官方更推荐的方式它提供了一层抽象与Spring框架集成得更好尤其是当你需要用到STOMP协议时常用于消息代理。我们先从基础的、类似原生功能的WebSocketHandler开始。4.1 实现WebSocketHandler接口WebSocketHandler是一个接口我们需要实现它来处理WebSocket事件。import org.springframework.web.socket.*; import org.springframework.web.socket.handler.TextWebSocketHandler; import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; Component // 声明为Spring Bean public class MyWebSocketHandler extends TextWebSocketHandler { // 继承TextWebSocketHandler更方便 private static final ConcurrentHashMapString, WebSocketSession sessionMap new ConcurrentHashMap(); /** * 连接建立成功后调用 */ Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { String sessionId session.getId(); sessionMap.put(sessionId, session); System.out.println(Spring WS 连接建立会话ID: sessionId); session.sendMessage(new TextMessage(服务器Spring WS连接成功)); } /** * 处理收到的文本消息 */ Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String payload message.getPayload(); // 获取消息内容 System.out.println(Spring WS 收到消息[ session.getId() ]: payload); // 广播 broadcast(Spring WS 广播: payload); } /** * 连接关闭后调用 */ Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { String sessionId session.getId(); sessionMap.remove(sessionId); System.out.println(Spring WS 连接关闭会话ID: sessionId 原因: status); } /** * 传输错误时调用 */ Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { System.out.println(Spring WS 传输错误会话ID: session.getId()); exception.printStackTrace(); } private void broadcast(String message) { TextMessage textMessage new TextMessage(message); sessionMap.forEach((id, sess) - { try { if (sess.isOpen()) { sess.sendMessage(textMessage); } } catch (IOException e) { e.printStackTrace(); } }); } }TextWebSocketHandler是WebSocketHandler的一个抽象实现它已经帮我们区分了文本消息和二进制消息。我们只需要覆盖处理文本消息的方法即可。如果还要处理二进制消息可以实现BinaryWebSocketHandler或直接实现WebSocketHandler接口。4.2 注册Handler与配置拦截器现在我们需要回到之前创建的WebSocketConfig配置类在registerWebSocketHandlers方法中注册这个Handler。Configuration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { Autowired private MyWebSocketHandler myWebSocketHandler; Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myWebSocketHandler, /spring/chat) // 指定处理器和路径 .setAllowedOrigins(*) // 设置允许的跨域来源生产环境应严格限制 .addInterceptors(new MyHandshakeInterceptor()); // 添加握手拦截器 } }这里引入了两个新概念setAllowedOrigins(*)解决跨域问题。WebSocket握手阶段会受到同源策略限制。在开发测试时可以用*但上线后务必替换为具体的、可信的域名列表如.setAllowedOrigins(https://trusted-domain.com)这是重要的安全措施。addInterceptors添加握手拦截器。这是Spring WebSocket非常强大的一个功能允许我们在握手前后进行拦截比如进行身份认证、记录日志、设置Session属性等。4.3 实现握手拦截器HandshakeInterceptorimport org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor; import java.util.Map; public class MyHandshakeInterceptor extends HttpSessionHandshakeInterceptor { /** * 握手之前调用可以在这里进行身份验证、参数提取等。 * 返回true则继续握手返回false则终止连接。 */ Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, MapString, Object attributes) throws Exception { // 例如从请求参数中获取token进行验证 String token request.getURI().getQuery(); // 简单演示实际应从query或header解析 System.out.println(握手前Token: token); // 可以将一些属性存入attributes后续在WebSocketHandler中可以通过WebSocketSession.getAttributes()获取 attributes.put(clientInfo, 来自拦截器的信息); // 调用父类方法以便将HTTP Session中的属性复制到WebSocket Session中如果需要 return super.beforeHandshake(request, response, wsHandler, attributes); } /** * 握手之后调用 */ Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) { System.out.println(握手完成); super.afterHandshake(request, response, wsHandler, ex); } }在WebSocketHandler中你可以这样获取拦截器设置的属性Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { MapString, Object attributes session.getAttributes(); String clientInfo (String) attributes.get(clientInfo); System.out.println(获取到拦截器设置的属性: clientInfo); // ... }4.4 Spring封装方式的优缺点优点完美的Spring集成WebSocketHandler本身是单例Bean可以轻松使用Autowired进行依赖注入没有原生方式的那个注入坑。功能强大天然支持拦截器方便进行统一的认证、鉴权、日志等操作。易于扩展是通往更高级特性如STOMP over WebSocket的基石。Spring的STOMP支持就是构建在WebSocketHandler之上的。更好的抽象提供了WebSocketSession对象它是对底层Session的封装接口更友好。缺点抽象层次高离原始的WebSocket API稍远如果想做一些非常底层的控制可能需要多绕一层。学习曲线对于只熟悉JSR-356的开发者需要学习一套新的API。5. 客户端连接测试与常见问题理论讲完了我们得实际连一下看看。这里以最常用的浏览器JavaScript客户端为例。5.1 编写HTML测试页面创建一个简单的test.html文件放在Spring Boot的static目录下或者通过控制器返回这个页面。!DOCTYPE html html head titleWebSocket 测试/title /head body h2原生端点测试 (/native/chat)/h2 button onclickconnectNative()连接原生端点/button button onclicksendNative()发送消息(原生)/button input typetext idnativeMsg placeholder输入消息 div idnativeOutput/div hr h2Spring端点测试 (/spring/chat)/h2 button onclickconnectSpring()连接Spring端点/button button onclicksendSpring()发送消息(Spring)/button input typetext idspringMsg placeholder输入消息 div idspringOutput/div script let nativeSocket; let springSocket; function connectNative() { // 注意协议是 ws 或 wss (加密) nativeSocket new WebSocket(ws://localhost:8080/native/chat); setupSocketEvents(nativeSocket, nativeOutput); } function connectSpring() { springSocket new WebSocket(ws://localhost:8080/spring/chat); setupSocketEvents(springSocket, springOutput); } function setupSocketEvents(socket, outputDivId) { socket.onopen function(event) { log(outputDivId, 连接已打开); }; socket.onmessage function(event) { log(outputDivId, 收到消息: event.data); }; socket.onclose function(event) { log(outputDivId, 连接关闭代码: event.code , 原因: event.reason); }; socket.onerror function(error) { log(outputDivId, 发生错误: error); }; } function sendNative() { let msg document.getElementById(nativeMsg).value; if (nativeSocket nativeSocket.readyState WebSocket.OPEN) { nativeSocket.send(msg); log(nativeOutput, 已发送(原生): msg); } else { alert(原生连接未建立); } } function sendSpring() { let msg document.getElementById(springMsg).value; if (springSocket springSocket.readyState WebSocket.OPEN) { springSocket.send(msg); log(springOutput, 已发送(Spring): msg); } else { alert(Spring连接未建立); } } function log(divId, message) { let div document.getElementById(divId); div.innerHTML p new Date().toLocaleTimeString() - message /p; } /script /body /html5.2 连接失败问题排查指南如果点击连接按钮没反应或者控制台报错可以按照以下步骤排查检查服务器是否启动确保Spring Boot应用已经成功启动并且没有端口冲突。检查端点路径确认客户端连接的URL如ws://localhost:8080/native/chat与服务器端ServerEndpoint或registry.addHandler中定义的路径完全一致。注意上下文路径Context Path如果你的应用设置了server.servlet.context-path/myapp那么WebSocket的完整路径应该是ws://localhost:8080/myapp/native/chat。检查依赖和配置对于原生ServerEndpoint确认类上有Component注解并且配置了ServerEndpointExporterBean。对于SpringWebSocketHandler确认配置类有EnableWebSocket注解并且Handler已正确注册。查看服务器日志连接握手失败通常会在服务器控制台打印异常信息比如404路径不对、403拦截器拒绝等。浏览器开发者工具打开浏览器的Network网络选项卡查看WebSocket连接类型是ws的状态。如果是101 Switching Protocols说明握手成功。如果是其他状态码如404, 500可以查看响应详情。跨域问题如果前端页面地址如http://localhost:63342与后端地址ws://localhost:8080不同源浏览器会阻止握手。确保在服务器端通过.setAllowedOrigins(*)或指定具体源来允许跨域。再次强调生产环境不要用*。防火墙/代理问题确保本地或服务器防火墙没有屏蔽WebSocket使用的端口通常是80/ws或443/wss。6. 进阶话题心跳、断线重连与分布式会话一个健壮的WebSocket服务不能只满足于连通。在实际生产环境中网络不稳定、服务器重启是常态。下面聊聊如何让WebSocket连接更可靠。6.1 心跳机制HeartbeatWebSocket连接可能因为网络空闲而被中间设备如Nginx、防火墙断开。心跳机制就是客户端和服务器定期发送一个小数据包Ping/Pong告诉对方“我还活着”。服务器端发送心跳在Spring的WebSocketHandler中可以在连接建立后启动一个定时任务。Component public class MyWebSocketHandler extends TextWebSocketHandler { Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { // ... 其他逻辑 startHeartbeat(session); } private void startHeartbeat(WebSocketSession session) { ScheduledExecutorService scheduler Executors.newSingleThreadScheduledExecutor(); // 每隔30秒发送一次Ping消息 scheduler.scheduleAtFixedRate(() - { try { if (session.isOpen()) { // Spring提供了sendPingMessage方法底层会发送Ping帧 session.sendMessage(new PingMessage()); // 或者发送一个特定的文本消息作为心跳 // session.sendMessage(new TextMessage(ping)); } else { scheduler.shutdown(); // 连接关闭停止心跳任务 } } catch (IOException e) { e.printStackTrace(); scheduler.shutdown(); } }, 30, 30, TimeUnit.SECONDS); // 需要将会话和scheduler关联起来在连接关闭时清理资源这里简化了 } }客户端处理心跳与超时浏览器WebSocket API可以监听onmessage事件来处理服务器发来的Ping或特定文本并定时向服务器发送Pong或特定文本响应。let heartbeatInterval; socket.onopen function() { log(连接打开); // 启动心跳每20秒发送一个ping heartbeatInterval setInterval(() { if (socket.readyState WebSocket.OPEN) { socket.send(ping); // 或发送二进制的ping帧 } }, 20000); }; socket.onmessage function(event) { if (event.data ping) { // 收到服务器ping回复pong socket.send(pong); return; } // ... 处理其他业务消息 }; socket.onclose function() { clearInterval(heartbeatInterval); // 清理定时器 log(连接关闭); };6.2 客户端断线重连网络波动导致连接断开是不可避免的。一个良好的客户端应该具备自动重连的能力。let socket; let reconnectAttempts 0; const maxReconnectAttempts 5; const reconnectDelay 2000; // 2秒 function connect() { socket new WebSocket(ws://localhost:8080/spring/chat); socket.onopen function() { log(连接成功); reconnectAttempts 0; // 重置重连计数 }; socket.onclose function(event) { log(连接断开尝试重连...); if (reconnectAttempts maxReconnectAttempts) { reconnectAttempts; setTimeout(connect, reconnectDelay * reconnectAttempts); // 退避重连 } else { log(重连次数已达上限请检查网络或联系管理员。); } }; socket.onerror function(error) { log(连接错误: error); socket.close(); // 触发onclose进行重连 }; // ... 其他事件监听 }6.3 分布式会话管理之前我们用内存ConcurrentHashMap保存会话。这在单机环境下没问题但一旦部署多台服务器用户可能连接到A服务器而他的好友连接到B服务器。当A服务器的用户发送消息时无法直接找到在B服务器上的好友会话进行推送。解决方案引入消息中间件常见的做法是使用Redis的Pub/Sub功能或专业的消息队列如RabbitMQ, Kafka来解耦。会话标识与存储用户连接WebSocket时生成一个全局唯一的会话ID或使用用户ID并将服务器节点ID 会话ID作为键存入Redis。同时订阅一个公共的广播频道。消息发送当A服务器的某个会话需要发消息时它不直接遍历本地Map而是将消息和目标标识用户ID、群组ID或广播发布到Redis的特定频道。消息接收与转发所有服务器节点都订阅了Redis的广播频道。当B服务器从Redis收到一条消息时它会检查目标会话是否在自己本地。如果在就通过本地的WebSocketSession将消息发送出去。// 伪代码示例使用RedisTemplate Component public class RedisMessageListener { Autowired private RedisTemplateString, Object redisTemplate; Autowired private MyWebSocketHandler webSocketHandler; // 需要能获取到本地会话Map PostConstruct public void init() { // 订阅频道 redisTemplate.getConnectionFactory().getConnection().subscribe((message, pattern) - { String channel new String(message.getChannel()); String body new String(message.getBody()); // 解析消息体获取目标用户ID String targetUserId parseTargetUserId(body); // 查找本地会话并发送 WebSocketSession session webSocketHandler.getSession(targetUserId); if (session ! null session.isOpen()) { session.sendMessage(new TextMessage(body)); } }, websocket.broadcast.getBytes()); } // 发送消息到Redis频道的方法 public void sendMessageToRedis(String channel, String message) { redisTemplate.convertAndSend(channel, message); } }这样无论用户连接到哪台服务器消息都能通过Redis这个“中转站”准确送达。这是构建大规模、高可用WebSocket应用的关键一步。7. 性能调优与生产环境注意事项当你的WebSocket服务用户量上来之后一些在开发阶段不明显的问题就会暴露出来。这里分享几个关键的调优点和注意事项。7.1 服务器参数调优以最常用的Tomcat为例在application.yml中可以对WebSocket连接进行一些优化server: tomcat: max-connections: 10000 # 最大连接数 max-threads: 200 # 最大工作线程数 min-spare-threads: 10 # 最小空闲线程数 # WebSocket相关缓冲区设置在Spring Boot 2.x中部分配置需要通过自定义Bean实现对于WebSocket更需要关注的是连接超时和缓冲区大小。WebSocket连接是长连接默认不会超时。但你可以通过自定义ServletWebSocketContainer来配置Bean public ServletWebSocketContainerFactory createWebSocketContainer() { return new ServletWebSocketContainerFactory() { Override protected void customizeTomcat(ConfigurableTomcatWebSocketContainerFactory container) { // 设置异步发送超时时间毫秒 container.setAsyncSendTimeout(60000L); // 设置最大会话空闲超时时间毫秒超过后服务器会主动关闭连接 container.setMaxSessionIdleTimeout(15 * 60 * 1000L); // 15分钟 // 设置消息缓冲区大小字节 container.setMaxTextMessageBufferSize(8192); // 8KB container.setMaxBinaryMessageBufferSize(8192); } }; }7.2 使用Nginx反向代理在生产环境我们通常不会让客户端直接连接应用服务器而是前面挂一个Nginx做反向代理和负载均衡。Nginx从1.3版本开始就支持WebSocket代理配置很简单但很关键http { map $http_upgrade $connection_upgrade { default upgrade; close; } upstream websocket_backend { server app1:8080; server app2:8080; # 可以配置负载均衡策略如ip_hash保证同一客户端连接到同一后端 ip_hash; } server { listen 80; server_name your-domain.com; location /ws/ { # 你的WebSocket路径前缀 proxy_pass http://websocket_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; # 以下两行很重要防止代理超时断开连接 proxy_read_timeout 3600s; proxy_send_timeout 3600s; } } }核心就是那三个proxy_set_headerUpgrade和Connection头用于告知Nginx这是WebSocket连接需要“升级”协议。proxy_read_timeout和proxy_send_timeout需要设置得足够长因为WebSocket是长连接。7.3 监控与日志线上服务离不开监控。你需要关注几个核心指标活跃连接数反映当前负载。消息吞吐率每秒收发消息数。连接建立/关闭速率异常升高可能意味着有问题。错误率各种错误握手失败、消息解析失败、发送超时的数量。对于Spring封装的WebSocket你可以实现WebSocketHandlerDecoratorFactory来装饰所有的Handler从而统一收集 metrics。Configuration public class WebSocketMetricsConfig implements WebSocketConfigurer { // ... 其他配置 Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myWebSocketHandler, /path) .addInterceptors(new MyHandshakeInterceptor()) .setAllowedOrigins(*) .withSockJS(); // 如果需要SockJS支持可以加这个 } Bean public WebSocketHandlerDecoratorFactory loggingDecoratorFactory() { return new WebSocketHandlerDecoratorFactory() { Override public WebSocketHandler decorate(WebSocketHandler handler) { return new WebSocketHandlerDecorator(handler) { Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { // 连接建立记录日志或增加计数器 log.info(WebSocket连接建立: {}, session.getId()); Metrics.counter(websocket.connections).increment(); super.afterConnectionEstablished(session); } Override public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception { // 连接关闭 log.info(WebSocket连接关闭: {}, status: {}, session.getId(), closeStatus); Metrics.counter(websocket.connections).decrement(); super.afterConnectionClosed(session, closeStatus); } }; } }; } }结合Micrometer等监控库可以将这些指标暴露给Prometheus再通过Grafana展示这样你就能对WebSocket服务的健康状态一目了然了。从最基础的连接建立到心跳重连保活再到分布式架构下的会话共享最后到生产环境的部署调优这基本上就是一个WebSocket服务从零到一、从一到一百需要经历的关键路径。选择原生注解还是Spring封装没有绝对的好坏只有适合与否。对于大多数Spring Boot项目从与Spring生态无缝集成的角度出发我更倾向于使用Spring封装的WebSocketHandler方式起步它的拦截器、STOMP扩展等特性会让后续的开发更顺畅。而如果你需要极致的控制或者项目本身就是从其他标准容器迁移而来那么原生的ServerEndpoint也是一个可靠的选择。关键在于理解其背后的原理这样无论用哪种方式当问题出现时你都能知道该从哪里入手排查。