前言
在项目开发中,实时消息推送是高频需求,比如双屏联动、大屏监控、在线聊天、订单状态推送等场景。WebSocket 作为 HTML5 的核心特性,实现了浏览器与服务器的全双工双向通信,相比传统的轮询 / 长轮询方式,大幅降低服务端压力,提升实时性和用户体验。
本文以SpringBoot 2.7.x (最稳定版本,零基础友好)为基础,手把手教大家从 0 到 1 集成 WebSocket,实现左屏 / 右屏双端实时消息互推功能。全程代码可直接复制使用,兼顾Jar 包内嵌 Tomcat和War 包外部 Tomcat两种部署方式,解决部署冲突问题,同时完善异常处理、连接管理、心跳检测等生产级细节,小白跟着步骤走就能跑通。
本文核心优势
- 零基础友好:代码全复制、步骤全拆解,无复杂配置,新手直接用;
- 部署无坑:自动适配 Jar/War 包部署,无需手动修改代码,避免容器冲突;
- 生产级健壮:完善的异常处理、失效连接清理、心跳检测,防止内存泄漏;
- 支持多端登录:同一用户多设备连接,所有端都能收到消息,避免 Session 覆盖;
- 双测试方式:在线工具快速验证 + 自定义 HTML 页面,前端后端全打通;
- 配套全补全:统一响应类、启动类改造等缺失代码全部补全,无需额外找依赖。
一、环境准备(新手必看)
1.1 基础开发环境
无需高版本,基础环境即可运行,推荐搭配:
- JDK:1.8(兼容性最好,无版本问题)
- SpringBoot:2.7.10(本文统一版本,避免依赖冲突)
- Maven:3.6.0+
- 开发工具:IDEA/Eclipse(推荐 IDEA,自带 Maven 管理)
- 测试工具:浏览器、WebSocket 在线测试工具
1.2 核心依赖
在pom.xml中引入 SpringBoot 官方的 WebSocket Starter 依赖,无需额外引入其他包,Spring 已做封装:
bash
<!-- 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>
二、核心配置类(解决 Jar/War 部署兼容)
SpringBoot 中使用@ServerEndpoint注解实现 WebSocket 时,必须注册ServerEndpointExporter 让 Spring 扫描并管理 WebSocket 端点,但内嵌 Tomcat(Jar 包)和外部 Tomcat(War 包) 对该 Bean 的要求不同:
Jar 包部署(内嵌 Tomcat):需要手动创建ServerEndpointExporter Bean;
War 包部署(外部 Tomcat):由容器自身初始化 WebSocket,手动创建会导致 Bean 冲突。
因此我们通过Spring 条件注解@Conditional 实现动态判断,自动适配两种部署方式。
- Jar 包部署(内嵌 Tomcat):需要手动创建ServerEndpointExporter Bean;
- War 包部署(外部 Tomcat):由容器自身初始化 WebSocket,手动创建会导致 Bean 冲突。
2.1 自定义条件判断类
创建包com.tydt.framework.config,编写WebSocketAutoWired类,实现Condition接口,核心逻辑是判断是否为内嵌 Tomcat 环境:
bash
/**
* 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()
);
}
}
2.2 WebSocket 核心配置类
编写WebSocketConfig类,通过@Conditional关联上面的条件判断类,动态创建ServerEndpointExporter:
bash
/**
*
* 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,从根本上解决 Jar/War 部署的冲突问题。
三、WebSocket 工具类(连接管理 + 消息发送)
创建工具类WebSocketUtils,用于统一管理客户端 Session 连接、发送消息、移除连接等操作,使用ConcurrentHashMap 保证多线程下的线程安全,同时支持同一用户多端连接(避免 Session 被覆盖 )。
包路径:com.itl.common.utils
bash
/**
* 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()),避免向失效连接发送消息;
- 完善的异常捕获,发送消息失败时自动移除失效 Session,防止内存泄漏;
- 提供精准移除(单 Session)和批量移除(全 Session)两种方法,适配不同场景。
四、WebSocket 服务端端点(核心业务处理)
创建WebSocketService类,使用@ServerEndpoint注解定义 WebSocket 服务端地址,通过@OnOpen、@OnMessage、@OnClose、@OnError注解处理 WebSocket 的连接打开、接收消息、连接关闭、连接异常 四大事件,同时通过@Component注解让 Spring 管理该 Bean。
包路径:com.itl.framework.web.service
bash
/**
* 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());
}
}
核心注意点:
- @ServerEndpoint("/connect/{userId}"):定义 WebSocket 的服务端连接地址,前端通过ws://ip:port/connect/left连接左屏,ws://ip:port/connect/right连接右屏;
- @Component:必须添加,否则 Spring 无法扫描到该端点,配合配置类的ServerEndpointExporter完成注册;
- @OnError方法不支持@PathParam注解:原代码中该注解会导致运行时参数解析异常,直接通过 Session 遍历移除即可;
- 增加心跳检测:客户端定时发送&,服务端回执&,避免因长时间无交互导致连接被防火墙 / 服务器断开。
五、测试接口(HTTP 触发 WebSocket 消息推送)
创建 Controller,提供 HTTP 接口,用于通过后端接口触发 WebSocket 消息推送(比如业务系统调用接口向前端推送消息),实现左屏 / 右屏双端消息互推,同时使用AjaxResult返回统一的响应结果(SpringBoot 项目通用)。
bash
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();
}
}
接口说明:
- 左屏推右屏:GET http://ip:port/websocket/right?message=测试消息
- 右屏推左屏:GET http://ip:port/websocket/left?message=测试消息
- 响应结果:成功返回{"code":200,"msg":"操作成功","data":null},失败返回{"code":500,"msg":"操作失败","data":null}。
六、前端测试(两种方式)
6.1 在线 WebSocket 测试工具(快速验证)
推荐使用在线工具:WebSocket 在线测试,无需编写前端代码,直接测试连接和消息推送。在线测试网站 https://wstool.js.org/
测试步骤:
- 打开两个浏览器窗口,分别访问在线测试工具;
- 第一个窗口连接地址填ws://localhost:8080/connect/left,点击连接,提示 "连接成功";
- 第二个窗口连接地址填ws://localhost:8080/connect/right,点击连接,提示 "连接成功";
- 左屏窗口发送消息Hello 右屏,右屏窗口会收到左屏推送:Hello 右屏;
- 右屏窗口发送消息Hello 左屏,左屏窗口会收到右屏推送:Hello 左屏;
- 调用 HTTP 接口http://localhost:8080/websocket/right?message=接口推右屏,右屏窗口会收到该消息。
6.2 自定义 HTML 测试页面(项目使用)
编写简单的 HTML 页面,通过原生 WebSocket API 实现连接和消息收发,可直接放入项目的resources/static目录下:
bash
<!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>
使用说明:
复制两份页面,分别修改userId为left和right,命名为left.html和right.html;
启动项目后,访问http://localhost:8080/left.html和http://localhost:8080/right.html;
两个页面可互相发送消息,同时支持后端接口推送。
七、部署方式说明
本文的配置已完美适配Jar 包内嵌 Tomcat和War 包外部 Tomcat两种部署方式,无需修改任何代码。
7.1 Jar 包部署(推荐,SpringBoot 默认)
- pom.xml中打包方式为jar:
bash
<packaging>jar</packaging >
- 执行 Maven 命令打包:mvn clean package -DskipTests;
- 运行 Jar 包:java -jar xxx.jar;
- 核心原理:内嵌 Tomcat 环境,WebSocketAutoWired返回true,创建ServerEndpointExporter,WebSocket 正常注册。
7.2 War 包部署(外部 Tomcat)
- pom.xml中修改打包方式为war,并排除内嵌 Tomcat:
bash
<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>
- 修改启动类,继承SpringBootServletInitializer,重写configure方法:
bash
@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);
}
}
- 执行 Maven 命令打包:mvn clean package -DskipTests;
- 将 war 包放入外部 Tomcat 的webapps目录,启动 Tomcat 即可;
- 核心原理:外部 Tomcat 环境,WebSocketAutoWired返回false,不创建ServerEndpointExporter,由 Tomcat 容器自身初始化 WebSocket,避免冲突。
八、常见问题及解决方案
8.1 客户端连接报 404 错误
原因:未创建ServerEndpointExporter Bean,Spring 未扫描到@ServerEndpoint注解;
解决方案:检查配置类WebSocketConfig和条件判断类WebSocketAutoWired是否正确,Jar 包部署时确保matches方法返回true。
8.2 War 包部署到外部 Tomcat 启动报 Bean 冲突
原因:外部 Tomcat 环境下创建了ServerEndpointExporter Bean,与容器自身的 WebSocket 初始化冲突;
解决方案:确保条件判断类WebSocketAutoWired在 War 包部署时返回false,不创建该 Bean。
8.3 发送消息时报 IO 异常
原因:向失效的 Session(连接已关闭 / 网络中断)发送消息,或未做异常捕获;
解决方案:在sendMessage方法中增加session.isOpen()判断,同时捕获异常并移除失效 Session(本文工具类已实现)。
8.4 同一用户多端登录,只有最后一个连接能收到消息
原因:原代码使用Map<String, Session>存储连接,新连接会覆盖旧连接;
解决方案:改为Map<String, Set>存储,同一用户的所有连接都加入 Set(本文工具类已实现)。
8.5 长时间无交互,连接被断开
原因:防火墙 / 服务器会断开长时间无数据交互的 TCP 连接;
解决方案:实现心跳检测,客户端定时发送心跳包(如&),服务端回执,保持连接活跃(本文代码已实现)。
九、总结
本文详细讲解了 SpringBoot 集成 WebSocket 的全流程,从核心依赖引入→配置类编写(解决 Jar/War 兼容)→工具类封装(连接管理 + 消息发送)→服务端端点实现(事件处理)→测试接口开发→前端测试,一步一步实现了双屏实时消息互推的功能,同时解决了项目开发和部署中的常见问题。
本文的代码具有以下特点:
- 高可用性:完善的异常处理、Session 有效性判断、失效连接清理,避免内存泄漏;
- 高扩展性:工具类和服务端端点解耦,可根据业务需求自定义消息处理逻辑;
- 高兼容性:支持 Jar 包和 War 包两种部署方式,无需手动修改代码;
- 线程安全:使用 ConcurrentHashMap 和 ConcurrentHashSet 保证多线程下的连接管理安全。
WebSocket 的应用场景非常广泛,除了双屏联动,还可以用于在线聊天、实时监控、订单推送、弹幕等场景,只需在本文代码的基础上,根据业务需求修改WebSocketUtils的receive方法和消息发送逻辑即可。
如果本文对你有帮助,欢迎点赞 + 收藏 + 关注,博主会持续更新 SpringBoot、微服务、分布式等系列文章!