SpringBoot集成WebSocket:构建高可用实时消息推送系统的完整指南
在现代Web应用中,实时双向通信已成为提升用户体验的关键技术。无论是双屏数据同步、在线协作还是实时监控仪表盘,都需要后端服务能够主动、即时地将数据推送到前端。本文将深入探讨如何在SpringBoot项目中,以生产级标准集成WebSocket,构建一个健壮、可扩展的实时消息推送系统。
一、WebSocket:超越传统轮询的实时通信方案
在传统的HTTP请求-响应模式下,实现实时更新通常依赖于轮询或长轮询,这不仅增加了服务器负载,还带来了显著的延迟。WebSocket协议的出现彻底改变了这一局面。作为HTML5的核心规范之一,它建立在TCP之上,通过在单个TCP连接上提供全双工、双向的通信通道,实现了真正的低延迟实时数据交换。
对于后端架构而言,集成WebSocket意味着能够更高效地处理诸如即时聊天、实时数据可视化、订单状态推送等场景。与传统的轮询机制相比,WebSocket能够:
- 大幅降低服务器压力:建立一次连接,即可持续通信,避免频繁的HTTP握手。
- 提升响应实时性:服务端可以随时主动推送数据,无需等待客户端请求。
- 减少网络开销:每个消息的头部信息远小于HTTP请求。
在微服务架构中,WebSocket服务可以作为独立的中间件组件,通过清晰的API与其他业务服务(如用户服务、订单服务)解耦,专门负责管理连接和消息路由。
二、项目环境与核心依赖配置
为了确保最佳的兼容性和稳定性,我们选择SpringBoot 2.7.x这一经过广泛验证的版本。这个版本对WebSocket的支持已经非常成熟,且避免了新版本可能引入的不稳定因素。项目的基础依赖非常简单,主要引入SpringBoot官方封装的WebSocket Starter。
在pom.xml文件中,我们只需添加以下核心依赖:
<!-- SpringBoot集成WebSocket核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 可选:SpringMVC基础依赖(项目已引入可忽略) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
这个依赖已经包含了我们所需的所有底层实现。无需额外引入复杂的Netty或其他容器特定的库,SpringBoot已经为我们做好了抽象和整合,这体现了其“约定优于配置”的哲学,极大简化了后端开发的复杂度。
[AFFILIATE_SLOT_1]三、解决部署兼容性的核心配置
在实际企业部署中,应用可能以Jar包(内嵌Tomcat)或War包(部署到外部Tomcat)的形式运行。这两种方式对WebSocket端点的初始化要求不同,处理不当会导致启动失败。这是集成WebSocket时最常见的“坑”之一。
关键问题:使用@ServerEndpoint注解时,必须向Spring容器注册一个ServerEndpointExporter Bean。然而:
- Jar包部署(内嵌Tomcat):必须手动创建此Bean。
- War包部署(外部Tomcat):容器会自动初始化,手动创建会导致Bean冲突。
我们的解决方案是创建一个智能的条件配置类,动态判断运行环境。首先,定义一个条件判断类:
/**
* All rights reserved.
*/
package com.itl.framework.config;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.ClassUtils;
/**
* 类描述:WebSocket条件判断类,控制ServerEndpointExporter是否创建
* jar包部署(内嵌Tomcat)返回true,war包部署(外部Tomcat)返回false
* @author itl
* @version 1.0
*
* 修订历史:
* 日期 修订者 修订描述
* 2026-02-05 xxx 修复matches方法固定返回false问题,实现jar/war包部署动态判断
*/
public class WebSocketAutoWired implements Condition {
/**
* 核心判断方法:jar包部署(内嵌Tomcat)为true; war包部署(外部Tomcat)为false
*/
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 判断类加载器中是否存在内嵌Tomcat核心类 → 存在=jar包部署,不存在=war包部署
return ClassUtils.isPresent(
"org.apache.catalina.startup.Tomcat",
context.getClassLoader()
);
}
}
接着,创建WebSocket配置类,使用@Conditional注解关联上述条件:
/**
*
* All rights reserved.
*/
package com.itl.framework.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* 类描述:WebSocket核心配置类
* 动态创建ServerEndpointExporter,解决内嵌Tomcat/外部Tomcat部署兼容问题
* @author itl
* @version 1.0
* 新增条件注解,适配内嵌/外部Tomcat
*/
@Configuration
public class WebSocketConfig {
/**
* 注册WebSocket端点处理器,仅内嵌Tomcat(jar包)时创建
* 外部Tomcat(war包)由容器自身初始化,无需手动创建
*/
@Bean
@Conditional(WebSocketAutoWired.class)
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
✅ 核心原理:项目启动时,Spring会根据WebSocketAutoWired.matches()方法的返回值,智能决定是否创建ServerEndpointExporter Bean,从而从根本上解决部署环境冲突问题,实现一份代码,两种部署方式无缝运行。
四、构建生产级的连接管理与消息工具
一个健壮的WebSocket服务,离不开对客户端连接的集中、安全的管理。我们设计一个WebSocketUtils工具类,它作为整个实时通信系统的中间件核心,负责Session的存储、消息的路由与发送。
工具类需要解决几个关键问题:
- 线程安全:使用
ConcurrentHashMap存储连接,避免多线程并发问题。 - 多端登录:同一用户(如userId)可能在多个设备(浏览器标签页、不同浏览器)同时连接,需要支持向所有活跃连接广播消息。
- 连接健康度:及时清理已关闭或无效的Session,防止内存泄漏。
以下是工具类的核心实现:
/**
* All rights reserved.
*/
package com.itl.common.utils;
import java.util.Map;
import java.util.Set;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import javax.websocket.Session;
/**
* 类描述:WebSocket工具类,管理客户端Session和消息发送
* @author itl
*
* 修订历史:
* 日期 修订者 修订描述
* 优化Session管理,支持单用户多连接;增加异常处理和Session有效性判断
*/
public class WebSocketUtils {
// 存储客户端连接:key=用户ID,value=该用户的所有Session连接(支持多端登录)
public static Map<String, Set<Session>> clients = new ConcurrentHashMap<>();
/**
* 添加客户端连接
* @param userId 用户唯一标识
* @param session 客户端会话
*/
public static void add(String userId, Session session) {
// 不存在则创建新的Set,存在则直接添加;ConcurrentHashMap.newKeySet()保证线程安全
clients.computeIfAbsent(userId, k -> ConcurrentHashMap.newKeySet()).add(session);
}
/**
* 处理客户端发送的消息(可根据业务自定义)
* @param userId 发送消息的用户ID
* @param message 消息内容
*/
public static void receive(String userId, String message) {
// 示例:双屏联动,左屏消息推右屏,右屏消息推左屏
if ("left".equals(userId)) {
sendMessage("right", "左屏推送:" + message);
} else if ("right".equals(userId)) {
sendMessage("left", "右屏推送:" + message);
}
System.out.println("收到用户[" + userId + "]的消息:" + message);
}
/**
* 精准移除某用户的某一个Session连接(连接关闭/异常时调用)
* @param userId 用户唯一标识
* @param session 要移除的会话
*/
public static void remove(String userId, Session session) {
Set<Session> sessions = clients.get(userId);
if (sessions != null) {
sessions.remove(session);
// 若该用户无任何连接,移除key,避免空集合占用内存
if (sessions.isEmpty()) {
clients.remove(userId);
}
}
}
/**
* 移除某用户的所有连接
* @param userId 用户唯一标识
*/
public static void remove(String userId) {
clients.remove(userId);
}
/**
* 向指定用户发送消息
* @param userId 接收消息的用户ID
* @param message 消息内容
* @return 成功发送的连接数
*/
public static int sendMessage(String userId, String message) {
Set<Session> sessions = clients.get(userId);
// 无该用户连接,直接返回0
if (sessions == null || sessions.isEmpty()) {
return 0;
}
int successCount = 0;
Iterator<Session> it = sessions.iterator();
while (it.hasNext()) {
Session session = it.next();
// 判断Session是否有效(连接未关闭)
if (!session.isOpen()) {
it.remove(); // 移除失效Session,避免内存泄漏
continue;
}
try {
// 异步发送消息(推荐),同步发送使用session.getBasicRemote().sendText(message)
session.getAsyncRemote().sendText(message);
successCount++;
} catch (Exception e) {
it.remove(); // 发送失败,移除失效Session
e.printStackTrace(); // 实际项目建议使用日志框架(如Logback/Log4j2)
}
}
// 清理空集合
if (sessions.isEmpty()) {
clients.remove(userId);
}
return successCount;
}
}
设计亮点:将存储结构从Map<String, Session>改为Map<String, Set<Session>>,是支持多端登录的关键。同时,在发送消息前检查session.isOpen(),并捕获所有可能的IOException,确保了系统的高可用性。
五、实现WebSocket服务端端点与业务逻辑
服务端端点类使用@ServerEndpoint注解定义,它处理WebSocket生命周期的四个核心事件:连接建立、接收消息、连接关闭和发生错误。我们将业务逻辑集中于此。
端点地址设计为/connect/{userId},其中userId(如“left”、“right”)用于标识连接方,这在双屏联动场景中非常直观。核心实现如下:
/**
* All rights reserved.
*/
package com.itl.framework.web.service;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
import com.itl.common.utils.WebSocketUtils;
/**
* 类描述:WebSocket服务端端点,处理客户端连接和事件回调
* 服务端地址:/connect/{userId}
* @author itl
* 修复onError方法参数注解问题;优化连接管理,精准移除Session
*/
@ServerEndpoint("/connect/{userId}") // WebSocket连接地址,{userId}为用户唯一标识
@Component // 必须交给Spring管理,否则无法扫描
public class WebSocketService {
/**
* 连接打开事件(客户端首次连接时调用)
* @param userId 路径参数中的用户ID
* @param session 客户端会话
*/
@OnOpen
public void onOpen(@PathParam("userId") String userId, Session session) {
System.out.println("【WebSocket】连接打开成功!");
WebSocketUtils.add(userId, session);
System.out.println("【WebSocket】用户" + userId + "上线,当前在线人数:" + WebSocketUtils.clients.size());
}
/**
* 接收客户端消息事件
* @param userId 发送消息的用户ID
* @param message 客户端发送的消息
* @return 服务端向客户端的回执消息
*/
@OnMessage
public String onMessage(@PathParam("userId") String userId, String message) {
// 心跳检测(可选),客户端发送&时,服务端回执&,避免连接被断开
if (message.equals("&")) {
return "&";
} else {
// 调用工具类处理消息
WebSocketUtils.receive(userId, message);
return "【服务端回执】已收到消息:" + message;
}
}
/**
* 连接异常事件(网络中断、客户端崩溃等)
* 注意:@OnError注解不支持@PathParam参数,会导致参数解析异常
* @param session 异常的客户端会话
* @param throwable 异常信息
*/
@OnError
public void onError(Session session, Throwable throwable) {
// 遍历移除该失效的Session
WebSocketUtils.clients.forEach((userId, sessions) -> {
WebSocketUtils.remove(userId, session);
});
throwable.printStackTrace();
System.out.println("【WebSocket】连接异常,已移除失效会话");
}
/**
* 连接关闭事件(客户端主动关闭连接)
* @param userId 断开连接的用户ID
* @param session 关闭的客户端会话
*/
@OnClose
public void onClose(@PathParam("userId") String userId, Session session) {
System.out.println("【WebSocket】连接关闭成功!");
WebSocketUtils.remove(userId, session);
System.out.println("【WebSocket】用户" + userId + "下线,当前在线人数:" + WebSocketUtils.clients.size());
}
}
⚠️ 注意事项:
- 务必添加
@Component注解,否则Spring无法管理该Bean。 @OnError方法无法直接使用@PathParam获取参数,需要通过Session反向查找。- 实现了简单的心跳检测(处理“&”消息),以保持长时间连接活跃,避免被防火墙或服务器中断。
此外,我们提供一个标准的HTTP API接口,允许其他后端服务或定时任务触发WebSocket消息推送,实现业务系统与实时推送系统的解耦。
import com.itl.common.utils.WebSocketUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* WebSocket测试控制器,双屏消息互推接口
* @author itl
* @date 2026-02-05
*/
@RestController
@RequestMapping("/websocket")
@Api(tags = "WebSocket测试接口")
public class WebSocketController {
/**
* 接收左屏消息并推送至右屏
* @param message 消息内容
* @return 推送结果(1=成功,0=失败)
*/
@ApiOperation(value = "左屏推右屏", notes = "HTTP接口触发,向右屏推送消息")
@ApiImplicitParam(name = "message", value = "推送的消息内容", required = true, dataType = "String")
@GetMapping(value = "/right")
public AjaxResult right(String message) {
// toAjax:通用工具类,1=成功,0=失败
return toAjax(WebSocketUtils.sendMessage("right", message));
}
/**
* 接收右屏消息并推送至左屏
* @param message 消息内容
* @return 推送结果(1=成功,0=失败)
*/
@ApiOperation(value = "右屏推左屏", notes = "HTTP接口触发,向左屏推送消息")
@ApiImplicitParam(name = "message", value = "推送的消息内容", required = true, dataType = "String")
@GetMapping(value = "/left")
public AjaxResult left(String message) {
return toAjax(WebSocketUtils.sendMessage("left", message));
}
/**
* 通用响应结果封装(项目已实现可忽略)
* @param rows 成功数
* @return AjaxResult
*/
private AjaxResult toAjax(int rows) {
return rows > 0 ? AjaxResult.success() : AjaxResult.error();
}
}
[AFFILIATE_SLOT_2]
六、前端测试、部署与总结
完成服务端开发后,可以通过多种方式进行测试。对于快速验证,推荐使用在线的WebSocket测试工具。对于集成测试,可以编写简单的HTML页面。
一个简单的前端测试页面示例如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WebSocket双屏测试</title>
</head>
<body>
<h3>WebSocket双屏联动测试(<span id="screenType">左屏</span>)</h3>
<input type="text" id="msgInput" placeholder="请输入消息内容">
<button onclick="sendMsg()">发送消息</button>
<div id="msgList" style="margin-top: 20px; width: 500px; height: 300px; border: 1px solid #ccc; padding: 10px; overflow-y: auto;"></div>
<script>
// 定义用户ID,left=左屏,right=右屏
const userId = "left";
document.getElementById("screenType").innerText = userId === "left" ? "左屏" : "右屏";
// WebSocket连接地址,替换为自己的服务端地址
const ws = new WebSocket("ws://localhost:8080/connect/" + userId);
// 连接成功回调
ws.onopen = function() {
addMsg("【系统提示】WebSocket连接成功!");
};
// 接收消息回调
ws.onmessage = function(event) {
addMsg("【收到消息】" + event.data);
};
// 连接关闭回调
ws.onclose = function() {
addMsg("【系统提示】WebSocket连接关闭!");
};
// 连接异常回调
ws.onerror = function() {
addMsg("【系统提示】WebSocket连接异常!");
};
// 发送消息
function sendMsg() {
const msg = document.getElementById("msgInput").value;
if (!msg) {
alert("请输入消息内容!");
return;
}
ws.send(msg);
addMsg("【发送消息】" + msg);
document.getElementById("msgInput").value = "";
}
// 追加消息到页面
function addMsg(content) {
const msgList = document.getElementById("msgList");
const div = document.createElement("div");
div.style.margin = "5px 0";
div.innerText = new Date().toLocaleString() + " - " + content;
msgList.appendChild(div);
// 滚动到底部
msgList.scrollTop = msgList.scrollHeight;
}
// 心跳检测,每30秒发送一次&,防止连接断开
setInterval(() => {
ws.send("&");
}, 30000);
</script>
</body>
</html>
在部署方面,我们的配置已完美适配两种方式。对于Jar包部署(SpringBoot默认):
<packaging>jar</packaging >
对于War包部署到外部Tomcat,需要修改打包方式并调整启动类:
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 排除内嵌Tomcat -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入servlet-api依赖 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
@SpringBootApplication
public class Application extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
总结:本文详细阐述了在SpringBoot中构建生产级WebSocket服务的完整路径。从解决部署兼容性这一核心痛点出发,设计了线程安全、支持多端登录的连接管理工具,实现了包含心跳检测的健壮端点,并提供了灵活的测试方式。这套方案不仅适用于双屏联动,其架构也易于扩展到在线客服、实时数据监控、协同编辑等更复杂的实时交互场景中,是提升现代Web应用用户体验的强大技术支撑。
浙公网安备 33010602011771号