在 Web 应用里实现即时聊天,传统做法是客户端每隔几秒发一次 HTTP 请求拉取新消息,也就是短轮询。这种方式不仅延迟不稳定,而且大量请求会消耗服务器资源。WebSocket 在客户端和服务端之间建立一条持久连接,服务端可以随时向客户端推送数据,非常适合点对点私聊和群聊场景。Spring Boot 通过 spring-boot-starter-websocket 模块提供了对 WebSocket 的完整支持,再配合 STOMP 协议和内置消息代理,开发者无需手动处理底层帧,就能实现清晰的消息路由。

接下来会从服务端配置、消息模型、控制器逻辑以及前端订阅方式逐步展开,重点说明点对点消息如何利用 /user 目的地精准送达,群聊消息如何通过 /topic 广播给所有在线用户。
一、引入依赖并创建 WebSocket 配置类
在 pom.xml 中加入 spring-boot-starter-websocket 依赖,这个 starter 会传递引入 spring-messaging、spring-websocket 以及 Tomcat 的相关支持。如果项目使用 Gradle,可以添加对应的 implementation 配置。依赖就绪后,需要创建一个配置类实现 WebSocketMessageBrokerConfigurer 接口,重写 configureMessageBroker 和 registerStompEndpoints 方法。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
上面这段 XML 可以直接放进 pom.xml 的 dependencies 节点。添加依赖后,如果使用 Spring Boot 的自动配置,也可以不写额外配置,但为了控制消息前缀和端点路径,建议自定义配置类。
创建 WebSocketConfig 类并标注 @Configuration 和 @EnableWebSocketMessageBroker。在 configureMessageBroker 方法中调用 enableSimpleBroker("/topic", "/queue") 开启基于内存的消息代理,并设置 setApplicationDestinationPrefixes("/app"),让客户端发送到 /app 开头的消息进入注解方法处理。注册端点时使用 addEndpoint("/ws").withSockJS(),这样即便浏览器或网络环境不支持原生 WebSocket,也可以降级到 SockJS 的轮询或长连接。
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 WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic", "/queue");
registry.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").withSockJS();
}
}
这里把 /topic 和 /queue 都配置成简单消息代理的目的地。简单消息代理基于内存,适合单机部署;如果后面要横向扩展,可以换成外部消息代理,例如 RabbitMQ 或 ActiveMQ。客户端在连接时访问的端点是 /ws,这个路径与普通 HTTP 接口不冲突,因为它专门处理 WebSocket 握手请求。
二、设计消息模型并编写服务端控制器
为了让点对点和群聊共用一套接口,可以定义一个 ChatMessage 类,包含消息类型、发送者、接收者和内容四个字段。消息类型用来区分 CHAT 和 JOIN 等动作,发送者和接收者使用用户标识,接收者为空时表示群聊消息。内容字段保存文本,后续扩展图片或文件时再增加附件字段即可。保持消息体简洁有助于前后端快速对接。
public class ChatMessage {
private MessageType type;
private String sender;
private String receiver;
private String content;
public enum MessageType {
CHAT,
JOIN,
LEAVE
}
public MessageType getType() { return type; }
public void setType(MessageType type) { this.type = type; }
public String getSender() { return sender; }
public void setSender(String sender) { this.sender = sender; }
public String getReceiver() { return receiver; }
public void setReceiver(String receiver) { this.receiver = receiver; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
}
消息实体定义好后,创建一个控制器,使用 @MessageMapping 注解映射客户端发来的目的地。例如客户端向 /app/chat.send 发送消息,服务端用 @MessageMapping("/chat.send") 接收。群聊场景中,方法返回 ChatMessage 对象,并配合 @SendTo("/topic/public") 把返回值广播给所有订阅 /topic/public 的客户端。
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
@Controller
public class ChatController {
@MessageMapping("/chat.send")
@SendTo("/topic/public")
public ChatMessage sendMessage(@Payload ChatMessage message) {
return message;
}
@MessageMapping("/chat.join")
@SendTo("/topic/public")
public ChatMessage join(@Payload ChatMessage message) {
message.setContent(message.getSender() + " 加入了聊天室");
return message;
}
}
群聊逻辑借助 @SendTo 注解已经足够实现。点对点消息则需要更灵活的方式,不能简单返回广播,而是要指定接收者。这时可以注入 SimpMessagingTemplate,使用 convertAndSendToUser 方法把消息发送到指定用户。点对点方法可以直接写在同一个控制器里。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
@Controller
public class PrivateChatController {
@Autowired
private SimpMessagingTemplate messagingTemplate;
@MessageMapping("/chat.private")
public void sendPrivateMessage(@Payload ChatMessage message) {
// 接收者不能为空,否则会发送失败
messagingTemplate.convertAndSendToUser(
message.getReceiver(),
"/queue/messages",
message
);
}
}
调用 convertAndSendToUser 时,第一个参数是接收者的用户名,第二个参数是目的地。Spring 会把这个目的地与用户会话关联,最终转换成该用户订阅的 /user/queue/messages 地址。用户认证信息来源于 WebSocket 握手阶段的 Principal,如果项目没有安全认证,需要手动设置用户身份,否则 user destination 无法工作。
三、前端建立连接并订阅频道
前端页面需要引入 SockJS 和 STOMP.js 两个脚本,也可以使用 npm 安装对应的模块。连接时先创建 SockJS 实例,再通过 Stomp.over 包装成 STOMP 客户端。连接成功后,客户端可以订阅 /topic/public 接收群聊消息,同时订阅 /user/queue/messages 接收点对点消息。注意 /user 前缀是客户端订阅路径,服务端发送时用的是 /queue/messages,框架会自动补齐用户前缀。
var socket = new SockJS('/ws');
var stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
stompClient.subscribe('/topic/public', function(message) {
var chat = JSON.parse(message.body);
showPublicMessage(chat);
});
stompClient.subscribe('/user/queue/messages', function(message) {
var chat = JSON.parse(message.body);
showPrivateMessage(chat);
});
});
发送群聊消息时,客户端调用 stompClient.send,目的地写成 /app/chat.send,消息体为 JSON 字符串。发送点对点消息时,目的地改为 /app/chat.private,JSON 中包含 receiver 字段。前端需要获取当前登录用户名,如果在没有安全框架的项目里,可以直接让用户手动输入昵称,再把昵称作为 sender 传给后端。
function sendPublicMessage(content) {
var message = {
type: 'CHAT',
sender: currentUser,
content: content
};
stompClient.send('/app/chat.send', {}, JSON.stringify(message));
}
function sendPrivateMessage(receiver, content) {
var message = {
type: 'CHAT',
sender: currentUser,
receiver: receiver,
content: content
};
stompClient.send('/app/chat.private', {}, JSON.stringify(message));
}
这里使用 JSON.stringify 将对象转成字符串,STOMP 协议对消息体没有强制格式要求,但是统一使用 JSON 可以减少服务端解析成本。服务端控制器方法通过 @Payload 自动反序列化成 ChatMessage 对象,前提是 Jackson 依赖存在。Spring Boot 默认已经包含 Jackson,一般不需要额外配置。
订阅 /user/queue/messages 时,如果服务端没有认证,客户端拿不到用户身份,订阅会失败或消息无法送达。一个简单的处理方式是继承 DefaultHandshakeHandler,在 determineUser 方法里根据请求参数返回一个 Principal。虽然这种方式不够安全,但适合本地调试和演示。
四、点对点消息的分发过程与注意事项
点对点消息的传递链路比群聊更复杂。客户端订阅的是 /user/queue/messages,但服务端调用 convertAndSendToUser 时写的是 /queue/messages。Spring 的消息处理组件 UserDestinationMessageHandler 会拦截以 /user/ 开头的订阅,把用户会话与目的地关联起来;发送时会根据 Principal 解析出真实的会话 ID,再把消息推送到对应的客户端。这个过程要求 WebSocket 会话在握手阶段绑定用户身份,如果 Principal 为空,消息会因找不到目标而静默丢失。
群聊的广播比较简单,服务端将消息发送到 /topic/public 后,消息代理会检查所有订阅该目的地的会话,并逐个推送。简单消息代理会将订阅信息保存在内存中,因此服务端重启后订阅关系会丢失,但不影响实时消息。对于需要持久化离线消息的场景,建议引入外部代理或数据库,而不是依赖 WebSocket 本身。
另一个容易忽略的点是消息序列化与反序列化。默认情况下,Spring 使用 Jackson 处理 JSON,如果 ChatMessage 类的字段名与前端发送的 JSON key 不一致,需要添加 @JsonProperty 注解。字段类型也要匹配,例如 content 为字符串,前端不要传数字,否则可能抛出转换异常。
五、连接认证与异常处理优化
生产环境中不应该让前端带着用户名参数直接连接 WebSocket。可以在握手拦截器中校验 JWT 或 Session,通过后把用户信息写入 attributes,再在 determineUser 中读取。Spring Security 与 WebSocket 集成后,可以直接从 Principal 获取当前用户。配置 HandshakeInterceptor 可以统一处理跨域、Token 传递等问题。
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import java.util.Map;
public class AuthHandshakeInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Map<String, Object> attributes) {
// 从 URL 或 Header 中提取 token,写入 attributes
String token = request.getURI().getQuery();
attributes.put("token", token);
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Exception exception) {
}
}
注意这里的 Map<String, Object> 在代码块中已经做了转义,显示为 Map 类型。拦截器返回 true 表示允许握手,false 则拒绝连接。实际项目中应当在 beforeHandshake 里校验 token 有效性,避免匿名用户占用连接资源。
断线重连可以通过前端监听连接断开事件来实现,SockJS 本身提供一定程度的自动重试,但 STOMP 连接过期后需要重新订阅。心跳机制可以在服务端配置 setHeartbeatValue,或在客户端 connect 时设置 heartbeat 参数。例如 stompClient.heartbeat.outgoing = 20000; 可以让客户端每 20 秒发送一次心跳,帮助代理及时清理失效会话。
Spring BootWebSocket点对点群聊修改时间:2026-09-19 19:32:34