简介这是一份基于 Java Swing、多线程与 MySQL 实现的仿 QQ 聊天室项目适合作为 Java 期末大作业或课程设计参考。系统覆盖用户注册、登录、找回密码、查看在线人员、群聊/私聊、修改密码、注销账户等完整功能源码结构清晰包含 34 个 Java 源文件与对应 class 编译文件可直接对照学习或二次开发。压缩包共 291 个文件、21.46MB另有 178 张 png 界面效果图、16 个 xml 配置、1 个 sql 数据库脚本和 1 份项目展示 PPT从运行环境搭建到答辩展示均有配套材料。项目已调试通过、无 bug已有 587 人学习下载对于需要快速搭建聊天室项目、理解 Swing 界面编程与多线程通信的同学具有较高参考价值。1. 从课程设计到能跑的聊天室这套 Java Chat 到底值不值得你动手如果你在找「java做的chat聊天系统附带数据库文件、项目展示PPT」大概率是正在做 Java 课程设计、毕业设计或者想用一个小项目把 JSP/Servlet、数据库、前端三件套串起来。先说结论这类项目的定位是「教学闭环」不是「生产级 IM」。它的价值在于——数据库文件让你不用从零建表PPT 让你不用从零憋答辩稿而你真正要做的是把代码跑起来、讲明白、能改能扩展。反直觉的是这种「带数据库带 PPT」的项目最大的坑反而不是代码跑不起来而是你拿到手后不知道该改哪里、更不知道该在答辩时突出什么。本文会从选型、建表、实时消息三条主线拆开讲最后落到你一定会遇到的乱码、连接池和版本兼容问题上。适合正在做 Java 课程设计、或者准备用 SSM/Spring Boot 重写旧项目的人照着重现一遍。2. 技术选型先想清楚Servlet 还是 Spring Boot这不是面子问题2.1 这类带数据库和 PPT 的项目常见技术栈是哪一套我见过几十个「Java chat 聊天系统」课程设计技术栈高度集中。绝大多数是 JSP Servlet MySQL Tomcat偶尔有加了 Bootstrap 和 jQuery 的再新一点的是 Spring Boot MyBatis WebSocket。你拿到手的项目是哪一套直接决定你要装什么环境、怎么部署——这不是偏好问题是你能不能跑起来的现实问题。JSP Servlet 这套老组合的好处是不需要 Maven 拉依赖不需要配置数据源框架一个 Tomcat 丢进去就能跑适合机器上只有 JDK 和 Tomcat 的课程设计环境。Spring Boot 版本的好处是内嵌 Tomcat启动就是一个 main 方法前后端分离也好做但你需要 Maven、需要联网拉依赖、需要处理端口冲突。我的建议是——先看你的 JDK 版本。如果你装的是 JDK 8两个都能跑如果你装的是 JDK 17 及以上老项目里的 javax.servlet 包可能直接编译报错那就要么换 JDK 8要么选 Spring Boot 3 jakarta.servlet 的新版本。这个兼容性问题是这类项目翻车的第一大原因后面避坑章节会细说。2.2 用 Servlet 手写登录和注册最小的可运行骨架不管最终选哪套登录注册都是聊天系统的入口。如果是 SSR 架构流程是浏览器提交表单 → Servlet 接收参数 → 查数据库验证 → session 记录登录态 → 重定向到聊天主页。以下是核心代码骨架这段逻辑在任何 Java Web 课程设计里都能直接复用WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); // 用 PreparedStatement 防 SQL 注入避免拼音拼接字符串 String sql SELECT id, nickname FROM user WHERE username ? AND password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { HttpSession session req.getSession(); session.setAttribute(userId, rs.getInt(id)); session.setAttribute(nickname, rs.getString(nickname)); resp.sendRedirect(chat.jsp); } else { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(login.jsp).forward(req, resp); } } } catch (SQLException e) { e.printStackTrace(); resp.sendError(500, 数据库访问异常); } } }这段代码有三个参数细节值得你注意。第一WebServlet(/login)是 Servlet 3.0 的注解式注册前提是 Tomcat 7 以上如果你的 Tomcat 是 6 以下的古董必须去 web.xml 里配servlet-mapping否则 404。第二conn.prepareStatement(sql)的写法就是为了防注入——课程设计答辩时老师基本必问这一句和你手拼字符串的区别回答「PreparedStatement 会预编译、参数化传值SQL 语句和数据分离」这一分就拿到了。第三登录成功用sendRedirect而不是forward是为了避免刷新页面时重复提交表单这是 Web 开发的基本功答辩时讲「PRG 模式」会显得你水平在线。2.3 连接池为什么要配怎么配不配你的 Chat 撑不过 10 个用户课程设计里最常见的跑路姿势是数据库工具有 Navicat但代码里每次DriverManager.getConnection()现连现断。这样写小 demo 没问题但聊天系统是高频请求场景——每个用户每次拉取好友列表、发消息、刷新在线状态都要建连而 MySQL 默认的max_connections也就 151连接一多直接就Too many connections了。而且每次建连的握手开销在局域网里可能感觉不到一旦答辩现场用的是无线网络延迟立刻放大。正确的做法是配数据库连接池。如果你拿到的是 SSM 或 Spring Boot 项目Druid 和 HikariCP 二选一如果是纯 Servlet 项目至少用 Apache DBCP 或者 C3P0。以下是最常见的 Druid 配置模板可以直接抄进application.properties或独立 properties 文件里spring.datasource.druid.urljdbc:mysql://localhost:3306/chat_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai spring.datasource.druid.usernameroot spring.datasource.druid.password123456 spring.datasource.druid.initial-size5 spring.datasource.druid.min-idle5 spring.datasource.druid.max-active20 spring.datasource.druid.max-wait60000 spring.datasource.druid.validation-querySELECT 1这里serverTimezoneAsia/Shanghai是很多人忽略的坑——MySQL 8 默认时区是 UTC你不加这个参数存时间字段会差 8 小时。还有validation-querySELECT 1它保证从池里拿出来的连接是可用的避免数据库重启后拿到一堆失效连接。initial-size和min-idle都设成 5意思是应用启动就预建 5 条连接空闲时也维持 5 条不要等用户来了才现建。至于max-active20对课程设计的体量绰绰有余记着这个数不要乱改——你又不是在跑双十一。3. 数据库文件拆开看三张表撑起一个聊天系统的增删改查3.1 用户表、好友表、消息表字段设计背后的理由标题说附带数据库文件你导入后第一件事不是看数据而是看表结构。一个标准的聊天系统库核心就三张表user、friend、chat_message。很多课程设计还会加一张friend_request好友申请表和group_chat群聊表但那是加分项不是必需品。以下是你会频繁操作的核心表结构CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录账号, password VARCHAR(64) NOT NULL COMMENT 登录密码, nickname VARCHAR(50) DEFAULT NULL COMMENT 显示昵称, avatar VARCHAR(255) DEFAULT default.png COMMENT 头像路径, status TINYINT DEFAULT 0 COMMENT 在线状态0离线 1在线, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE friend ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL COMMENT 用户ID, friend_id INT NOT NULL COMMENT 好友的用户ID, remark VARCHAR(50) DEFAULT NULL COMMENT 好友备注名, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_friend (user_id, friend_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE chat_message ( id INT AUTO_INCREMENT PRIMARY KEY, from_user_id INT NOT NULL COMMENT 发送方用户ID, to_user_id INT NOT NULL COMMENT 接收方用户ID, content TEXT NOT NULL COMMENT 消息内容, msg_type TINYINT DEFAULT 1 COMMENT 1文本 2图片 3文件, is_read TINYINT DEFAULT 0 COMMENT 0未读 1已读, send_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 发送时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;为什么user表要把username设为 UNIQUE因为账号是登录凭证唯一约束在数据库层兜底比你在 Java 代码里先查后插要可靠得多——并发注册时两个请求同时查到「不存在」就会重复插入有唯一索引直接报错你 catch 一下告诉用户「账号已被注册」就行。friend表加联合唯一键uk_user_friend也是同理防止 A 重复添加 B 为好友。chat_message表必须要is_read字段否则你根本没法实现「未读消息数」这种看着不起眼、其实是聊天系统核心体验的功能。3.2 从 SQL 文件到跑起来导入数据库的三个动作拿到数据库文件后新手最常见的翻车点是直接在 Navicat 里双击 .sql 文件然后发现表建好了但中文乱码。正确的导入路径是先建库、再选字符集、再导数据。命令行做法如下mysql -u root -p # 进入 MySQL 后执行以下三条语句 CREATE DATABASE IF NOT EXISTS chat_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE chat_db; SOURCE /path/to/chat_db.sql;第一条语句的utf8mb4是关键——你要存 Emoji 表情聊天内容里极其常见utf8是存不下的会直接报错或者变成问号。COLLATE utf8mb4_unicode_ci是排序规则选它是因为对中文和特殊字符的排序更符合直觉。如果你用的是 Navicat 这种图形工具对应操作是右键连接 → 新建数据库 → 字符集选utf8mb4→ 排序规则选utf8mb4_unicode_ci然后右键该库 → 运行 SQL 文件 → 选择你拿到的 .sql。导入后不要急着看数据先执行一条SHOW TABLES;确认表数量跟你预期一致再打开user表看看有没有初始账号——很多课程设计会预置一个admin/123456这能帮你省掉注册流程直接进聊天页。3.3 数据库文件里的数据一致性Java 面试里怎么问都不虚标题既然带了数据库文件说明数据是预先灌好的。但你要知道聊天系统对数据一致性天然敏感。举一个最经典的场景A 给 B 发了一条消息写进chat_message表但 B 刷新后看不到——为什么最常见原因是写入成功了但你查询时用了to_user_id而前端传参把字段名写成了toUserIdMyBatis 里没做驼峰映射结果查出来是 null。另一个更隐蔽的坑是事务如果你在「发送消息」接口里同时做两件事——插入消息记录 更新好友的未读计数——这两步必须在一个事务里否则消息入库了计数没更新用户就永远看不到红点。Transactional(rollbackFor Exception.class) public void sendMessage(Integer fromUserId, Integer toUserId, String content) { chatMessageMapper.insert(new ChatMessage(fromUserId, toUserId, content)); friendMapper.incrementUnread(toUserId, fromUserId); }Transactional(rollbackFor Exception.class)这个注解不是加上就完事了。关键在于Spring 默认只在 RuntimeException 时回滚如果你自己的业务异常继承的是Exception不加rollbackFor就不会回滚——消息插进去了、计数更新失败了数据就脏了。另外注意事务要生效这个方法必须通过 Spring 代理调用不能同类内部this.sendMessage()直接调那是事务失效的重灾区面试和答辩时被问到「事务为什么没生效」十有八九是这个原因。4. 聊天的核心从轮询到 WebSocket实时消息到底怎么选4.1 轮询方案代码简单但会被人问「这是实时聊天吗」如果你拿到的项目用的是 Ajax 定时刷新消息列表也就是每 3 秒setInterval发一次请求拉取新消息你千万别急着换成 WebSocket——先把它跑通再考虑升级。轮询方案有它存在的合理性实现简单、没有长连接管理、不会因为代理服务器或防火墙断连。在课程设计这个体量下10 个用户轮询每 3 秒一次请求MySQL 和 Tomcat 毫无压力。核心逻辑是前端定时器 后端查新消息接口。后端接口一般长这样接收当前用户 ID 和最后一次拉取的消息 ID然后查WHERE to_user_id ? AND id ?返回比上次更新的消息。这里有个细节要提醒你——「增量查询」用id 上次最大id比用send_time 上次时间更准确因为在同一秒内发了两条消息时间戳相同按时间去查会漏消息。按 ID 查不会漏这是轮询方案最重要的一条性能与准确性平衡点。4.2 WebSocket 方案用 Java 原生注解实现服务端主动推送如果你想让系统看起来更像「实时聊天」WebSocket 是更优解。它的优势是一次握手后建立长连接服务端可以主动推送消息给指定用户不用客户端反复问「有没有新消息」。最常见的做法是用 Java 的javax.websocket原生注解Java EE / Tomcat 自带也可以用 Spring 的WebSocketHandler。以下是课程设计中最容易理解的一套ServerEndpoint(/chat/{userId}) public class ChatEndpoint { // 用 ConcurrentHashMap 存所有在线会话key 是用户ID private static final MapInteger, Session ONLINE_SESSIONS new ConcurrentHashMap(); OnOpen public void onOpen(Session session, PathParam(userId) Integer userId) { ONLINE_SESSIONS.put(userId, session); // 可以在这里把 user 表的 status 字段更新为 1在线 } OnMessage public void onMessage(String message, Session session) { // 消息格式约定为 JSON{toUserId: 2, content: 你好} // 解析后从 ONLINE_SESSIONS 找到目标用户推送 JsonObject payload JsonParser.parseString(message).getAsJsonObject(); Integer toUserId payload.get(toUserId).getAsInt(); Session targetSession ONLINE_SESSIONS.get(toUserId); if (targetSession ! null) { targetSession.getBasicRemote().sendText(payload.get(content).getAsString()); } // 如果目标不在线就只存数据库等对方上线后再拉取 } OnClose public void onClose(PathParam(userId) Integer userId) { ONLINE_SESSIONS.remove(userId); // 相应把 status 更新为 0 } }这段代码的ONLINE_SESSIONS必须是ConcurrentHashMap因为 WebSocket 是多线程环境下并发访问的用普通 HashMap 会在高并发时出现 CPU 100% 甚至死循环。另外注意OnClose里的PathParam(userId)——如果你的路径参数是从OnOpen里解析的OnClose里也要重新声明一次因为容器不会帮你保存上个方法的参数。最大的坑是如果你用 Nginx 做了反向代理记得配置proxy_set_header Upgrade $http_upgrade;和proxy_set_header Connection upgrade;否则 WebSocket 握手一定失败浏览器控制台会一直报WebSocket connection to ws://... failed——这个问题能卡你一下午而且和 Java 代码本身半毛钱关系都没有。4.3 离线消息怎么补WebSocket 连接不是无线的实时聊天的另一半是「离线消息」。你要清楚一点WebSocket 只能覆盖「双方都在线」的场景。只要 A 给不在线的 B 发了消息B 下次登录时必须有办法把错过的消息捞回来。这个逻辑几乎必然落在数据库上——发送时先写chat_message表目标在线则顺便推 WebSocket不在线就等对方登录后查库。具体做法是在登录进聊天页时调一个「拉取离线消息」接口SELECT * FROM chat_message WHERE to_user_id ? AND is_read 0然后把is_read批量置 1。这里有个体验细节如果你把所有未读消息一次性查出来全部置为已读用户根本看不出哪条是新的更好的做法是按from_user_id分组先展示「谁给你发了消息」点开好友再看到具体内容。消息体可以顺手返回发送方昵称和头像路径——一条 SQLJOIN user就能搞定别在前端拿一个fromUserId再挨个查用户信息那是 N1 查询的经典反面教材。5. 避坑指南从数据库连接到页面乱码这 6 个坑我帮你先踩5.1 中文乱码从 Tomcat 到 MySQL 每一层都得是 UTF-8现象注册时填中文昵称登录后显示「??」或者发送中文消息收到方看到一串乱码。原因字符编码在传输链路上被某一步转成了 ISO-8859-1 或 GBK。解决按顺序检查四层。第一JSP 页面顶部加% page contentTypetext/html;charsetUTF-8 %第二Servlet 里req.setCharacterEncoding(UTF-8)必须在getParameter()之前调用第三MySQL 连接串加characterEncodingutf8建库时指定utf8mb4第四Tomcat 的server.xml里给 Connector 加URIEncodingUTF-8。如果以上都做了还乱码十有八九是你在 IDE 里复制粘贴了别人的源码而 IDE 默认编码是 GBK——全选文件改成 UTF-8 保存再重新编译这个坑最阴间但它真的存在。5.2 数据库文件导入后外键报错现象用 Navicat 导入 .sql 文件提示Cannot add foreign key constraint表建了一半就停了。原因导入顺序不对。你拿到的 SQL 文件里如果先建了chat_message表它外键引用user表但user表还没建出来外键约束就报错了。解决用命令行SOURCE方式导入让 MySQL 按文件顺序逐条执行如果图形工具一次性执行失败把 SQL 文件打开手动把建表语句按依赖顺序拆开执行——先user再friend最后chat_message。另外一个常见原因两张表的引擎不一致一张是 InnoDB、一张是 MyISAMInnoDB 支持外键MyISAM 根本不认外键也会导致同样的报错。5.3 JDK 17 跑老项目javax 还是 jakarta一句话的事现象编译报错package javax.servlet does not exist。原因JDK 9 开始模块化JDK 11 移除了 Java EE 模块Java EE 也改名为 Jakarta EE——Servlet 的包名从javax.servlet变成了jakarta.servlet。Tomcat 9 及以下是javaxTomcat 10 及以上是jakarta。解决要么把 JDK 降到 8 并用 Tomcat 9要么把源码里所有javax.servlet批量替换成jakarta.servlet。降 JDK 是最省事的路——装一个 JDK 8IDE 里 Project Structure 把 SDK 切过去Tomcat 也换成 9全程不到五分钟。硬着头皮换 jakarta 包名也不难但后续 Spring、JSP 等库的版本可能也要跟着升级不建议课程设计阶段折腾。5.4 数据库连接池配置正确但启动报「Access denied」现象Tomcat 启动后第一次访问登录接口控制台弹出java.sql.SQLException: Access denied for user rootlocalhost (using password: YES)。原因密码错了或者 MySQL 8 的认证插件是caching_sha2_password而你的连接池驱动是旧版 MySQL Connector/J 5.x不支持这个新认证方式。解决先检查配置里的 username/password 和实际一致不一致就改配置。如果一致还报错就是驱动版本问题——换成mysql-connector-java8.0.x并且把依赖里旧的 5.1.x 排掉。还有一个隐藏坑如果 MySQL 8 里你给 root 账号设置的是空密码注意配置里password后面空着和没有这个属性是两回事别在这上面耗时间。5.5 「明明在登录页跳转到了 chat.jsp但马上又弹回登录页」现象登录成功后跳转到聊天页刷新一下就被拦截器踢回登录页。原因session 没有持久化或者拦截器放行条件写错。常见拦截器逻辑是判断session.getAttribute(userId) null就重定向到登录页——但如果你在登录后把用户数据存进了请求域request而不是会话域session跳转后自然就丢了。解决登录成功时务必存 session而不是req.setAttribute。另一个脏坑是你用了 Spring Security 或 Shirocsrf 校验把表单请求拒了但不报错直接重定向——看浏览器 Network 面板如果chat.jsp的状态码是 302 而不是 200问题大概率不在你的代码而在过滤器链的配置上。5.6 PPT 上被问到「单点登录怎么处理」直接答不上来现象答辩演示时老师看着 PPT 里「登录模块」这一页随口问了一句「用户已经在聊天页面了再打开一个标签页访问登录页会怎么样」。你愣住。原因你没有处理「已登录用户重复登录」的情况——现在大多数课程设计不会处理但这正是拉开分数差的地方。解决一个最小实现是——登录成功时把 userId 写进 session同时在数据库user表维护一个session_id字段每次访问受保护资源时拦截器比对当前 session 的 id 和数据库里存的 session_id不一致就强制下线。这样实现成本不高但答辩效果立刻不一样——这证明你想过并发登录的问题而大多数人没想过。6. 让项目从「能跑」到「能加分」Session 管理和答辩演示的三个细节先搞清楚一个概念HTTP 是无状态的WebSocket 管的是实时消息通道但你的登录状态依然靠 Session 维持。一个合格的聊天系统至少要能在两个地方用到 Session登录后把userId和nickname放进去以及过滤器里拦截未登录请求。以下是一个极简过滤器写法WebFilter(/*) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); // 放行登录页、静态资源和登录接口本身 if (uri.endsWith(login.jsp) || uri.endsWith(login) || uri.endsWith(.css) || uri.endsWith(.js)) { chain.doFilter(req, resp); return; } if (request.getSession().getAttribute(userId) null) { response.sendRedirect(login.jsp); return; } chain.doFilter(req, resp); } }这段代码要特别留意放行条件不要写成uri.contains(login)因为chat_login、user_login_image这类路径也会被放行造成逻辑漏洞。endsWith只是其中一种写法更严谨的是用 URL 白名单列表去匹配。会话超时时间默认是 30 分钟课程设计的演示场景里可以调短一点——在web.xml里配session-configsession-timeout15/session-timeout/session-config。答辩演示层面的三个建议。第一演示前先把chat_db里预置的几个账号网页打开别现场注册——注册时如果昵称撞了唯一索引报错弹窗会让你手忙脚乱。第二演示实时聊天时开两个浏览器窗口Chrome 和 Edge 各一个一个发消息、一个收消息比在一个页面里自己和自己说话可信得多也顺便验证了不同 Session 之间互不干扰。第三PPT 里讲到数据库那一页时别只贴建表语句——把 ER 图放上去用箭头标出friend.user_id → user.id和chat_message.from_user_id → user.id的关系这是评委一眼就能看懂的数据库设计证据比任何文字都有力。最后说一下我自己的习惯拿到任何一份带「数据库文件 PPT」的课程设计代码第一件事永远是打开DBUtil.java或application.properties看数据库连接配置改成本地账号密码后跑一遍登录——这一步能过滤掉八成“代码没问题、环境没配好”的隐藏问题。然后用五分钟通读表结构确认三张核心表之间没有外键循环依赖再去看 WebSocket 或轮询的代码位置。这套流程走下来你答辩时被问「系统整体怎么跑的」的时候脑子里会有一条清晰的线用户从登录页进来Session 记住了我是谁我发消息时先落库再推送对方不在线就等上线后拉取离线消息——就这么简单但能把这条线讲清楚的人分数都比只会点开演示页说「这是登录、这是聊天」的人高一个档。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?