Java聊天系统课设实战:Spring Boot+WebSocket+MyBatis-Plus从零跑通
简介这是一套面向高校学生与Java初学者的聊天系统实战项目可作为Java期末大作业或课程设计参考。项目仿照QQ聊天室实现涵盖用户注册、登录、找回密码、在线人员名单、群聊与私聊、账号注销、修改密码及主页退出等完整功能采用Java程序设计结合Swing组件、多线程与MySQL数据库等大学阶段基础技术适合用来巩固面向对象编程、界面开发与数据库操作的综合能力。资源包共291个文件约21.46MB其中包含34个java源码、53个class编译文件、178个png界面截图、16个xml配置、4个properties配置以及1个sql数据库源文件和1份项目展示PPT结构完整便于对照源码与效果图理解实现思路。目前已有587人学习下载。项目已调试成功、无明显bug附带数据库文件与展示PPT读者可据此快速还原运行环境梳理模块划分与多线程通信逻辑并在此基础上二次开发或撰写实验报告。1. Java聊天系统从零到跑通一份带库带PPT的课设为什么值得你亲手复现如果你正在搜「java做的chat聊天系统」大概率不是想听概念而是手里压着一个课程设计或者简历项目需要一套能跑起来、能讲清楚、还能拿去答辩的东西。这个标题背后其实是一套完整的交付物Java 服务端、聊天通信逻辑、一份可导入的数据库文件外加一份项目展示 PPT。它解决的不是「聊天软件怎么做」这种宏大命题而是「我如何在有限时间里交出一个结构完整、逻辑自洽、演示不翻车的系统」。适合的人群很明确Java 基础刚过一遍、学过 Socket 和 JDBC、想用 Spring Boot 或原生 Socket 做点真东西的学生和初级工程师。我见过太多人卡在环境配置和数据库导入上代码写完了却跑不起来最后 PPT 里全是截图没有真机演示。这篇就按我实际带人做课设的路径把选型、建库、通信、打包、演示串成一条能复现的线顺带把那些让人半夜改 bug 的坑提前标出来。2. 技术选型与工程骨架为什么我劝你用 Spring Boot MyBatis-Plus 而不是裸 Socket2.1 通信层选型BIO、NIO 还是 WebSocket做聊天系统第一个岔路口就是通信方式。很多课设模板用的是原生ServerSocket加多线程也就是 BIO 模型一个客户端一个线程。这种写法代码量少、逻辑直白答辩时也好讲但它的天花板很低线程数一上去上下文切换和内存占用就会让机器喘不过气。如果你只是演示三五个客户端互发消息BIO 完全够用甚至更稳因为不用处理粘包拆包之外太多状态。但如果你想让系统看起来「像个现代项目」或者简历上想写点有分量的东西我一般会推荐 Spring Boot 内嵌 Tomcat 配合 WebSocket。WebSocket 在 HTTP 握手之后升级为全双工长连接服务端可以主动推消息不用客户端轮询。Spring 的spring-boot-starter-websocket把握手、会话管理、消息编解码都封装好了你只需要关注业务。代价是你要理解WebSocketHandler、HandshakeInterceptor和WebSocketSession这几个概念配置类写错一个注解连接就是 404。这里有个血泪经验不要一上来就追求 Netty。Netty 性能确实强但它的学习曲线和调试成本对课设级别来说不划算。你花三天搞懂ChannelPipeline和ByteBuf别人已经用 WebSocket 把登录、群聊、私聊都跑通了。选型的核心不是「哪个最强」而是「哪个能让你在截止日期前把功能演示完整」。2.2 持久层选型MyBatis-Plus 省掉的不是代码是心智数据库操作这块热搜里频繁出现「mybatisplus根据java实体类生成创建表的sql语句」说明很多人已经意识到手写 CRUD 的枯燥。MyBatis-Plus 的价值在于你定义好实体类加几个注解基础的增删改查就不用写了。对于聊天系统来说用户表、消息表、好友关系表、群组表这些表的操作 80% 是标准 CRUD剩下 20% 才是复杂查询。我一般会这样组织User实体对应user表Message实体对应message表用TableName和TableId标注映射关系。分页查历史消息用Page对象条件构造器LambdaQueryWrapper写起来比 XML 清爽。但要注意MyBatis-Plus 不是银弹多表联查和复杂聚合还是得回 XML 或者手写 SQL。课设里消息表按时间倒序查最近 50 条用orderByDesc加last(limit 50)就够了别为了秀技术把简单查询写成存储过程。2.3 工程目录结构让答辩老师一眼看懂你的分层一个清晰的目录结构能省掉答辩时一半的解释成本。我习惯按功能垂直切分而不是按技术层水平切分。比如chat-system/ ├── src/main/java/com/example/chat/ │ ├── config/ # WebSocket、MyBatis-Plus、跨域配置 │ ├── controller/ # HTTP 接口登录、注册、拉取历史消息 │ ├── entity/ # 数据库实体类 │ ├── mapper/ # MyBatis-Plus Mapper 接口 │ ├── service/ # 业务逻辑消息路由、在线状态管理 │ ├── websocket/ # WebSocket 处理器与拦截器 │ └── ChatApplication.java ├── src/main/resources/ │ ├── application.yml # 数据源、端口、MyBatis-Plus 配置 │ ├── mapper/ # 复杂查询的 XML如果有 │ └── static/ # 前端页面或演示用的简易 HTML └── sql/ └── chat_db.sql # 数据库文件直接导入即可这个结构的好处是websocket包专门放通信逻辑service包放业务规则controller只负责 HTTP 入口。答辩时老师问「消息怎么存的」你直接指service里的MessageService问「连接怎么建立的」你指websocket里的ChatWebSocketHandler。职责边界清楚代码就不会变成一锅粥。提示application.yml里数据库密码不要用明文提交到公开仓库课设演示可以用环境变量或者本地配置覆盖养成习惯比什么都重要。3. 数据库文件怎么建怎么导从 SQL 脚本到能跑通的第一条消息3.1 核心表结构设计与字段含义聊天系统的数据库不需要太复杂但几个关键字段必须想清楚。下面是我常用的最小可用表结构直接对应标题里说的「数据库文件」。-- 用户表存账号、昵称、密码哈希、在线状态 CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL COMMENT 登录名唯一, password VARCHAR(128) NOT NULL COMMENT BCrypt 哈希后的密码, nickname VARCHAR(50) DEFAULT NULL COMMENT 显示昵称, avatar VARCHAR(255) DEFAULT NULL COMMENT 头像地址, online TINYINT DEFAULT 0 COMMENT 0 离线 1 在线, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 消息表存单聊和群聊消息用 type 区分 CREATE TABLE message ( id BIGINT NOT NULL AUTO_INCREMENT, from_user_id BIGINT NOT NULL COMMENT 发送者, to_user_id BIGINT DEFAULT NULL COMMENT 接收者群聊时为空, group_id BIGINT DEFAULT NULL COMMENT 群组 ID单聊时为空, content TEXT NOT NULL COMMENT 消息内容, type TINYINT DEFAULT 0 COMMENT 0 单聊 1 群聊, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_to_user_time (to_user_id, create_time), KEY idx_group_time (group_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT消息表; -- 群组表与群成员表如果做群聊 CREATE TABLE chat_group ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL, owner_id BIGINT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE group_member ( id BIGINT NOT NULL AUTO_INCREMENT, group_id BIGINT NOT NULL, user_id BIGINT NOT NULL, join_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_group_user (group_id, user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段设计上有几个点值得说。password存 BCrypt 哈希而不是明文这是底线答辩时被问到安全也有话讲。message表用to_user_id和group_id两个可空字段区分单聊群聊比建两张表更省事查询时用type过滤。索引建在(to_user_id, create_time)上因为拉历史消息永远是「某人的消息按时间倒序」这个复合索引能直接命中。3.2 导入数据库文件的两种方式与常见报错拿到chat_db.sql之后导入方式取决于你用的客户端。命令行方式最通用# 先登录 MySQL注意 -p 后面不要跟密码回车后再输入 mysql -u root -p # 创建数据库并指定字符集这一步很多人漏掉导致中文乱码 CREATE DATABASE chat_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 切换到该库 USE chat_db; # 导入 SQL 文件路径用绝对路径或当前目录相对路径 SOURCE /path/to/chat_db.sql;如果你用 Navicat 或者 DataGrip直接右键「运行 SQL 文件」选路径就行。但不管哪种方式最常见的报错是Unknown database chat_db原因是你没先建库另一个是Incorrect string value原因是库或表的字符集不是utf8mb4存 emoji 或者中文就会炸。解决办法就是建库时显式指定utf8mb4连接串里也加上characterEncodingutf8。注意MySQL 8 的驱动类名是com.mysql.cj.jdbc.DriverMySQL 5 是com.mysql.jdbc.Driver。用错驱动会报Loading class ... is deprecated或者直接连不上。application.yml里 URL 还要加serverTimezoneAsia/Shanghai否则时间字段可能差 8 小时。3.3 用 MyBatis-Plus 反向生成实体类数据库建好之后实体类不用手写。如果你用 IDEA装个 MyBatis-Plus 插件配置好数据源右键表就能生成Entity、Mapper、Service、Controller。生成出来的代码检查两点主键策略是不是AUTO字段类型和数据库是否对得上。比如TINYINT对应Integer而不是BooleanDATETIME对应LocalDateTime。生成完把TableName和TableId确认一遍就能直接注入Mapper用了。// 生成的 UserMapper 接口继承 BaseMapper 就自带 CRUD Mapper public interface UserMapper extends BaseMapperUser { // 复杂查询再在这里加方法简单查询不用写 } // Service 层直接调用selectOne 按用户名查用户 User user userMapper.selectOne( new LambdaQueryWrapperUser().eq(User::getUsername, username) );这段代码的逻辑是LambdaQueryWrapper用方法引用代替字符串字段名编译期就能发现拼写错误。参数说明上eq是等值条件多个条件可以链式.and()或.or()。查不到会返回null所以后面必须判空否则登录逻辑直接空指针。4. WebSocket 通信与消息路由让两个浏览器窗口真正聊起来4.1 握手拦截与用户身份绑定WebSocket 连接建立之前服务端需要知道「这个连接是谁」。常见做法是在握手阶段从 URL 参数或者 Cookie 里取 token解析出用户 ID存到WebSocketSession的attributes里。这样后续收到消息时直接能从会话里拿到发送者身份不用每条消息都带用户 ID。Component public class AuthHandshakeInterceptor implements HandshakeInterceptor { Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, MapString, Object attributes) { // 从 URL 参数取 token实际项目应该校验 JWT 或 session String token UriComponentsBuilder.fromUri(request.getURI()) .build().getQueryParams().getFirst(token); if (token null || token.isEmpty()) { return false; // 拒绝握手前端会收到 403 } Long userId JwtUtil.parseUserId(token); // 假设有工具类 attributes.put(userId, userId); return true; } Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { // 握手后回调一般不用写逻辑 } }beforeHandshake返回false会直接中断连接前端onerror或者onclose能感知到。参数attributes是个 Map放进去的东西在整个会话生命周期都能取。这里用 JWT 举例课设里如果没做 JWT用简单的 session 或者直接传 userId 也能跑但答辩时最好说清楚「生产环境必须校验身份」。4.2 会话管理与在线用户映射服务端需要维护一个「用户 ID 到 WebSocketSession」的映射才能把消息推给指定的人。用ConcurrentHashMap存线程安全且读写性能够用。Component public class SessionManager { // key 是 userIdvalue 是会话 private static final MapLong, WebSocketSession SESSIONS new ConcurrentHashMap(); public void add(Long userId, WebSocketSession session) { SESSIONS.put(userId, session); } public void remove(Long userId) { SESSIONS.remove(userId); } public WebSocketSession get(Long userId) { return SESSIONS.get(userId); } // 给指定用户发消息会话关闭时返回 false public boolean sendTo(Long userId, String payload) { WebSocketSession session SESSIONS.get(userId); if (session ! null session.isOpen()) { try { session.sendMessage(new TextMessage(payload)); return true; } catch (IOException e) { // 发送失败说明连接已断清理掉 SESSIONS.remove(userId); } } return false; } }这段代码的关键点是session.isOpen()判断和异常捕获。WebSocket 连接可能因为网络抖动或者客户端关闭而失效但服务端的 Map 里还留着引用。如果不判断直接sendMessage会抛IOException严重时把主线程拖垮。所以发送失败就顺手remove保证 Map 里的会话都是活的。4.3 消息处理器的完整逻辑与 JSON 协议设计TextWebSocketHandler是 Spring 提供的基类重写handleTextMessage就能处理客户端发来的文本。协议用 JSON字段包括type消息类型、to接收者、content内容。服务端解析后先存库再转发。Component public class ChatWebSocketHandler extends TextWebSocketHandler { Autowired private SessionManager sessionManager; Autowired private MessageService messageService; private final ObjectMapper objectMapper new ObjectMapper(); Override public void afterConnectionEstablished(WebSocketSession session) { Long userId (Long) session.getAttributes().get(userId); sessionManager.add(userId, session); // 更新数据库在线状态 messageService.setOnline(userId, true); } Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { Long fromUserId (Long) session.getAttributes().get(userId); JsonNode node objectMapper.readTree(message.getPayload()); String type node.get(type).asText(); Long toUserId node.has(to) ? node.get(to).asLong() : null; String content node.get(content).asText(); // 先落库保证消息不丢 Message msg messageService.saveMessage(fromUserId, toUserId, content, type); // 构造转发给接收者的 JSON ObjectNode forward objectMapper.createObjectNode(); forward.put(type, chat); forward.put(from, fromUserId); forward.put(content, content); forward.put(time, msg.getCreateTime().toString()); // 单聊转发给 to群聊遍历群成员 if (single.equals(type) toUserId ! null) { sessionManager.sendTo(toUserId, forward.toString()); } else if (group.equals(type)) { // 省略群成员查询遍历调用 sendTo } } Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { Long userId (Long) session.getAttributes().get(userId); sessionManager.remove(userId); messageService.setOnline(userId, false); } }逻辑说明afterConnectionEstablished在连接建立时把会话注册进 Map同时把用户标记为在线。handleTextMessage是核心先解析 JSON再调用saveMessage落库最后构造转发消息。注意落库在前、转发在后这样即使转发失败消息记录还在用户刷新页面能拉到历史。参数上type字段区分单聊群聊to字段在群聊时可以不传由服务端根据群 ID 查成员列表。提示ObjectMapper建议注入 Spring 容器里的实例不要每次new否则自定义的日期格式配置不生效。课设里如果时间字段序列化出来是数组就是这个问题。5. 打包、部署与 PPT 演示让答辩现场不出洋相5.1 打成可执行 JAR 与常见启动失败排查Spring Boot 项目用 Maven 打包命令是mvn clean package -DskipTests。打出来的 JAR 在target目录下用java -jar chat-system-0.0.1-SNAPSHOT.jar启动。但这一步翻车率极高热搜里「java启动失败怎么解决」不是白搜的。最常见的三类问题端口被占用、数据库连不上、依赖冲突。端口占用报Port 8080 was already in use改application.yml里的server.port或者杀掉占用进程。数据库连不上报Communications link failure检查 MySQL 服务是否启动、URL 里的 IP 和端口是否正确、防火墙是否放行。依赖冲突报NoSuchMethodError或者ClassNotFoundException用mvn dependency:tree看冲突的包在pom.xml里排除掉旧版本。# 查看 8080 端口占用Linux/Mac lsof -i :8080 # Windows netstat -ano | findstr :8080 # 打包时跳过测试避免测试用例连数据库失败导致打包中断 mvn clean package -DskipTests # 指定配置文件启动方便切换本地和演示环境 java -jar chat-system-0.0.1-SNAPSHOT.jar --spring.profiles.activedemo5.2 项目展示 PPT 该放什么从架构图到真机截图标题里提到「项目展示 PPT」这不是让你堆文字。答辩 PPT 的核心是让老师在 5 分钟内看懂三件事系统能做什么、你怎么实现的、难点在哪。我一般建议 8 到 10 页第一页封面第二页功能演示截图登录、聊天、历史消息第三页技术栈和架构图第四页数据库表关系第五页核心代码片段WebSocket 处理器或者消息路由第六页遇到的难点和解决方式第七页总结和展望。架构图不要用花哨的 3D 图标用 draw.io 画个分层图就行前端页面、WebSocket 连接、Controller 层、Service 层、Mapper 层、MySQL。箭头标清楚数据流向。难点那页写真实踩过的坑比如「消息乱序」怎么用时间戳加前端排序解决「会话并发」怎么用ConcurrentHashMap保证线程安全。老师一听就知道是你自己做的。5.3 演示前的检查清单与后悔药演示前半小时按这个清单过一遍MySQL 服务是否启动、数据库是否导入、application.yml里的密码是否改成演示机的、JAR 包是否是最新打的、浏览器是否清过缓存、两个测试账号是否能同时登录。我吃过亏本地跑得好好的换到答辩教室的电脑上MySQL 版本不一样utf8mb4排序规则报错当场改配置改了十分钟。后来我习惯把 SQL 文件里的COLLATE去掉用数据库默认的兼容性更好。还有个小技巧提前录一段两分钟的操作视频放在 PPT 里。万一现场网络或者环境出问题直接放视频至少证明系统是能跑的。这不是作弊是工程思维——演示的可靠性也是交付质量的一部分。6. 从课设到能讲的项目三个让面试官多问一句的改造点课设做完只是起点如果你想让这个 Java 聊天系统在简历上真正有分量我建议做三个低成本改造。第一个是加消息已读回执客户端收到消息后回一个 ACK服务端更新消息状态前端显示「已读」。实现上就是在message表加status字段WebSocket 协议加一种ack类型。这个功能不复杂但能体现你对「消息可靠性」的理解面试时聊到 TCP 和应用层确认的区别你有实际例子。第二个改造是把在线状态从数据库挪到 Redis。课设里用user表的online字段没问题但高频读写会给数据库压力。用 Redis 的SETEX存online:userId过期时间 30 秒客户端心跳续期。这样即使服务端崩溃在线状态也会自动过期不会出现「用户明明退了还显示在线」的玄学问题。引入 Redis 还能顺带讲缓存穿透和雪崩面试话题直接打开。第三个改造是消息分页加载。课设里往往一次拉全部历史消息一多前端就卡。改成滚动加载每次拉 20 条用create_time和id做游标。SQL 上就是WHERE to_user_id ? AND id ? ORDER BY id DESC LIMIT 20比OFFSET分页在深分页时性能好得多。这个点能引出索引优化和游标分页的讨论比背八股文有说服力。-- 游标分页查历史消息lastId 是当前已加载的最小 id SELECT * FROM message WHERE (to_user_id ? OR from_user_id ?) AND id ? ORDER BY id DESC LIMIT 20;这条 SQL 的关键是id lastId代替OFFSET避免全表扫描。参数lastId由前端维护第一次传一个极大值或者不传条件。索引idx_to_user_time在这里不一定最优因为OR条件可能走不到索引实际项目里可以拆成两条查询再合并或者建(from_user_id, id)和(to_user_id, id)两个索引。这些取舍在答辩或者面试时讲出来比单纯说「我用了 MyBatis-Plus」有深度得多。我自己带人做课设这些年最大的教训就是别等到最后一天才联调。数据库、后端、前端三条线任何一条卡住都会连锁反应。我现在的习惯是第一天就把数据库建好、把最简单的登录接口跑通哪怕聊天功能还没写至少保证工程骨架是活的。后面每加一个功能就立刻用两个浏览器窗口验证问题不过夜。希望帮到你。本文还有配套的精品资源点击获取