
简介面向Java课程设计与毕业设计的完整资料包以“简单即时通讯工具”为开发实例包含源代码和论文两部分适合需要完整项目参考的Java学习者和毕业生。压缩包共713个文件、约5.05MB除42个java源文件与60个class文件外还有29个properties配置文件、数百个gif/jpg/png图片素材、wav消息音效以及doc格式论文覆盖配置、界面、提示音与设计文档。从内容预览可看出项目实现了客户端启动、好友管理、聊天窗口、群聊、用户查找、资料更新等典型功能class与java文件对应存在便于对照阅读源码、梳理程序逻辑。目前已有202人学习虽然没有大体量代码但功能模块完整适合毕业设计答辩参考和Java网络编程入门实践。1. 毕设选题撞上“即时通讯”这个Java课设到底值不值得做如果你正在为Java毕业设计选题发愁又不想卷电商秒杀、卷管理系统CRUD那“简单即时通讯工具的设计与开发”是一个性价比很高的方向技术栈是纯Java客户端用Swing服务端用Socket中间夹一层MySQL存用户和聊天记录整个系统跑在局域网甚至本机就能演示。这个题目几乎是照着“课设标准答案”长的——难度不高不低代码量大概两三千行覆盖面却足够广涉及网络编程、多线程、GUI事件分发、数据库读写答辩时每个点都能单独被追问出深度。这套源代码加论文的组合适合两类人一类是Java课程刚结束、想拿一个完整系统当毕设的学生另一类是初级工程师想补齐Socket编程和线程模型的基础。这个方向真正麻烦的不是“不会写”而是“跑不起来”——Swing窗口卡死、中文乱码、端口占用、客户端收不到消息这些坑会在你第一次运行时全部冒出来。后面的内容我会直接从架构和跑通步骤讲起再把最容易翻车的几个位置单独拉出来说透。2. 架构与技术选型为什么课设都用Socket Swing而不是WebSocket Vue2.1 三个硬约束评审口味、代码量、答辩追问先回答一个你可能纠结过的问题为什么这个项目不推荐做成网页版即时通讯最常见的理由是“网页版要先搭Tomcat、配WebSocket、写前端页面复杂度翻倍还不容易讲清楚”。毕业设计评审看重的不是技术新而是逻辑完整、模块清晰、能自圆其说。Socket Swing的好处在于所有通信逻辑都在你眼皮底下一条消息从客户端A发出经过服务端转发到客户端B这条链路中间没有任何框架帮你“魔法完成”答辩时你可以直接打开源码逐行解释这是WebSocket、Netty这些封装度高、动不动几百行的框架很难做到的。另一个硬约束是资料匹配度。你拿到的zip里面是源代码加论文如果坚持改成Web版等于把论文里所有架构图、测试截图、核心代码全部重写工作量直接翻倍。Swing虽然老但它在JDK里自带不需要额外装依赖导师打开电脑能直接跑这对答辩现场来说非常重要。我一般会给选型定三条底线一是技术栈必须覆盖Java核心知识集合、线程、IO、JDBC至少碰到两个二是演示必须能离线完成不依赖外网服务三是论文中能画出清晰的流程图和时序图。Socket Swing MySQL恰好同时满足。相比之下用Netty虽然简历好看但对课设来说有点“杀鸡用牛刀”而且并发模型一旦没讲清楚答辩被追问时很容易露馅。2.2 通信协议设计先定义消息格式再写代码动手写代码之前最关键的一步是定义协议。所谓协议就是客户端和服务端约定好的“一句话该怎么说”。很多新手直接上来就用ObjectOutputStream传Java对象这样虽然省事但跨版本兼容差而且消息体里一旦混入类结构变化两端就静默崩掉查起来非常痛苦。常见做法是用自定义文本协议每一条消息按类型加分隔符。比如我习惯的格式是LOGIN|用户名|密码 SEND|目标用户|消息内容 BROADCAST|发送者|消息内容 HEARTBEAT LOGOUT|用户名对应的响应格式LOGIN_OK|用户ID|昵称 LOGIN_FAIL|错误原因 MSG|发送者|时间戳|消息内容这种协议的好处有三个第一在Swing的聊天框里可以直接打印出来调试肉眼可见第二用String.split(\\|)就能解析不需要引入额外的JSON库第三论文里可以作为“设计成果”展示占篇幅又显得有思考。缺点是消息内容里如果包含|字符会截断出错所以发送前要做一个替换或转义。转义规则可以这样处理将消息内容里的|替换为#124;解析时再替换回来。2.3 服务端线程模型一个线程池撑起所有客户端服务端是整个系统的核心它的结构基本固定ServerSocket在指定端口监听每接受一个连接就交给一个SocketHandler处理。最简单的方式是为每个客户端创建一个线程但如果客户端数量上到几十个线程开销就会明显拖慢响应。课设阶段我推荐用ExecutorService线程池来管理这些处理线程既能展示对Java并发包的理解又不会太复杂。一个可用的线程池参数可以参考newFixedThreadPool(10)加一个ArrayBlockingQueue队列。固定线程数为10意味着最多同时处理10个客户端连接超出部分排队等待。如果课堂演示时老师问“为什么是10不是100”合理的回答是“课设场景下并发量有限固定线程池够用且不会因为线程切换导致CPU飙高”不要硬说自己能支撑高并发。关键设计点是每个客户端连接需要独立读消息但写消息要统一管理。常见的做法是维护一个ConcurrentHashMapString, Socketkey是用户名value是连接Socket。收到A发给B的消息时服务端从这个表里找到B的Socket把消息写出去。如果B不在线就把消息存入数据库的离线消息表等B上线后再拉取。这里顺带说明ConcurrentHashMap的弱一致性在遍历时可能看不到最新添加的连接但课设场景下不会造成致命问题答辩时能说出这个约束反而是加分项。2.4 数据库设计用户表、好友表、离线消息表三张起步数据库不用设计得太复杂但至少要有三张表用户表、好友表、离线消息表。好友表可以决定是做单双向好友校验——课设一般做双向验证即A添加BB同意后才能互发消息。管理员用不到不要画蛇添足加角色表。建表SQL可以这样写CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(32) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT 密码, nickname VARCHAR(32) DEFAULT NULL COMMENT 昵称, avatar VARCHAR(255) DEFAULT NULL COMMENT 头像路径, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE t_friend ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL COMMENT 用户ID, friend_id INT NOT NULL COMMENT 好友ID, remark VARCHAR(32) DEFAULT NULL COMMENT 好友备注, UNIQUE KEY uk_user_friend (user_id, friend_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT好友关系表; CREATE TABLE t_offline_msg ( id INT PRIMARY KEY AUTO_INCREMENT, from_user VARCHAR(32) NOT NULL COMMENT 发送者, to_user VARCHAR(32) NOT NULL COMMENT 接收者, content TEXT NOT NULL COMMENT 消息内容, send_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 发送时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT离线消息表;这里的核心点是字符集必须用utf8mb4不能用utf8否则用户昵称里出现Emoji表情会直接写入失败。另外password字段我建议明文存储因为课设论文里不涉及加密算法讲解强行用MD5反而增加答辩追问风险。如果你想让代码显得更专业可以用Base64做一层编码并注明这是演示用、生产环境需要加盐哈希——这个说法答辩时很加分。2.5 客户端技术Swing线程模型是最大的隐性门槛客户端用Swing做界面最核心的约束是Swing是单线程模型所有UI操作必须在事件分发线程EDT上执行任何阻塞操作放到EDT里都会让界面卡死。很多新手第一次跑通项目时一点“登录”按钮窗口就转圈然后提示“未响应”十有八九是把Socket的阻塞读放到了EDT里。正确的做法是界面线程只负责发送消息接收消息单独开一个SocketListener线程收到数据后用SwingUtilities.invokeLater把更新UI的操作切回EDT。这个问题的处理会在第5章里详细展开。这里要建立的概念是Swing本身不复杂复杂的是你必须在写代码前就意识到它的线程边界否则后面调试起来会非常折磨。3. 用IDEA在本地跑通全套项目从JDK到双端启动3.1 环境准备JDK 1.8是软件包的最稳选择这个项目基于Swing和Socket开发没有用到JDK 9以后的模块化特性所以我强烈建议直接用JDK 1.8。原因有两个第一IDEA里JDK 1.8对Swing可视化插件的支持最稳定第二如果你用JDK 17运行老项目经常会遇到--add-opens之类的模块访问警告虽然不影响运行但答辩时弹出一堆红色警告会让导师印象打折。安装时注意一个细节Windows系统上JDK安装完成后一定要在命令行里执行java -version确认版本。如果你的机器上已经装了JDK 21或更高版本可以在IDEA的Project Structure - SDK里单独指定1.8路径不要修改系统全局的JAVA_HOME否则其他项目可能受影响。数据库方面MySQL 5.7和8.0均可。如果电脑上没装MySQL建议装一个8.0社区版并记住端口号默认3306。驱动包方面项目lib目录下应包含mysql-connector-java的jar为了兼容8.0建议用8.0.x版本的连接驱动连接URL写法为jdbc:mysql://localhost:3306/im?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8。serverTimezone不加的话8.0驱动会报时区错误。3.2 导入项目与目录结构说明用IDEA导入时选择“打开”整个文件夹注意不要选src目录。等Maven如果有pom.xml或纯IntelliJ项目结构识别后你会看到典型的源代码组织方式└── src ├── main │ ├── java │ │ ├── com.im.client // 客户端登录窗口、聊天窗口、监听线程 │ │ ├── com.im.server // 服务端主启动类、客户端处理线程、消息转发 │ │ ├── com.im.common // 公共类协议常量、消息实体、DB工具类 │ │ └── com.im.util // 工具类字符串处理、日期格式化等 │ └── resources // 配置文件、日志配置 └── sql // 建表脚本如果你的压缩包里没有pom.xml说明项目是纯JDK 手动导包的方式。这种情况下IDEA需要手动把lib目录下的jar包添加到模块依赖里右键项目 - Open Module Settings - Dependencies -- JARs全部勾选即可。之后在Project Structure里确认JDK为1.8语言级别设为8。这里有个常见的“第一次导入就全是红叉”的情况通常是IDEA找不到JDK或没引入库。处理顺序是先设置JDKFile - Project Structure - SDKs再检查每个Module的Dependencies里是否有红色的漏项最后Build - Rebuild Project。如果全项目只有一两个文件报红多半是import路径写错检查com.im.common下的包名是否与目录一致。3.3 初始化数据库先建库再启动服务端在导入代码后、启动服务端之前必须先初始化数据库。如果你偷懒跳过这步服务端启动时会直接抛java.sql.SQLException原因是找不到数据库。用命令行或Navicat执行以下操作mysql -uroot -p SOURCE /你的项目路径/sql/init.sql; SHOW DATABASES;init.sql里包含建库、建表、插入两个测试账号。测试账号一般是alice/123456和bob/123456。这样你启动两个客户端后可以直接用这两个账号登录并互相私聊省去每次现注册的时间成本。如果命令行找不到mysql命令说明MySQL的bin目录没加进PATH可以在Navicat的查询窗口里执行同样的SQL。初始化后建议顺手执行一条SELECT * FROM t_user;确认用户名密码无误这个习惯能省掉后面“登录失败”时排查账号数据的半个钟头。3.4 修改数据库连接参数一行都不能错打开com.im.common包下的DB工具类或者resources目录下的db.properties文件。如果你拿到了源代码大概率看到的是类似下面这样的配置// DBUtil.java public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/im?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 123456; private static final String DRIVER com.mysql.cj.jdbc.Driver; public static Connection getConnection() throws SQLException { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new SQLException(MySQL驱动加载失败请检查lib目录); } return DriverManager.getConnection(URL, USER, PASSWORD); } }如果你的MySQL密码不是123456改PASSWORD那一行即可。另外注意驱动类名MySQL 5.7用com.mysql.jdbc.DriverMySQL 8.0用com.mysql.cj.jdbc.Driver两者写反会报ClassNotFoundException。这些参数的含义先记住useSSLfalse关闭加密连接本地开发不需要证书serverTimezoneAsia/Shanghai修正时区报错characterEncodingutf8保证中文和表情能正常写入。不需要开通公网访问权限本地回环地址就够完成全流程演示。3.5 启动顺序是硬规矩先Server后Client一切就绪后找到com.im.server包下的ServerMain类点运行。控制台出现“服务端已启动端口8888”之类的字样就说明监听成功。然后运行com.im.client包下的ClientMain会弹出登录窗口。连续启动两个客户端实例分别用两个测试账号登录。登录成功后窗口标题栏通常显示当前用户昵称和在线状态。接着在A客户端里选择B用户发送一条消息如果B窗口里出现消息说明全链路已经跑通。如果控制台报警告但仍能运行通常是时区或SSL警告不影响功能答辩前可以在启动类里加一行TimeZone.setDefault(TimeZone.getTimeZone(GMT8))把日志刷干净。如果客户端连不上服务端先别急着改代码用下面的命令验证telnet 127.0.0.1 8888能连上就说明网络层没问题问题出在协议或线程逻辑连不上则先看服务端是否在运行、端口是否被占用。把验证命令放在“改代码”之前能少走一半弯路。4. 三个核心模块拆解登录、消息转发和心跳如何用Java代码落地4.1 登录模块服务端校验密文客户端不回传密码明文登录流程是客户端把用户名和密码从JTextField和JPasswordField取出拼成LOGIN|用户名|密码字符串写入Socket输出流。服务端收到后解析协议查数据库验证返回LOGIN_OK或LOGIN_FAIL。这里有一个非常容易踩的坑JPasswordField.getText()已经被官方废弃正确做法是用new String(passwordField.getPassword())。虽然两者返回效果一样但用已废弃方法会在代码审查时被导师点名。另一个问题是密码不要在客户端做加密再传因为Swing客户端反编译非常容易加密等于白做。合理做法是把加密放在服务端的数据库访问层客户端只做传输。客户端发送登录请求的代码片段// 登录按钮的ActionListener中 private void doLogin() { String username usernameField.getText().trim(); String password new String(passwordField.getPassword()); if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(loginFrame, 用户名或密码不能为空); return; } // sendLogin会经过Socket输出流发送协议消息 networkService.send(LOGIN| username | password); }这段代码里networkService是一个封装了Socket输出流的类send方法内部做了synchronized包裹防止用户连续点击按钮时两条消息交错写坏一个流。注意trim()去除首尾空格——很多用户登录失败的原因是复制粘贴时多了一个看不见的空格。JPasswordField取出的字符串因为底层是char[]转成String后应立即置空尽量缩短密码在内存中的存活时间。服务端校验与响应的代码// ClientHandler.java 处理线程内部 if (protocolType.equals(LOGIN)) { String username parts[1]; String password parts[2]; User user userDao.findByUsernameAndPassword(username, password); if (user ! null) { if (onlineUsers.containsKey(username)) { send(LOGIN_FAIL|该账号已在其他设备登录); } else { onlineUsers.put(username, socket); send(LOGIN_OK| user.getNickname()); } } else { send(LOGIN_FAIL|用户名或密码错误); } }这段代码里有两个设计细节值得注意。一是用onlineUsers这个ConcurrentHashMap做了在线状态管理重复登录能被提前拦截。二是校验逻辑放在服务端而不是客户端保证绕过客户端直接连端口时没有安全漏洞。如果登录失败客户端收到LOGIN_FAIL后应当弹窗并清空密码框保留用户名这是Swing交互的基本习惯。如果你把LOGIN_FAIL后的分支写成了直接关窗口后面演示好友聊天时每次都要重新输入账号体验非常差。4.2 消息转发模块在内存中传递消息并支持离线暂存私聊和群聊在代码里差别不大。私聊是服务端找到目标Socket后直接转发群聊是遍历所有在线Socket逐个写。群聊的实现可以简单到在一个broadcast方法里遍历onlineUsers.values()但要注意ConcurrentHashMap遍历过程中如果有客户端退出会抛ConcurrentModificationException吗不会弱一致性迭代器不会抛这个异常它可能在遍历中漏掉或重复元素但不会崩溃。想要更稳妥可以给当前遍历的map加锁或者先将values()复制到一个ArrayList再遍历。消息转发核心逻辑public void handleMessage(String rawMessage) { // 协议示例SEND|目标用户|消息内容 String[] parts rawMessage.split(\\|, 3); String targetUser parts[1]; Socket targetSocket onlineUsers.get(targetUser); if (targetSocket ! null) { // 在线直接转发在消息前拼接发送者昵称 String forwarded MSG| this.username | System.currentTimeMillis() | parts[2]; PrintWriter writer new PrintWriter(targetSocket.getOutputStream()); writer.println(forwarded); writer.flush(); } else { // 离线存库等目标用户上线后再拉取 offlineMsgDao.save(this.username, targetUser, parts[2]); } }这里的split(\\|, 3)非常关键——限定了只拆分成3段第三段消息内容可以继续包含|而不被拆碎。如果不加这个限制用户在聊天里输入“你好|在吗”这种带竖线的句子消息会被截断这是文本协议的经典边界问题。另一处关键点是使用PrintWriter println而不是OutputStream.write因为println会自动追加换行符配合BufferedReader.readLine()读取时天然形成了消息边界。这里需要解释一下既然用了println那么消息内容里就不能出现换行符否则会被拆成两条脏数据。解决方案有两个一是发送前把\n替换成br收到后再还原二是在底层协议层用Base64编码整个消息内容彻底避开分隔符冲突。课设阶段用第一种就够了在代码注释里写明“换行符已被转义”即可。服务端在消息转发前还应当做一次敏感字符过滤最简单的实现是维护一个黑名单词表循环contains检查命中就拒绝发送并返回提示。这个功能不需要复杂算法但写在论文里可以占一个“消息安全策略”小节答辩时能多讲两分钟。4.3 心跳与掉线检测阻塞读线程卡住后如何发现Socket编程里有一个现实问题如果客户端直接拔网线、强制断电服务端的readLine()方法不会返回任何异常或null它只是静静卡住。于是onlineUsers表里会残留一个已经死掉的连接。如果其他用户给这个“僵尸账号”发消息服务端向一个失效Socket写入数据首次写入可能不报错这就会造成消息静默丢失。解决办法是心跳机制。服务端给每个连接挂一个定时任务比如每30秒检查一次该客户端最后一次活跃时间。客户端需要每20秒发送一个HEARTBEAT消息服务端收到后刷新时间戳。定时扫描线程每10秒执行一次发现超过45秒没有心跳的连接就主动关闭它并从在线列表移除。服务端心跳扫描代码// DaemonThreadFactory 使扫描线程随主线程退出而终止 ScheduledExecutorService scheduler Executors.newScheduledThreadPool(1, r - { Thread t new Thread(r, heartbeat-scanner); t.setDaemon(true); return t; }); scheduler.scheduleAtFixedRate(() - { long now System.currentTimeMillis(); for (String username : onlineUsers.keySet()) { Socket socket onlineUsers.get(username); UserSession session sessionMap.get(username); if (now - session.getLastHeartbeat() 45000) { System.out.println(客户端心跳超时强制下线: username); onlineUsers.remove(username); try { socket.close(); } catch (IOException ignored) {} } } }, 10, 10, TimeUnit.SECONDS);这段代码的启动参数需要专门说明scheduleAtFixedRate的第一个10是首次执行的延迟秒数第二个10是每次执行的间隔。扫描线程的判定阈值45秒是三倍的20秒心跳间隔——这个“三倍”不是玄学而是为了容忍一次心跳消息因网络抖动延迟到达不至于误杀正常在线用户。如果你把阈值设成25秒Wi-Fi一抖用户就被踢下线体验非常糟糕。客户端的线程模型也要配套调整SocketListener线程在阻塞读时应当每读到一个HEARTBEAT响应就刷新UI上的在线状态但不能在HEARTBEAT响应时清空聊天框内容。这里你需要注意客户端本身发心跳是定时任务但Timer默认运行在UI线程里执行Socket写入会极快地完成不会卡界面但如果你在心跳回调里读写一个较大的消息体就可能造成UI微卡顿稳妥做法是把心跳发送也丢进后台线程池执行。4.4 离线消息的拉取时机登录成功后先取再进主界面离线消息的拉取要放在登录成功之后、聊天窗口弹出之前。有的项目会把这个逻辑放在登录按钮事件里同步执行如果离线消息很多用户会看到“登录成功”后卡一两秒才弹出窗口这是数据量少时不易察觉、数据量大时立刻暴露的问题。正确做法是登录成功后就立即弹窗离线消息在后台线程里拉取完成后再通过invokeLater追加到聊天界面顶部并在窗口标题栏显示“收到N条离线消息”。这样就要处理一个并发问题用户可能一边看消息一边有新的在线消息进来两处同时往聊天记录区域追加文本Swing组件在EDT线程中完成了合并。这里不急着展开有关并发写入的细节先记住一个结论任何对Swing组件的写入都通过SwingUtilities.invokeLater包一层。这个习惯能让你避开绝大多数Swing线程陷阱。5. 避坑实录跑通这个项目最容易踩的五个经典坑5.1 坑一客户端等半天没响应原来是读写流不匹配现象客户端点击发送按钮后服务端控制台能打印收到消息但客户端界面一直不更新也没有报错。原因这是非常典型的“流类型错配”问题。服务端用BufferedReader.readLine()按行读但客户端写消息时用的是ObjectOutputStream.writeObject()或者OutputStream.write(bytes)没有追加换行符。readLine()没有读到换行符就不会返回于是服务端看起来像“卡住”了。解决统一通信两端的数据流类型要么全部用BufferedOutputStream println的方式按行写要么全部用DataOutputStream.writeUTF()和DataInputStream.readUTF()成对使用。我一般会在项目里加一个ProtocolUtil工具类把消息发送封装成一个统一方法这样两端都调用同一个工具就不会出现半个项目按行读、半个项目按对象读的混乱局面。自检时可以先在两端各写一个System.out.println(发送: message)确认发出内容再在接收端打印原始消息看看是“没发出去”还是“发出去但解析不了”。5.2 坑二中文乱码问题源头不在代码而在文件编码现象客户端窗口标题正常但用户昵称和聊天内容里的中文全是乱码。原因绝大多数情况下不是数据库字符集的问题而是IDEA或Eclipse的项目文件编码与Java默认编码不一致。Windows中文系统默认的IDE文件编码是GBK如果你把源码保存成UTF-8但编译器按GBK读所有中文字符串常量全变乱码。数据库连接URL里的characterEncodingutf8只能保证数据库侧的读写正确解决不了源码层面的编码错位。解决先看IDEA右下角的文件编码显示把所有源文件统一改成UTF-8并在Settings - Editor - File Encodings里把全局编码、项目编码、默认编码全设为UTF-8。打开Help - Edit Custom VM Options加一行-Dfile.encodingUTF-8。改完后Build - Rebuild Project并重启客户端。这里注意如果数据库里已经存了GBK的乱码数据要清掉对应表重新初始化不要直接在乱码数据上继续测试。检查乱码是否清干净可以用一条SQL查询看nickname字段在Navicat中是否正常显示中文正常后再继续下一步。5.3 坑三重启服务端时报端口被占用只能改端口重启现象服务端上一次关闭后再次启动控制台抛出java.net.BindException: Address already in use: JVM_Bind服务起不来。原因服务端不是正常退出而是IDE里点了红色停止按钮强制中断此时ServerSocket没有走到close()方法操作系统仍然认为这个端口被占用。Windows下端口释放有最长到数分钟的等待时间尤其在TIME_WAIT状态下。解决先别急着改端口号。执行以下命令找到占用进程netstat -ano | findstr 8888 tasklist | findstr java taskkill /PID 占用PID编号 /F如果频繁出现这个情况建议把ServerSocket的启动放进一个try-catch-finally在finally里调用serverSocket.close()。同时在服务端主类中显式声明public static volatile boolean running true;并在关闭钩子Runtime.getRuntime().addShutdownHook()中做清理。这只是从程序上做善后防止“Socket没有干净关闭”的另一种表现——close()后线程仍在调用Socket读取导致连接看起来还活着。5.4 坑四Swing窗口卡成“沙滩球”移动一下才恢复现象登录窗口还能操作但点击“登录”后整个窗口立即无响应鼠标拖到Windows任务栏上会显示“未响应”过一会儿又自己恢复。原因阻塞读操作直接放在了事件分发线程EDT中。点击登录按钮后当前线程要对Socket执行BufferedReader.readLine()这是一个阻塞调用会一直等消息返回。如果服务端恰好不发任何数据Swing的EDT就被卡死了。解决严格按照Swing单线程模型来拆分逻辑。客户端启动时SocketListener作为独立的守护线程执行阻塞读收到消息后调用SwingUtilities.invokeLater更新UI。登录按钮的事件回调里只做发送请求的操作不做接收等待。判断是否写对了看代码里Socket的读取是否和JButton的ActionListener出现在同一个类方法中——如果同一个方法里先写了readLine()再写setText()基本就是错的。把接收监听拆成一个内部类SocketListener implements Runnable它唯一职责就是循环readLine()然后按协议分发消息给不同的事件处理方法。5.5 坑五消息能发出去但聊天记录区不刷新加了repaint也没用现象服务端收到并转发了消息目标客户端的控制台也能打印收到消息但聊天框区域不显示新内容手动拉伸窗口后消息才出现。原因这不是线程问题而是Swing文本组件的刷新被干扰了。当你用JTextArea.append()在EDT中追加文本后组件本身不需要手动repaint()。但如果你在追加之前对JTextArea调用了setEditable(false)在部分JDK版本上会触发组件重绘失效导致内容在屏幕外缓冲更新但界面不显示。解决用JTextPane替代JTextArea配合StyledDocument插入字符串。很多课设代码里都倾向于用JTextArea因为它简单但如果你遇到“不刷新”的情况换成JTextPane是最快的解决路径。还有一种更简单的做法每次追加文本后调用textArea.setCaretPosition(textArea.getDocument().getLength())把光标移到末尾这个操作会强制Swing刷新到当前文本行。不要用Thread.sleep来等重绘——这是典型的“用玄学修Bug”一旦时序抖动反而让界面更卡。6. 从“跑通”到“答辩”离线验证、增量设计、评分项自查“跑通”只是第一步答辩时老师更关心的是你有没有理解自己写的东西以及能不能现场被追问出边界。拿这套代码你可以按下面几步完成最后阶段的准备我按自己的经验从高性价比开始排序。先做一次完整链路验证项目里通常预置两个测试账号把这个流程走一遍——注册一个新账号、用新账号登录、与测试账号互发消息、发送时带特殊字符|和换行、关闭一个客户端后让另一个用户发消息后再登录。用浏览器进入H2或直接用Navicat查看离线消息表确认离线消息在用户上线后被正确拉取。这个验证过一遍后答辩中“是否支持离线消息”这类问题你就有了实打实的答复。其次是性能观察。开三个客户端同时登录快速连续发送30条消息观察服务端控制台是否出现信息堆积或线程异常。这个测试还有一个隐性价值如果某条消息发送失败你能用System.currentTimeMillis()打印每次转发耗时很快定位是转发逻辑耗时长还是数据库存储慢。对于普通课设而言30条消息全链路延迟不超过2秒就算合格不需要引入中间件做压力测试。最后是代码注释和命名规范。复盘的教训是哪怕项目功能完整如果SocketHandler里有一堆a1、temp这类命名答辩时自己都容易看混。花一个晚上清扫所有变量和方法名把中文注释补齐再把System.out.println统一换成Logger输出。这一项投入产出比极高——很多答辩老师的评分逻辑是“源码可以不精美但必须能看出你认真整理了”。新增功能的话不要贪多挑一个方向做深就足够。常见加分改动有三个一是聊天记录落库并加历史查询窗口代码量大概增加一百多行二是增加一个简单的“正在输入”状态提示通信协议里新增一个TYPING|用户名类型即可三是给消息内容做表情替换将类似[微笑]的文本替换为图片路径。挑一个你最感兴趣的去改改完后记得把数据库表结构和协议文档同步更新到论文中保持“代码与论文一致”这条底线。我自己的习惯是完成后保留一份“部署检查清单”JDK版本、数据库脚本、账号密码、启动顺序、常用端口。这样即使隔三周再打开项目也能十分钟内重新跑起来不会因为忘了某个环境参数而急得冒汗。做毕业设计最怕的不是技术难而是“做完了却讲不清楚”。这个项目虽然简单结构却完整足够你在答辩台上把网络编程、并发、GUI、数据库四条线都讲出细节。希望帮到你。本文还有配套的精品资源点击获取