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的存储、消息的路由与发送。

工具类需要解决几个关键问题:

  1. 线程安全:使用ConcurrentHashMap存储连接,避免多线程并发问题。
  2. 多端登录:同一用户(如userId)可能在多个设备(浏览器标签页、不同浏览器)同时连接,需要支持向所有活跃连接广播消息。
  3. 连接健康度:及时清理已关闭或无效的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应用用户体验的强大技术支撑。

posted on 2026-03-11 18:10  blfbuaa  阅读(88)  评论(0)    收藏  举报