HTTP协议的工作模式是请求-响应,客户端不发起请求,服务器就无法主动推送数据。早期实现实时通知时,开发者常使用短轮询或长轮询,前者频繁发起HTTP请求,浪费带宽和服务器资源;后者虽然减少了请求次数,但连接挂起期间仍占用线程,并且延迟不稳定。WebSocket在TCP之上建立了一条全双工通道,客户端与服务端完成一次握手后,双方可以随时向对方发送消息,特别适合聊天、协同编辑、行情推送等场景。Spring Boot整合WebSocket并不复杂,只需要引入starter依赖,并通过注解或配置类注册端点即可快速搭建服务。

一、依赖引入与基础配置
要在Spring Boot项目中使用WebSocket,第一步是在pom.xml中引入spring-boot-starter-websocket依赖。该starter会自动引入必要的WebSocket API和Spring WebSocket模块,无需额外手动添加Tomcat或Jetty的WebSocket支持,因为Spring Boot内嵌容器已经包含了相应实现。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
引入依赖后,如果使用基于注解的@ServerEndpoint方式,还需要向Spring容器注册一个ServerEndpointExporter。这个Bean负责扫描所有带有@ServerEndpoint注解的类,并把它们注册为WebSocket端点。如果遗漏了这一步,客户端连接时会出现404错误。配置类写法非常简单:
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
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
需要注意的是,如果项目采用外置Servlet容器部署,并且容器本身已经支持WebSocket,例如Tomcat 8以上版本,那么在某些情况下可以不显式配置ServerEndpointExporter,但为了保持开发环境与生产环境的一致性,建议始终显式声明。另外,如果同时引入了Spring Security,还需要对WebSocket握手路径进行放行,否则连接可能会被安全过滤器拦截。
二、使用@ServerEndpoint实现原生WebSocket端点
原生WebSocket方式最直接,适合消息格式简单、不需要复杂路由的场景。创建一个带有@ServerEndpoint注解的类,并为其指定连接路径。该类同时需要被Spring管理,通常加上@Component注解。在类内部使用@OnOpen、@OnMessage、@OnClose和@OnError四个注解分别处理连接建立、消息到达、连接关闭和异常事件。
下面是一个简单的聊天室端点实现。所有在线会话被保存在一个线程安全的ConcurrentHashMap中,消息到达后广播给所有客户端。注意ConcurrentHashMap<String, Session>中的泛型在源码中需要正确书写,在HTML源码展示时需要转义尖括号。
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
@Component
@ServerEndpoint("/ws/chat")
public class ChatEndpoint {
private static final ConcurrentHashMap<String, Session> SESSIONS = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session) {
SESSIONS.put(session.getId(), session);
broadcast("用户 " + session.getId() + " 加入聊天室,当前在线:" + SESSIONS.size());
}
@OnMessage
public void onMessage(String message, Session session) {
broadcast("用户 " + session.getId() + " 说:" + message);
}
@OnClose
public void onClose(Session session) {
SESSIONS.remove(session.getId());
broadcast("用户 " + session.getId() + " 离开,当前在线:" + SESSIONS.size());
}
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
private void broadcast(String message) {
for (Session session : SESSIONS.values()) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
这种实现方式非常直观,但存在一些限制。例如消息的发送与接收直接耦合在同一个类中,当业务变得复杂时,多个端点之间很难统一管理。此外,原生WebSocket没有消息路由能力,无法根据消息头或目的地址将消息分发给特定用户或群组。如果只是做简单的控制台日志推送或者极小型聊天应用,它完全够用;一旦需要订阅、点对点私聊、消息确认等能力,就应当考虑更上层的方案。
Session管理是原生方案中最容易出错的地方。多个客户端同时连接时,对SESSIONS的读写必须保证线程安全。虽然ConcurrentHashMap本身支持并发操作,但遍历时进行删除仍然可能产生弱一致性问题。上面的广播方法在遍历时没有修改集合,因此安全;但如果在遍历过程中尝试移除已断开的Session,建议使用迭代器并调用remove(),或者将失效会话先收集再统一清理。
三、使用STOMP协议实现消息代理与订阅发布
如果业务要求消息按主题订阅、支持点对点私聊或者需要与消息代理集成,更适合使用STOMP协议。STOMP是一种简单的面向文本的消息协议,可以在WebSocket之上提供类似消息队列的语义。Spring Boot通过spring-messaging和spring-websocket模块提供了STOMP支持,配置后可以在Controller中使用@MessageMapping和@SendTo注解处理消息。
首先需要开启STOMP消息代理。创建一个实现WebSocketMessageBrokerConfigurer接口的配置类,并标注@EnableWebSocketMessageBroker。在registerStompEndpoints方法中注册WebSocket端点,并允许跨域访问。在configureMessageBroker方法中启用一个基于内存的简单消息代理,并指定应用前缀和代理目标前缀。
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-stomp").setAllowedOriginPatterns("*").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic", "/queue");
registry.setApplicationDestinationPrefixes("/app");
}
}
上面的配置中,所有客户端发送消息到以/app开头的目标地址,这些消息会先进入Spring的Controller中处理;而服务端推送给客户端的消息则来自以/topic或/queue开头的代理地址。启用SockJS后,即使浏览器不支持原生WebSocket,也可以自动降级为HTTP流或长轮询,从而提高兼容性。
接着可以编写一个处理聊天消息的Controller。使用@MessageMapping映射客户端发送的目标,使用@SendTo将返回值广播到指定主题。这种方式将消息路由和业务逻辑分离,代码结构更清晰。
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
@Controller
public class ChatController {
@MessageMapping("/chat.send")
@SendTo("/topic/public")
public String sendMessage(String message) {
return message;
}
}
当客户端向/app/chat.send发送消息时,该方法被调用,返回的字符串会被推送到所有订阅了/topic/public的客户端。如果需要点对点私聊,可以使用@SendToUser注解,消息会发送到指定用户自己的队列中,默认队列前缀为/user。这种方式天然支持更复杂的消息路由,也更容易与RabbitMQ、ActiveMQ等外部消息代理对接。
四、前端连接与心跳保活
对于原生WebSocket端点,前端使用JavaScript的WebSocket对象即可连接。连接地址与后端注册的路径保持一致,例如ws://localhost:8080/ws/chat。生产环境如果使用HTTPS,则需要使用wss://协议。前端代码需要处理连接建立、消息接收、连接关闭等事件,并提供发送消息的函数。
const ws = new WebSocket("ws://localhost:8080/ws/chat");
ws.onopen = function() {
console.log("连接已建立");
heartBeat();
};
ws.onmessage = function(event) {
console.log("收到消息:", event.data);
};
ws.onclose = function() {
console.log("连接已关闭");
};
function sendMessage() {
const input = document.getElementById("msg");
ws.send(input.value);
input.value = "";
}
function heartBeat() {
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send("ping");
}
}, 30000);
}
WebSocket连接虽然保持长连接,但中间可能经过代理、负载均衡等设备,这些设备往往会在一定空闲时间后断开连接。心跳保活是生产环境必不可少的一环。客户端定时发送一个简单的ping消息,服务端可以原样返回pong或者直接忽略,目的是维持链路活跃。上面的heartBeat函数每30秒发送一次ping,实际间隔可以根据网络环境和业务需求调整。
如果使用STOMP和SockJS,前端需要引入SockJS和STOMP客户端库,连接地址改为http://localhost:8080/ws-stomp,并且使用STOMP的订阅和发送方法。STOMP客户端会处理底层WebSocket细节,应用代码只需调用subscribe和send,开发体验更接近消息队列。通常前端还需要处理连接断开后的自动重连,避免用户手动刷新页面。
五、常见问题与优化建议
在实际部署中,WebSocket服务会遇到一些问题。首先是跨域限制。浏览器要求WebSocket握手也遵循同源策略,如果前端页面与后端服务不在同一域名或端口,需要在服务端配置允许跨域。原生@ServerEndpoint可以通过实现ServerEndpointConfig.Configurator并重写checkOrigin方法;STOMP方式则使用setAllowedOriginPatterns配置允许的来源模式,生产环境应尽量指定具体域名,而不是使用通配符*。
其次是Session的线程安全与资源释放。WebSocket连接断开后,服务端要确保从容器中移除该Session,否则会累积无效连接,导致内存泄漏。对于原生方式,必须在@OnClose中清理。对于STOMP方式,Spring会管理Session生命周期,但自定义的Session存储仍然需要做好清理。此外,如果消息量大,建议使用线程池处理消息发送,避免阻塞WebSocket工作线程。
在集群或多实例部署时,内存中的Session和简单消息代理无法跨节点工作。此时需要使用Redis、RabbitMQ等外部组件作为消息代理,或者通过发布订阅机制同步Session状态。Spring提供了对RabbitMQ STOMP插件和外部Broker的支持,将enableSimpleBroker替换为enableStompBrokerRelay即可将消息路由交给外部代理,从而支持多实例横向扩展。对于消息大小超过默认限制的场景,可以通过WebSocketContainer的setDefaultMaxTextMessageBufferSize调整缓冲区大小,但更推荐对大数据做分片或使用专用文件传输通道。
Spring BootSpring WebSocket实时通信修改时间:2026-09-19 13:38:06