SpringBoot 集成 WebSocket 实现群聊和单聊功能,核心是基于 Spring 官方的spring-websocket模块,结合SimpleBroker(简单消息代理)实现消息的发布 / 订阅,再通过用户标识绑定处理单聊、广播地址处理群聊。
下面给出完整可运行的实现方案,包含配置类、消息实体、WebSocket 处理器、前端测试页面,兼顾易用性和 SpringBoot 最佳实践,适配 SpringBoot 2.x/3.x(3.x 仅需调整依赖坐标,代码无差异)。
一、核心依赖引入
首先在pom.xml中添加 WebSocket 核心依赖,SpringBoot 已做自动配置,无需额外引入容器依赖。
SpringBoot 2.x 依赖
<!-- WebSocket核心依赖 --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId></dependency><!-- Web基础依赖(用于前端页面访问) --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>SpringBoot 3.x 依赖(仅调整 jakarta 包,依赖坐标不变)
上述依赖直接使用即可,SpringBoot 3.x 会自动适配 jakarta.servlet-api,无需额外修改。
二、WebSocket 核心配置类
配置消息代理(处理消息转发)、端点注册(前端连接的入口)、消息映射(前端发送消息的路径),并开启用户认证绑定(为单聊做准备,通过Principal标识用户)。
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;/** * WebSocket配置类:开启STOMP协议的WebSocket消息代理,实现发布/订阅 * STOMP:简单文本定向消息协议,基于WebSocket封装,简化消息通信 */@Configuration@EnableWebSocketMessageBroker // 开启WebSocket消息代理,支持STOMP协议public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { /** * 配置消息代理:设置消息的订阅前缀(前端订阅此前缀的地址接收消息) */ @Override public void configureMessageBroker(MessageBrokerRegistry config) { // 启用简单消息代理(内存级,生产环境可替换为RabbitMQ/ActiveMQ) // 配置**群聊/单聊的消息接收前缀**,前端订阅以"/topic"(群聊)、"/user"(单聊)开头的地址 config.enableSimpleBroker("/topic", "/user"); // 配置**前端发送消息的前缀**,前端发送消息的地址需以"/app"开头,后端通过@MessageMapping匹配 config.setApplicationDestinationPrefixes("/app"); // 配置**单聊的用户前缀**,后端向指定用户发送消息时,会自动拼接"/user/{userId}/",前端订阅"/user/自己的id/xxx" config.setUserDestinationPrefix("/user"); } /** * 注册WebSocket端点:前端通过此地址建立WebSocket连接 * setAllowedOriginPatterns:允许跨域(生产环境可指定具体域名) * withSockJS:开启SockJS支持,当浏览器不支持WebSocket时,自动降级为轮询 */ @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws/connect") // 核心连接端点 .setAllowedOriginPatterns("*") // 允许所有跨域(生产环境谨慎使用) .withSockJS(); // 开启SockJS降级 }}关键概念说明:
- STOMP 协议:WebSocket 是底层协议,无统一的消息格式,STOMP 为其封装了发布 / 订阅、点对点的消息模型,简化开发;
- /topic:广播前缀,发送到此前缀的消息,所有订阅的客户端都能接收(用于群聊);
- /user:用户前缀,发送到此前缀的消息,仅指定用户能接收(用于单聊);
- /app:前端发送消息的前缀,后端通过@MessageMapping匹配此路径下的消息。
三、消息实体定义
定义前端发送的消息和后端推送的消息实体,统一消息格式,使用lombok简化 get/set(可手动实现)。
<!-- 引入lombok(可选) --><dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional></dependency>import lombok.Data;import java.util.Date;/** * 前端发送的消息实体 */@Datapublic class ChatMessage { // 消息类型:GROUP(群聊)、SINGLE(单聊) private MessageType type; // 发送者ID private String senderId; // 接收者ID(群聊时可为空/固定值,单聊时为目标用户ID) private String receiverId; // 消息内容 private String content; // 发送时间 private Date sendTime = new Date(); // 消息类型枚举 public enum MessageType { GROUP, SINGLE }}import lombok.Data;import java.util.Date;/** * 后端推送给前端的消息实体(可与发送实体复用,拆分更灵活) */@Datapublic class ResponseMessage { // 发送者ID private String senderId; // 接收者ID(群聊为"all",单聊为目标用户ID) private String receiverId; // 消息内容 private String content; // 发送时间 private Date sendTime; // 消息类型 private String type;}四、WebSocket 消息处理器(核心业务)
通过@Controller+@MessageMapping接收前端消息,结合SimpMessagingTemplate实现群聊广播和单聊点对点推送,这是业务处理的核心。
import org.springframework.beans.factory.annotation.Autowired;import org.springframework.messaging.handler.annotation.MessageMapping;import org.springframework.messaging.handler.annotation.SendTo;import org.springframework.messaging.simp.SimpMessagingTemplate;import org.springframework.stereotype.Controller;/** * WebSocket消息处理器:接收前端消息,处理群聊/单聊逻辑,推送消息给目标客户端 */@Controllerpublic class ChatController { /** * 核心工具类:用于向指定地址/指定用户发送消息 * Spring自动注入,封装了WebSocket消息发送的底层细节 */ @Autowired private SimpMessagingTemplate messagingTemplate; /** * 群聊接口:前端发送消息到"/app/chat/group",后端接收并广播到所有订阅"/topic/group"的客户端 * @MessageMapping:匹配前端发送的消息路径(拼接配置的/app前缀) * @SendTo:自动将返回值广播到指定的订阅地址 */ @MessageMapping("/chat/group") @SendTo("/topic/group") public ResponseMessage groupChat(ChatMessage chatMessage) { // 封装返回消息 ResponseMessage response = new ResponseMessage(); response.setSenderId(chatMessage.getSenderId()); response.setReceiverId("all"); response.setContent(chatMessage.getContent()); response.setSendTime(chatMessage.getSendTime()); response.setType("群聊"); return response; } /** * 单聊接口:前端发送消息到"/app/chat/single",后端推送给指定的接收者 * 单聊无需@SendTo,通过SimpMessagingTemplate的convertAndSendToUser方法定向推送 */ @MessageMapping("/chat/single") public void singleChat(ChatMessage chatMessage) { // 封装返回消息 ResponseMessage response = new ResponseMessage(); response.setSenderId(chatMessage.getSenderId()); response.setReceiverId(chatMessage.getReceiverId()); response.setContent(chatMessage.getContent()); response.setSendTime(chatMessage.getSendTime()); response.setType("单聊"); /** * 定向发送给指定用户:convertAndSendToUser(接收者ID, 订阅后缀, 消息体) * 底层会自动拼接为:/user/{receiverId}/topic/single(与前端订阅地址一致) * 第三个参数true:表示启用用户前缀(与配置的setUserDestinationPrefix一致) */ messagingTemplate.convertAndSendToUser( chatMessage.getReceiverId(), "/topic/single", response, null // 消息头,无需时传null ); }}核心方法说明:
- convertAndSendToUser:Spring 提供的单聊核心方法,自动处理用户标识的绑定,无需手动拼接用户地址;
- @SendTo:群聊专用,自动将方法返回值广播到指定的/topic地址,所有订阅该地址的客户端都能接收;
- 消息路径匹配规则:前端发送/app/chat/group → 后端@MessageMapping("/chat/group") 匹配(自动拼接/app前缀)。
五、前端测试页面(关键)
编写 HTML+JS 前端页面,使用STOMP.js和SockJS.js实现 WebSocket 连接、消息发送、消息接收,可直接放在resources/static目录下(SpringBoot 默认静态资源目录),启动项目后通过http://localhost:8080/chat.html访问。
创建resources/static/chat.html文件,内容如下:
<!DOCTYPE html><html lang="zh-CN"><head> <meta charset="UTF-8"> <title>SpringBoot WebSocket 群聊&单聊</title> <style> .chat-box { width: 600px; margin: 20px auto; } .msg-list { height: 400px; border: 1px solid #ccc; padding: 10px; overflow-y: auto; margin-bottom: 10px; } .msg { margin: 5px 0; padding: 8px; border-radius: 4px; } .group-msg { background: #e6f7ff; } .single-msg { background: #f0f9eb; } .input-box { display: flex; gap: 10px; } input, button { padding: 8px; font-size: 14px; } input { flex: 1; } </style> <!-- 引入STOMP和SockJS依赖(CDN) --> <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script></head><body><div class="chat-box"> <h3>WebSocket 群聊&单聊测试</h3> <div> <input type="text" id="senderId" placeholder="请输入自己的ID" value="user1"> <button onclick=connect()">连接WebSocket</button> <button onclick=disconnect()">断开连接</button> </div> <!-- 群聊区域 --> <div class="msg-list" id="groupMsgList">群聊消息:</div> <div class="input-box"> <input type="text" id="groupContent" placeholder="请输入群聊内容"> <button onclick=sendGroupMsg()">发送群聊</button> </div> <!-- 单聊区域 --> <div class="msg-list" id="singleMsgList">单聊消息:</div> <div class="input-box"> <input type="text" id="receiverId" placeholder="请输入接收者ID" value="user2"> <input type="text" id="singleContent" placeholder="请输入单聊内容"> <button onclick=sendSingleMsg()">发送单聊</button> </div></div><script> var stompClient = null; // STOMP客户端实例 // 1. 建立WebSocket连接 function connect() { const senderId = document.getElementById("senderId").value; if (!senderId) { alert("请输入自己的ID!"); return; } // 创建SockJS连接,指向后端注册的端点 const socket = new SockJS('/ws/connect'); // 初始化STOMP客户端 stompClient = Stomp.over(socket); // 建立连接(可传递请求头,如token,用于用户认证) stompClient.connect({senderId: senderId}, function (frame) { console.log("连接成功:" + frame); alert("WebSocket连接成功!"); // 2. 订阅**群聊消息**:后端广播到/topic/group的消息,此处接收 stompClient.subscribe('/topic/group', function (msg) { const data = JSON.parse(msg.body); showMsg(data, "groupMsgList", "group-msg"); }); // 3. 订阅**自己的单聊消息**:后端推送的/user/{senderId}/topic/single,STOMP自动拼接用户ID stompClient.subscribe('/user/topic/single', function (msg) { const data = JSON.parse(msg.body); showMsg(data, "singleMsgList", "single-msg"); }); }, function (error) { console.error("连接失败:", error); alert("WebSocket连接失败,请重试!"); }); } // 断开WebSocket连接 function disconnect() { if (stompClient) { stompClient.disconnect(); stompClient = null; alert("已断开WebSocket连接!"); console.log("连接已断开"); } } // 发送群聊消息 function sendGroupMsg() { if (!stompClient) { alert("请先连接WebSocket!"); return; } const senderId = document.getElementById("senderId").value; const content = document.getElementById("groupContent").value; if (!content) { alert("请输入群聊内容!"); return; } // 发送消息到后端/app/chat/group路径 stompClient.send("/app/chat/group", {}, JSON.stringify({ type: "GROUP", senderId: senderId, content: content })); // 清空输入框 document.getElementById("groupContent").value = ""; } // 发送单聊消息 function sendSingleMsg() { if (!stompClient) { alert("请先连接WebSocket!"); return; } const senderId = document.getElementById("senderId").value; const receiverId = document.getElementById("receiverId").value; const content = document.getElementById("singleContent").value; if (!receiverId || !content) { alert("请输入接收者ID和单聊内容!"); return; } // 发送消息到后端/app/chat/single路径 stompClient.send("/app/chat/single", {}, JSON.stringify({ type: "SINGLE", senderId: senderId, receiverId: receiverId, content: content })); // 清空输入框 document.getElementById("singleContent").value = ""; } // 展示消息到页面 function showMsg(data, listId, className) { const list = document.getElementById(listId); const msgDiv = document.createElement("div"); msgDiv.className = "msg " + className; msgDiv[xss_clean] = ` <span style="color: #999; font-size: 12px;">${data.sendTime}</span> <br> <span style="font-weight: bold;">${data.senderId}</span> → <span style="color: #666;">${data.receiverId}</span>: <span>${data.content}</span> `; list.appendChild(msgDiv); // 滚动到最新消息 list.scrollTop = list.scrollHeight; } // 页面关闭时断开连接 _window.onbeforeunload = function () { disconnect(); };</script></body></html>前端核心逻辑:
- 通过SockJS连接后端/ws/connect端点,降级处理不支持 WebSocket 的浏览器;
- 用STOMP.over(socket)创建 STOMP 客户端,通过stompClient.connect建立连接;
- 订阅/topic/group(群聊)和/user/topic/single(单聊),接收后端推送的消息;
- 通过stompClient.send向/app/xxx路径发送消息,后端@MessageMapping匹配处理。
六、测试运行(一步到位)
- 启动 SpringBoot 项目,确保端口为 8080(可在application.yml中修改);
- 打开两个及以上浏览器标签页,访问http://localhost:8080/chat.html;
- 第一个标签页输入 ID 为user1,点击连接 WebSocket;
- 第二个标签页输入 ID 为user2,点击连接 WebSocket;
- 群聊测试:在user1的群聊输入框输入内容,点击发送,user2能收到群聊消息,反之亦然;
- 单聊测试:在user1的单聊接收者 ID 输入user2,输入单聊内容发送,仅user2能收到单聊消息;user2向user1发送单聊,仅user1能收到。
七、生产环境优化(关键扩展)
上述方案为基础可运行版本,生产环境需做以下优化,保证稳定性和安全性:
1. 跨域限制(替代通配符 *)
生产环境禁止使用setAllowedOriginPatterns("*"),指定具体的前端域名:

// 示例:允许http://localhost:3000和https://your-domain.com跨域registry.addEndpoint("/ws/connect") .setAllowedOriginPatterns("http://localhost:3000", "https://your-domain.com") .withSockJS();2. 用户认证(替代简单的 senderId)
实际项目中需结合Token/JWT/Session做用户认证,避免伪造用户 ID:
- 前端连接时,在stompClient.connect的请求头中传递 Token:
- javascript
- 运行
- stompClient.connect({token: "用户的JWT Token"}, function (frame) { ... });
- 后端实现ChannelInterceptor,拦截连接请求,解析 Token 获取真实用户 ID,绑定到Principal:
- java
- 运行
- @Component public class WebSocketAuthInterceptor implements ChannelInterceptor { @Override public Message<?> preSend(Message<?> message, MessageChannel channel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); // 处理CONNECT帧,解析Token if (StompCommand.CONNECT.equals(accessor.getCommand())) { String token = accessor.getFirstNativeHeader("token"); // 解析Token获取用户ID(自己实现JWT解析逻辑) String userId = JwtUtil.getUserIdFromToken(token); if (userId == null) { throw new AccessDeniedException("WebSocket连接认证失败,Token无效"); } // 将用户ID绑定到Principal,供后续单聊使用 accessor.setUser(() -> userId); } return message; } }
- 在 WebSocket 配置类中注册拦截器:
- java
- 运行
- @Autowired private WebSocketAuthInterceptor authInterceptor; @Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors(authInterceptor); }
3. 替换消息代理(内存→MQ)
默认的enableSimpleBroker是内存级消息代理,集群部署时会出现消息不通的问题,生产环境需替换为RabbitMQ/ActiveMQ(以 RabbitMQ 为例):
- 引入 RabbitMQ 依赖:
- xml
- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
- 配置 RabbitMQ 地址:
- yaml
- spring: rabbitmq: host: 127.0.0.1 port: 5672 username: guest password: guest
- 修改 WebSocket 配置,启用 RabbitMQ 作为消息代理:
- java
- 运行
- // 替换enableSimpleBroker为enableStompBrokerRelay config.enableStompBrokerRelay("/topic", "/user") .setRelayHost("127.0.0.1") // RabbitMQ地址 .setRelayPort(61613) // RabbitMQ的STOMP端口 .setClientLogin("guest") .setClientPasscode("guest");
4. 异常处理与连接监控
- 实现WebSocketHandlerDecoratorFactory监控连接的建立 / 断开;
- 增加全局异常处理器,捕获消息处理中的异常,避免服务崩溃;
- 限制单个用户的连接数,防止恶意连接。
八、核心知识点总结
- SpringBoot WebSocket基于spring-websocket模块,结合 STOMP 协议实现发布 / 订阅,简化消息通信;
- 群聊通过广播实现:后端@SendTo("/topic/group"),前端订阅/topic/group;
- 单聊通过用户定向推送实现:后端messagingTemplate.convertAndSendToUser,前端订阅/user/topic/single;
- SockJS用于 WebSocket 降级,保证低版本浏览器的兼容性。
九、常见问题排查
- 连接失败:检查端点地址是否正确(前端SockJS('/ws/connect')与后端addEndpoint("/ws/connect")一致)、跨域配置是否正确;
- 单聊收不到消息:检查接收者 ID 是否正确、后端convertAndSendToUser的订阅后缀是否与前端一致(如/topic/single);
- 群聊收不到消息:检查前端是否订阅了/topic/group、后端是否加了@SendTo("/topic/group");
- SpringBoot 3.x 报错:确认依赖为最新版本,3.x 使用 jakarta 包,无需修改代码,直接使用上述配置即可。