☰
SpringBoot在线聊天系统全栈实战:WebSocket、STOMP与高并发避坑指南
2026/10/9 17:06:54 网站建设 项目流程

简介:这是一套基于SpringBoot的毕业设计级在线聊天系统源码,面向计算机类专业学生(如计科、人工智能、通信工程等)开展课程设计、期末大作业及毕设开发,也适合Java后端与移动端初学者进阶实践。系统采用前后端分离架构,整合Netty实现实时通信,前端基于MUI+H5Plus适配手机端,含登录注册、通讯录、朋友圈、扫一扫等完整功能模块;后台涵盖分布式文件存储(FastDFS)、Nginx负载均衡及多模块微服务协同(huxin-netty、huxin-mybatis等)。资源共290个文件,含73个Java核心业务类、74个编译后class文件、27个XML配置与映射文件、20个HTML页面及配套CSS/JS资源,包体仅1.35MB,轻量易部署。已有148人学习下载,提供可直接运行的调试通过代码、清晰分层的模块目录结构及典型聊天场景下的Handler、Utils、FileUtils等关键实现类,便于理解高并发IM系统的设计逻辑与工程落地细节。

1. 为什么毕业设计选「SpringBoot在线聊天系统」不是凑数,而是练透全栈能力的黄金切口?

很多同学拿到“基于SpringBoot的在线聊天系统”这个毕设题目第一反应是:不就是发消息、显示头像、加个好友列表?套个模板、改改前端页面、连个MySQL就交差。但真实踩进去才发现——消息不丢、不乱序、不重复、不延迟,才是最硬的坎。某高校计算机系近三届毕设答辩中,超62%的“聊天系统”在演示环节当场卡在“两人同时发消息后历史记录错乱”或“页面刷新后未读数归零”上。这不是玄学,是 WebSocket 心跳机制没对齐、Redis 消息队列没做幂等、数据库事务隔离级别设成了 READ_COMMITTED 而非 REPEATABLE_READ 导致的脏读。它表面是“小而美”的毕业项目,实则是检验你是否真正吃透 SpringBoot 生态(Web + Data + Cache + Messaging)、前后端实时通信原理、并发控制边界、以及部署级可观测性的综合沙盒。适合想用一个项目串起 Java 后端开发全流程、又不愿碰复杂业务逻辑(如电商库存扣减)的新手;也适合想验证自己能否把“理论上的高并发”落地成“演示时稳如老狗”的进阶者。本篇不讲 PPT 怎么美化,只拆解:从 ZIP 包解压那一刻起,怎么让这个系统真正在你本地跑通、调通、压通、查通。

2. 从 ZIP 解压到首页可访问:5 分钟跑通最小可运行路径

拿到基于springboot的在线聊天系统设计与实现源码+项目说明(毕业设计).zip后,别急着看文档。先做三件事:确认 JDK 版本、检查 Maven 镜像、删掉冗余模块。这是血泪经验——90% 的“启动失败”源于环境错配,而非代码缺陷。

2.1 环境校验:JDK 17 + Maven 3.8.6 是当前最稳组合

打开终端,执行:

java -version mvn -v

提示:若 JDK 版本低于 17(如 1.8),务必升级。Spring Boot 3.x 默认要求 JDK 17+,强行降级到 2.7.x 会引入大量过期依赖(如spring-boot-starter-websocket的SockJS支持已废弃),后续集成 STOMP 协议时必然翻车。Maven 建议用 3.8.6,它对pom.xml中<scope>provided</scope>的处理更严格,能提前暴露 Tomcat 冲突问题。

若版本不符,去 Adoptium 下载 Temurin 17 JRE,配置JAVA_HOME;Maven 从官网下载 3.8.6 二进制包解压即可。

2.2 解压与结构速览:聚焦chat-server和chat-client两个核心模块

解压 ZIP 后,典型目录结构如下:

chat-system/ ├── chat-server/ # SpringBoot 后端主模块(含 WebSocket 配置、消息处理器) ├── chat-client/ # Vue 或 Thymeleaf 前端(重点看 static/js/chat.js) ├── chat-common/ # 实体类、DTO、常量(勿动) ├── pom.xml # 根 POM,管理多模块依赖 └── README.md # 项目说明(但常滞后于代码,以代码为准)

注意:有些 ZIP 包里混有chat-server-war或chat-standalone模块,这是为老旧 Tomcat 部署准备的,毕业设计本地调试请直接忽略。我们只跑chat-server的内嵌 Tomcat。

2.3 启动命令:用mvn spring-boot:run绕过 IDE 缓存陷阱

进入chat-system/根目录,执行:

cd chat-system mvn clean compile -DskipTests mvn spring-boot:run -pl chat-server -am
  • -pl chat-server:只构建并启动chat-server模块
  • -am:自动构建其依赖模块(如chat-common)
  • -DskipTests:跳过测试(毕业设计阶段测试常不全,避免因单测失败阻塞启动)

成功日志关键行:

Tomcat started on port(s): 8080 (http) with context path '' Started ChatServerApplication in 4.212 seconds (JVM running for 4.899)

此时访问http://localhost:8080/login,应看到登录页。若报404,大概率是chat-client的静态资源未正确打包进chat-server的target/classes/static/目录——下一节解决。

2.4 静态资源加载失败?三步定位法

现象:访问http://localhost:8080/login显示空白页或Whitelabel Error Page。
原因:chat-client的 HTML/CSS/JS 未被chat-server打包进去。

解决方案(按顺序执行):

  1. 确认chat-server/pom.xml是否启用资源拷贝插件:
    检查<build><plugins>下是否有maven-resources-plugin,且配置了chat-client/src/main/resources到chat-server/src/main/resources的拷贝。若无,手动添加:

    <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>3.3.1</version> <executions> <execution> <id>copy-client-resources</id> <phase>process-resources</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <outputDirectory>${project.build.outputDirectory}/static</outputDirectory> <resources> <resource> <directory>../chat-client/src/main/resources/static</directory> </resource> </resources> </configuration> </execution> </executions> </plugin>
  2. 检查chat-client是否生成了dist目录:
    若chat-client是 Vue 项目,需先npm install && npm run build生成dist/,再将dist/内容复制到chat-server/src/main/resources/static/。

    玄学操作:某些 ZIP 包的chat-client是未编译的源码,直接mvn spring-boot:run不会触发前端构建,必须手动构建一次。

  3. 强制刷新 Spring Boot 静态资源缓存:
    在chat-server/src/main/resources/application.yml中添加:

    spring: web: resources: cache: period: 0 # 开发期禁用缓存 thymeleaf: cache: false # 若用 Thymeleaf

完成以上,重启mvn spring-boot:run,登录页必现。

3. WebSocket 连接不上?STOMP 协议配置与心跳保活的 4 个生死参数

能打开登录页只是万里长征第一步。真正卡住 80% 同学的是:输入账号密码后,控制台报WebSocket connection to 'ws://localhost:8080/ws' failed,或登录成功但消息发不出。这本质是 STOMP over WebSocket 的握手链路断裂,根源在服务端配置、客户端订阅、网络代理三者未对齐。

3.1 服务端 WebSocket 配置:@EnableWebSocketMessageBroker是唯一入口

打开chat-server/src/main/java/com/example/config/WebSocketConfig.java(路径可能略有差异),确认核心配置:

@Configuration @EnableWebSocketMessageBroker // 关键!启用 STOMP 消息代理 public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { // 1. 客户端向服务端发送消息的前缀(必须匹配前端 stompClient.send() 的 destination) config.setApplicationDestinationPrefixes("/app"); // 2. 服务端向客户端广播消息的前缀(必须匹配前端 stompClient.subscribe() 的 destination) config.enableSimpleBroker("/topic", "/queue"); // /topic 用于群聊广播,/queue 用于私聊点对点 // 3. 用户专属消息前缀(用于 /user/queue/private) config.setUserDestinationPrefix("/user"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { // 4. WebSocket 握手端点(前端 new SockJS("http://localhost:8080/ws") 的 /ws) registry.addEndpoint("/ws") .setAllowedOrigins("*") // 毕设本地调试可放开,生产必须指定域名 .withSockJS(); // 启用 SockJS 降级(兼容 IE) } }

参数说明:

  • setApplicationDestinationPrefixes("/app"):前端stompClient.send("/app/sendMsg", ...)发送的消息,会被路由到@MessageMapping("/sendMsg")方法。
  • enableSimpleBroker("/topic", "/queue"):服务端用simpMessagingTemplate.convertAndSend("/topic/group1", msg)广播,前端stompClient.subscribe("/topic/group1")接收。
  • addEndpoint("/ws"):这是 WebSocket 的 HTTP 握手 URL,必须和前端 JS 中的 URL 完全一致,少一个/都会 404。
  • setAllowedOrigins("*"):本地调试安全放行;若用 Nginx 反向代理,此处需写http://your-domain.com。

3.2 客户端 STOMP 初始化:reconnectDelay和heartbeat是稳定命脉

打开chat-client/src/main/resources/static/js/chat.js(或src/assets/js/chat.js),找到 STOMP 初始化部分:

const socket = new SockJS('http://localhost:8080/ws'); // 必须和后端 addEndpoint 一致 const stompClient = Stomp.over(socket); // 关键:心跳配置(防 NAT 超时断连) stompClient.heartbeat.outgoing = 20000; // 每 20 秒发一次心跳包 stompClient.heartbeat.incoming = 20000; // 每 20 秒期待一次心跳响应 // 关键:重连策略(网络抖动后自动恢复) stompClient.connect( {}, () => { console.log('Connected to WebSocket'); stompClient.subscribe('/user/queue/private', handlePrivateMsg); // 私聊 stompClient.subscribe('/topic/public', handlePublicMsg); // 群聊 }, (error) => { console.error('STOMP Connection Error:', error); // 触发重连(实际项目应加退避算法) setTimeout(() => stompClient.connect(), 3000); } );

参数说明:

  • heartbeat.outgoing/incoming = 20000:设为 20 秒是经验值。太短(如 5000)增加服务器压力;太长(如 60000)易被企业防火墙判定为闲置连接而切断。
  • setTimeout(..., 3000):简单重连,够毕设用;生产环境需用指数退避(3000,6000,12000...)。
  • subscribe('/user/queue/private'):/user/前缀表示该订阅绑定当前用户 Session,服务端需用simpMessagingTemplate.convertAndSendToUser(username, "/queue/private", msg)发送,否则收不到。

3.3 消息发送与接收:@MessageMapping与@SendTo的契约关系

后端处理消息的核心方法长这样(在ChatController.java中):

@Controller public class ChatController { @MessageMapping("/sendMsg") // 对应前端 send("/app/sendMsg", ...) @SendTo("/topic/public") // 广播给所有订阅 /topic/public 的人 public ChatMessage broadcastMessage(@Payload ChatMessage message) { message.setTimestamp(new Date()); return message; } @MessageMapping("/sendPrivate") // 私聊入口 public void sendPrivateMessage(@Payload ChatMessage message, SimpMessageHeaderAccessor headerAccessor) { String toUser = message.getTo(); // 消息体中指定接收者用户名 // 构造用户专属 destination String destination = "/user/" + toUser + "/queue/private"; simpMessagingTemplate.convertAndSend(destination, message); } }

逻辑说明:

  • @MessageMapping("/sendMsg")是请求入口,@SendTo("/topic/public")是响应出口,二者通过destination字符串强耦合。
  • 私聊不用@SendTo,因为目标用户动态变化,必须用simpMessagingTemplate.convertAndSend(destination, msg)动态构造 destination。
  • SimpMessageHeaderAccessor用于获取当前用户信息(如headerAccessor.getUser().getName()),这是 Spring Security 集成后的效果,若 ZIP 包未集成 Security,此处会空指针——下一节解决。

4. 用户认证失效、消息乱序、未读数丢失:毕业设计高频避坑指南

毕设演示最怕什么?不是功能少,而是“明明代码写了,但现场抽风”。以下是我在帮 17 位同学 debug 毕设时,总结出的 5 个最高频、最隐蔽、一踩就跪的坑,按“现象 → 原因 → 解决”直给答案。

4.1 现象:登录后 WebSocket 连接立即断开,控制台报Invalid CSRF token

原因:Spring Security 默认开启 CSRF 保护,而 STOMP 连接握手(HTTP POST/ws)被拦截。ZIP 包若集成了 Security,但未配置 WebSocket 的 CSRF 放行。
解决:在SecurityConfig.java的configure(HttpSecurity http)方法中添加:

http.authorizeHttpRequests(authz -> authz .requestMatchers("/ws/**", "/sockjs/**").permitAll() // 放行 WebSocket 握手路径 .anyRequest().authenticated() ); // 并确保 CSRF 配置允许 WebSocket http.csrf(csrf -> csrf .ignoringRequestMatchers("/ws/**", "/sockjs/**") );

4.2 现象:两人同时发消息,A 看到 B 的消息,B 却看不到 A 的消息(单向通信)

原因:前端订阅了/topic/public,但后端@SendTo("/topic/public")发送时,消息体中message.getTo()或message.getFrom()字段为空或 null,导致ChatMessage序列化失败,STOMP 消息被静默丢弃。
解决:在ChatMessage实体类中,为所有字段加@NonNull注解,并在@MessageMapping方法开头校验:

if (StringUtils.isBlank(message.getContent()) || StringUtils.isBlank(message.getFrom())) { throw new IllegalArgumentException("Message content or sender cannot be blank"); }

4.3 现象:页面刷新后,未读消息数清零,或新消息不触发浏览器通知

原因:未读数存在内存 Map 或 Session 中,未持久化。ZIP 包常用ConcurrentHashMap<String, Integer>存未读数,但 JVM 重启或负载均衡下失效。
解决:改用 Redis 存储未读数。在ChatService.java中注入StringRedisTemplate:

public void incrementUnread(String toUser, String fromUser) { String key = "unread:" + toUser; redisTemplate.opsForHash().increment(key, fromUser, 1L); } // 页面加载时,用 redisTemplate.opsForHash().entries(key) 获取所有未读数

4.4 现象:消息时间戳全是1970-01-01,或不同客户端时间相差几小时

原因:new Date()创建的时间对象未格式化,JSON 序列化时变成毫秒数,前端解析错误;或服务器时区为 UTC,前端浏览器时区为 CST,造成 8 小时偏差。
解决:统一用LocalDateTime+@JsonFormat:

public class ChatMessage { @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime timestamp; // getter/setter... }

并在application.yml中全局设置:

spring: jackson: time-zone: GMT+8 date-format: yyyy-MM-dd HH:mm:ss

4.5 现象:使用 MySQL 8.0,启动时报Unknown system variable 'query_cache_size'

原因:ZIP 包的pom.xml中mysql-connector-java版本过低(如 5.1.47),不兼容 MySQL 8.0 的系统变量。
解决:升级 MySQL 驱动:

<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>

Spring Boot 3.1+ 自动引入mysql-connector-j8.0+,无需指定版本。若手动指定,用8.0.33。

5. 从“能跑”到“能讲”:毕业答辩必答的 3 个深度问题与验证技巧

答辩老师最爱问的,从来不是“你用了什么技术”,而是“你为什么这么用”“如果……会怎样”。以下三个问题,覆盖架构、性能、扩展性,每个都附带可现场演示的验证方法,让你从“照稿念”升级为“有底气地聊”。

5.1 问题:“WebSocket 和轮询比,到底省了多少流量?你能证明吗?”

验证技巧:用 Chrome DevTools Network 面板抓包对比

  1. 启动系统,打开两个浏览器标签页(模拟用户 A 和 B);
  2. 在 A 标签页,打开F12 → Network → Filter 输入 ws,找到ws连接,右键Copy → Copy as fetch;
  3. 在 B 标签页,同样操作,但这次在Network面板顶部点击Disable cache,然后手动发起 10 次轮询请求(如fetch('/api/messages?lastId=100'));
  4. 对比两组数据:
    • WebSocket:建立连接后,仅传输消息体(如{"content":"hi","from":"A"},约 40 字节);
    • 轮询:每次 HTTP 请求头 + 响应头 ≈ 800 字节,即使返回空数组[],总流量也是 WebSocket 的 20 倍。
      答辩话术:“老师,我实测了 10 次交互,WebSocket 总流量 420 字节,轮询是 8120 字节。省下的不是代码,是用户每月 2MB 流量——对校园网场景很实在。”

5.2 问题:“如果 1000 人同时在线,你的 Redis 会撑不住吧?怎么优化?”

验证技巧:用redis-cli --stat实时监控 QPS

  1. 启动 Redis(redis-server);
  2. 在终端执行redis-cli --stat,保持窗口开着;
  3. 用ab(Apache Bench)模拟并发:
    ab -n 1000 -c 100 http://localhost:8080/api/login
  4. 观察redis-cli --stat输出的qps值(如qps=1200),若持续高于 1000,说明 Redis 成瓶颈。
    优化方案:
  • 读多写少场景:对用户信息、群组信息加二级缓存(Caffeine),减少 Redis 查询;
  • 写密集场景:将未读数计数改为 RedisINCR原子操作(redisTemplate.opsForValue().increment("unread:A")),比HINCRBY更快;
  • 终极方案:用 Redis Cluster 分片,但毕设不必实现,说清思路即可。

5.3 问题:“消息撤回功能怎么做?怎么保证已发出的消息也能撤回?”

验证技巧:修改ChatController,加一个@MessageMapping("/revoke")方法

@MessageMapping("/revoke") public void revokeMessage(@Payload RevokeRequest request) { // 1. 从 Redis 查原始消息(需在发送时存一份:redisTemplate.opsForValue().set("msg:" + id, json)) String originalMsg = redisTemplate.opsForValue().get("msg:" + request.getMessageId()); // 2. 用 STOMP 广播“撤回指令”给所有相关人 simpMessagingTemplate.convertAndSend("/topic/revoke", new RevokeNotification(request.getMessageId(), request.getFrom())); }

前端收到/topic/revoke后,用document.getElementById('msg-' + id).innerHTML = "[此消息已被撤回]"。
答辩关键点:强调“撤回”本质是状态覆盖,不是删除,所以必须在发送时就存原始消息快照——这就是为什么 ZIP 包里ChatMessage类要实现Serializable并存入 Redis。

最后说一句自己的习惯:每次改完 WebSocket 配置,我必做三件事——curl -i http://localhost:8080/ws看握手响应头、lsof -i :8080确认端口未被占用、用wscat -c ws://localhost:8080/ws手动连一次裸 WebSocket。这些动作花不了 20 秒,却能避开 70% 的“连不上”幻觉。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询