Java聊天系统实战:WebSocket+MySQL+PPT全链路

发布时间:2026/10/9 6:23:34
Java聊天系统实战:WebSocket+MySQL+PPT全链路 简介这是一套面向高校学生的Java聊天系统完整项目源码可作为Java期末大作业或课程设计参考仿QQ聊天室实现。系统涵盖用户注册、登录、找回密码、在线名单查看、群聊与私聊、账号注销、修改密码及退出等基础功能采用Java Swing界面、多线程通信与MySQL数据库等大学阶段常用技术适合刚学完Java程序设计、需要动手实践的学生。压缩包共291个文件约21.46MB其中178个png为界面截图与头像素材34个java与53个class构成核心源码另有16个xml、4个properties配置文件、1个sql数据库脚本和1份pptx项目展示PPT结构完整。目前已有587人学习。项目已调试成功、无bug附带数据库源文件与展示PPT读者可据此理解C/S架构下多线程与数据库交互的实现思路快速搭建运行环境并对照源码完成二次开发或答辩准备。1. 从一份能跑起来的 Java chat 聊天系统说起数据库文件和 PPT 到底该怎么用很多人搜「java做的chat聊天系统附带数据库文件、项目展示PPT」真正想要的不是一份压缩包而是一条能落地的路径把 Java 后端、数据库表结构、前端聊天界面和一份能讲清楚的展示 PPT 串成一个完整作品。它适合三类人课程设计要交作业的学生、简历里想放一个可演示项目的 Java 工程师、以及想用 Spring Boot MyBatis 练手 WebSocket 的开发者。核心难点从来不是写一个while(true)收消息而是消息怎么存、在线状态怎么维护、数据库文件怎么导入、PPT 里该展示哪些技术点。下面按「先跑通、再讲透、最后避坑」的顺序拆开讲中间会给出可直接抄的建表 SQL、WebSocket 配置和消息落库代码。2. 先把架构定下来Java chat 聊天系统的最小可运行分层2.1 为什么选 Spring Boot WebSocket MySQL 这套组合做聊天系统第一件事是选通信方式。轮询前端每秒发一次 HTTP 请求问「有没有新消息」实现最简单但延迟高、服务器压力大几十个人在线就开始卡。长轮询稍好但连接管理依然麻烦。真正适合聊天的是 WebSocket一次握手后保持全双工连接服务端可以主动推消息延迟能压到毫秒级。Java 生态里Spring Boot 对 WebSocket 的封装已经足够成熟配合ServerEndpoint或WebSocketHandler都能快速起步。数据库选 MySQL 而不是直接存文件是因为聊天记录需要按会话、按时间、按用户多维查询文件系统做不了高效索引。MyBatis-Plus 在这里的价值是省掉大量单表 CRUD 的 XML实体类加注解就能生成建表语句这对课程设计这种「表不多但要求规范」的场景很合适。前端可以用原生 HTML JavaScript 的 WebSocket API也可以用 Vue本文以原生为主方便你直接复制运行。整体分层是这样的浏览器通过 WebSocket 连到后端后端用ConcurrentHashMap维护「用户 ID → Session」的映射消息进来先落库再转发离线消息在用户重连时从数据库拉取。这套结构不复杂但每一层都有容易翻车的地方后面会逐个说。2.2 数据库文件怎么导入三张核心表与建表 SQL拿到「附带数据库文件」通常是一个.sql文件里面是建表语句和初始数据。但很多同学直接source就报错原因是字符集或外键顺序不对。我一般会先手动建库再导入-- 创建数据库字符集必须用 utf8mb4否则 emoji 存不进去 CREATE DATABASE chat_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE chat_system; -- 用户表存账号、昵称、头像、在线状态 CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT 密码存BCrypt哈希, nickname VARCHAR(50) DEFAULT NULL COMMENT 昵称, avatar VARCHAR(255) DEFAULT NULL COMMENT 头像URL, online TINYINT DEFAULT 0 COMMENT 0离线 1在线, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 消息表核心表记录每一条聊天内容 CREATE TABLE message ( id BIGINT NOT NULL AUTO_INCREMENT, from_id BIGINT NOT NULL COMMENT 发送者ID, to_id BIGINT NOT NULL COMMENT 接收者ID群聊时为群ID, content TEXT COMMENT 消息内容, type TINYINT DEFAULT 0 COMMENT 0单聊 1群聊, status TINYINT DEFAULT 0 COMMENT 0未读 1已读, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_to_status (to_id, status), KEY idx_from_to (from_id, to_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 群组表如果做群聊才需要 CREATE TABLE group_info ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL, owner_id BIGINT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;导入时注意三点第一先执行SET NAMES utf8mb4;再导入避免中文乱码第二如果.sql文件里有DROP TABLE语句确认没有重要数据再跑第三外键约束如果顺序不对会报 1215 错误建议先不建外键用应用层保证一致性。消息表的idx_to_status索引很关键拉取未读消息时全靠它没有这个索引数据量上万后查询会明显变慢。2.3 用 MyBatis-Plus 实体类反向生成建表语句的实操热搜里有人问「mybatisplus根据java实体类生成创建表的sql语句」这在课程设计里很实用先写实体类再让框架生成表省得手写 SQL 对不上字段。MyBatis-Plus 本身不直接提供建表功能但可以配合mybatis-plus-generator或自己写一段反射代码。更常见的做法是用TableName和TableField注解标好字段然后用下面这段工具类打印建表 SQL// 简易版根据实体类字段生成 CREATE TABLE 语句 public class SqlGenerator { public static String generate(Class? clazz) { TableName table clazz.getAnnotation(TableName.class); String tableName table ! null ? table.value() : clazz.getSimpleName().toLowerCase(); StringBuilder sb new StringBuilder(CREATE TABLE tableName (\n); for (Field field : clazz.getDeclaredFields()) { // 跳过 serialVersionUID 这类静态字段 if (Modifier.isStatic(field.getModifiers())) continue; TableField tf field.getAnnotation(TableField.class); String column tf ! null !tf.value().isEmpty() ? tf.value() : camelToUnderline(field.getName()); String sqlType mapType(field.getType()); sb.append( ).append(column).append( ).append(sqlType).append(,\n); } sb.append( PRIMARY KEY (id)\n) ENGINEInnoDB DEFAULT CHARSETutf8mb4;); return sb.toString(); } private static String camelToUnderline(String name) { return name.replaceAll(([a-z])([A-Z]), $1_$2).toLowerCase(); } private static String mapType(Class? type) { if (type String.class) return VARCHAR(255); if (type Long.class || type long.class) return BIGINT; if (type Integer.class || type int.class) return INT; if (type Date.class || type LocalDateTime.class) return DATETIME; return VARCHAR(255); } }这段代码的逻辑是读实体类的TableName拿表名遍历字段把驼峰转下划线再按 Java 类型映射到 MySQL 类型。参数说明camelToUnderline处理createTime→create_timemapType只覆盖了常用类型遇到BigDecimal要自己补DECIMAL(10,2)。注意这只是生成草稿生产环境还是建议用 Flyway 或 Liquibase 管理表结构变更别让代码和数据库各说各话。3. WebSocket 服务端与消息落库从连接到持久化的完整链路3.1 用 ServerEndpoint 建立连接并维护在线用户表Java 里做 WebSocket 有两种主流方式Spring 的WebSocketHandler和 JSR-356 的ServerEndpoint。课程设计里ServerEndpoint更直观一个类就能搞定连接、收消息、关连接三个回调。下面是最小实现Component ServerEndpoint(/chat/{userId}) public class ChatEndpoint { // 在线用户表key 是 userIdvalue 是当前 WebSocket 会话 // 用 ConcurrentHashMap 是因为多线程同时上下线普通 HashMap 会出并发问题 private static final ConcurrentHashMapLong, Session ONLINE_USERS new ConcurrentHashMap(); private Long userId; OnOpen public void onOpen(Session session, PathParam(userId) Long userId) { this.userId userId; ONLINE_USERS.put(userId, session); // 更新数据库在线状态 updateOnlineStatus(userId, 1); System.out.println(用户 userId 上线当前在线数 ONLINE_USERS.size()); } OnMessage public void onMessage(String message, Session session) { // message 是 JSON 字符串解析出接收者和内容 JSONObject json JSON.parseObject(message); Long toId json.getLong(toId); String content json.getString(content); // 先落库再转发。顺序不能反否则对方收到消息但数据库没记录 saveMessage(userId, toId, content); // 如果对方在线直接推送不在线就等对方重连时拉取 Session target ONLINE_USERS.get(toId); if (target ! null target.isOpen()) { JSONObject push new JSONObject(); push.put(fromId, userId); push.put(content, content); push.put(createTime, new Date().toString()); target.getAsyncRemote().sendText(push.toJSONString()); } } OnClose public void onClose() { ONLINE_USERS.remove(userId); updateOnlineStatus(userId, 0); } OnError public void onError(Session session, Throwable error) { // 连接异常时清理避免僵尸会话占着 map ONLINE_USERS.remove(userId); error.printStackTrace(); } }关键点ONLINE_USERS必须是static因为每个 WebSocket 连接会创建新的 Endpoint 实例非静态变量拿不到全局在线表。PathParam从连接地址/chat/1001里取用户 ID前端连接时拼上就行。getAsyncRemote()是异步发送避免某个客户端网络慢阻塞其他消息推送。注意ServerEndpoint的类如果要用 Spring 注入的 Service需要额外配置ServerEndpointExporter否则Autowired会是 null这是新手最常踩的坑之一。3.2 消息落库MyBatis-Plus 插入与未读消息拉取消息落库看起来就是一条INSERT但聊天场景有两个特殊需求一是插入后要能拿到自增 ID 返回给前端做消息去重二是用户重连时要拉取所有未读消息并标记已读。用 MyBatis-Plus 的save方法可以自动回填 IDService public class MessageService { Autowired private MessageMapper messageMapper; // 保存消息返回带 ID 的实体 public Message saveMessage(Long fromId, Long toId, String content) { Message msg new Message(); msg.setFromId(fromId); msg.setToId(toId); msg.setContent(content); msg.setType(0); msg.setStatus(0); // 未读 msg.setCreateTime(new Date()); messageMapper.insert(msg); // insert 后 msg.getId() 会被自动填充 return msg; } // 拉取某个用户的全部未读消息 public ListMessage pullUnread(Long userId) { LambdaQueryWrapperMessage wrapper new LambdaQueryWrapper(); wrapper.eq(Message::getToId, userId) .eq(Message::getStatus, 0) .orderByAsc(Message::getCreateTime); ListMessage list messageMapper.selectList(wrapper); // 拉取后批量标记已读避免重复推送 if (!list.isEmpty()) { ListLong ids list.stream().map(Message::getId).collect(Collectors.toList()); messageMapper.update(null, new LambdaUpdateWrapperMessage() .in(Message::getId, ids) .set(Message::getStatus, 1)); } return list; } }参数说明LambdaQueryWrapper用方法引用代替字符串字段名重构时不会因为改字段名而漏改 SQL。orderByAsc保证消息按时间正序前端直接追加即可。批量标记已读用LambdaUpdateWrapper一次更新比循环单条更新快得多。注意update(null, wrapper)第一个参数传 null 表示不设置实体只按 wrapper 更新这是 MyBatis-Plus 的固定用法。3.3 前端最小页面连接、发送、接收三段式前端不需要框架也能跑核心就三件事建立连接、发消息、收消息。下面是一个能直接用的 HTML 片段!DOCTYPE html html headmeta charsetUTF-8titleChat/title/head body div idmsgList styleheight:300px;overflow-y:auto;border:1px solid #ccc;/div input idinput placeholder输入消息 button onclicksend()发送/button script // 假设当前用户 ID 是 1001接收者是 1002 const myId 1001; const ws new WebSocket(ws://localhost:8080/chat/${myId}); ws.onopen () console.log(连接成功); ws.onmessage (event) { const data JSON.parse(event.data); const div document.createElement(div); div.textContent 用户${data.fromId}: ${data.content}; document.getElementById(msgList).appendChild(div); }; ws.onclose () console.log(连接关闭可在此做重连); function send() { const content document.getElementById(input).value; if (!content) return; ws.send(JSON.stringify({ toId: 1002, content: content })); document.getElementById(input).value ; } /script /body /html逻辑说明ws.onmessage里解析 JSON 并追加到消息列表实际项目要加 XSS 过滤别直接innerHTML。ws.onclose里可以做指数退避重连比如 1 秒、2 秒、4 秒后重试避免网络抖动导致用户以为掉线。注意 WebSocket 地址的协议是ws://如果页面是 HTTPS 就要用wss://否则浏览器会拦截。4. 项目展示 PPT 怎么做把技术点讲成评委听得懂的故事4.1 PPT 的六页骨架与每页该放什么「附带项目展示 PPT」不是让你把代码贴上去而是用六页讲清楚「做了什么、怎么做的、难在哪」。我一般按这个骨架来第一页封面写项目名、技术栈、你的角色第二页需求与场景用一张图说明谁和谁聊天、消息怎么流转第三页架构图画浏览器、WebSocket、Spring Boot、MySQL 四层第四页核心代码只放ServerEndpoint和消息落库两段每段不超过 15 行第五页数据库设计放三张表的 ER 图标出索引第六页演示与数据放一张聊天截图和一条「消息落库成功」的数据库查询结果。评委最反感的是满屏代码和「本系统采用先进技术」这种空话。第四页放代码时一定要在旁边用一句话说明「这段解决了什么问题」比如「用 ConcurrentHashMap 维护在线表保证多用户并发上下线不丢连接」。第五页的 ER 图比建表 SQL 更直观用 draw.io 画十分钟就能搞定。4.2 演示环节的三个脚本登录、单聊、离线消息PPT 讲完通常要现场演示建议提前写好三个脚本每个不超过 30 秒。第一个是登录打开两个浏览器窗口分别用 1001 和 1002 登录展示在线用户数从 0 变 2。第二个是单聊1001 发「你好」1002 窗口立刻收到同时切到数据库客户端执行SELECT * FROM message ORDER BY id DESC LIMIT 1;展示消息已落库。第三个是离线消息关掉 1002 的窗口1001 再发一条然后重新打开 1002展示未读消息自动拉取。这三个脚本覆盖了「连接、实时、持久化」三个技术点比讲十分钟原理都有说服力。注意演示前把数据库里的测试数据清一下别让评委看到一堆乱码昵称。另外如果现场网络不稳定提前在本地跑好别依赖外网。5. 避坑与排查Java chat 聊天系统最常见的五个翻车点5.1 现象WebSocket 连接 404浏览器控制台报握手失败原因通常是ServerEndpointExporter没注册或者ServerEndpoint的路径和前端写的对不上。Spring Boot 内嵌 Tomcat 不会自动扫描ServerEndpoint必须手动声明一个 BeanConfiguration public class WebSocketConfig { Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }如果加了还是 404检查前端地址有没有多写或少写斜杠/chat/1001和/chat/1001/在有些容器里行为不同。另外如果项目用了 Spring Security默认会拦截 WebSocket 握手请求需要在配置里放行/chat/**。5.2 现象消息发出去对方收不到但数据库里有记录先看对方是否在线ONLINE_USERS.get(toId)返回 null 说明对方连接已断但没触发OnClose。常见原因是网络异常断开时OnClose不一定执行所以OnError里也要做清理。另一个可能是getAsyncRemote()发送时对方 Session 已关闭isOpen()判断能过滤大部分情况。如果对方确实在线却收不到检查前端onmessage是否被覆盖或者消息 JSON 字段名和后端推送的不一致。5.3 现象中文消息变成问号emoji 直接报错这是字符集问题要查三个地方数据库建库时是不是utf8mb4连接 URL 有没有加characterEncodingutf8以及 WebSocket 传输时有没有做编码转换。MySQL 的utf8其实是utf8mb3最多存三个字节emoji 是四字节必须用utf8mb4。连接 URL 建议写成jdbc:mysql://localhost:3306/chat_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai少一个参数都可能出问题。5.4 现象用户下线后在线状态没更新在线列表越来越长OnClose没触发或者ONLINE_USERS.remove没执行。除了在OnClose和OnError里都清理还可以加一个心跳机制前端每 30 秒发一个ping后端收到后更新最后活跃时间后台定时任务扫描超过 90 秒没心跳的连接并关闭。这样即使 TCP 连接假死也能被清理掉。注意心跳消息不要落库在onMessage里判断typeping直接返回。5.5 现象项目打成 tar 包部署到服务器后启动失败热搜里有人问「怎么把java项目打成tar包」和「java启动失败怎么解决」这两个经常一起出现。Spring Boot 项目用mvn clean package打出可执行 jar再用tar -czvf chat.tar.gz chat.jar application.yml打包。部署到服务器后启动失败先看日志里有没有Port 8080 was already in use再看数据库连接是不是还写的localhost。服务器上要用nohup java -jar chat.jar chat.log 21 后台启动然后tail -f chat.log看日志。如果报OutOfMemoryError加-Xms256m -Xmx512m限制堆大小别让一个小聊天系统把服务器内存吃光。6. 进阶技巧用消息表分区和 Redis 缓存把聊天系统撑到千人在线单机版跑通后如果想让这个项目在简历里更有分量可以加两个进阶点。第一个是消息表按时间分区MySQL 支持PARTITION BY RANGE按月分表后单表数据量控制在百万级以内查询未读消息时只扫当前分区。建分区语句要在建表时加ALTER TABLE message PARTITION BY RANGE (TO_DAYS(create_time)) ( PARTITION p202601 VALUES LESS THAN (TO_DAYS(2026-02-01)), PARTITION p202602 VALUES LESS THAN (TO_DAYS(2026-03-01)), PARTITION p202603 VALUES LESS THAN (TO_DAYS(2026-04-01)) );注意分区键必须是主键的一部分所以message表的主键要改成(id, create_time)联合主键否则会报 1503 错误。这个改动影响 MyBatis-Plus 的TableId需要把create_time也标成主键字段。第二个是用 Redis 缓存在线用户和最近消息。ONLINE_USERS从本地ConcurrentHashMap换成 Redis 的 Hashkey 是chat:onlinefield 是 userIdvalue 是服务器节点标识。这样多台服务器部署时用户 A 连在节点 1、用户 B 连在节点 2节点 1 发消息时可以通过 Redis 查到 B 在节点 2再用消息队列转发过去。最近 20 条消息用 Redis List 缓存用户打开聊天窗口先读缓存没有再查数据库能明显降低数据库压力。我自己的习惯是任何聊天系统上线前先用 JMeter 模拟 500 个 WebSocket 连接同时发消息观察内存和数据库连接池。有一次没压测就部署结果 200 人在线时数据库连接池爆了报HikariPool-1 - Connection is not available后来把maximum-pool-size从 10 调到 50 才稳住。所以别省这一步压测数据比 PPT 上的架构图更能说明问题。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询