简介这份资源是一套面向高校Java课程实验设计的完整源码方案围绕C/S架构的小型档案管理系统展开适合正在完成面向对象与多线程综合实验的学生以及需要Socket通信与数据库整合练习的开发者参考。压缩包共47个文件约1.37MB包含14个java源文件与15个class编译文件另有sql建库脚本、txt说明文档、jar依赖包及docx实验指导书覆盖客户端与服务器端两套工程结构。系统实现了用户登录与权限分级、档案文件上传下载、条件查询、删除及用户信息修改等功能服务器端采用多线程响应多客户端请求用户与档案属性存入关系数据库档案实体以文件形式保存在服务器目录。目前已有875人学习下载读者可据此获得可直接运行的实验工程、数据库脚本与依赖配置便于对照实验要求理解Socket通信、多线程处理与JDBC操作的整合思路快速完成课程设计或二次开发。1. 从一份 Java 实验设计说起小型档案管理系统到底能跑通什么很多同学做 Java 课程实验时最头疼的不是写不出代码而是不知道一个「完整系统」该长什么样。平时练习都是单文件、单方法一到实验设计就要求有界面、有数据库、有网络通信还要能演示增删改查直接卡住。这份「Java实验设计-实现一个小型档案管理系统」就是冲着这个场景来的它把 Java 基础语法、JDBC 数据库操作、Socket 网络通信和 Swing 图形界面串成一个能跑起来的闭环适合正在做课程设计、需要交实验报告、或者想拿一个完整小项目练手的同学。档案管理这个题目本身不复杂但麻雀虽小五脏俱全正好用来理解一个桌面应用从客户端到服务端再到数据库的完整链路。2. 系统骨架拆解三层结构怎么分、数据怎么流2.1 为什么选 C/S 架构而不是纯本地单机拿到这个题目第一个要做的决策是架构选型。纯本地单机版本最简单所有逻辑塞进一个 JFrameJDBC 直连数据库代码量少、调试快。但实验设计通常要求体现「网络编程」能力所以这份资源采用的是 C/S 架构客户端负责界面展示和用户交互服务端负责业务逻辑和数据库读写两者通过 Socket 传输对象或 JSON 字符串。这么选的理由有三个。第一Socket 编程是 Java 实验的常见考点用上就能在报告里单独写一章。第二C/S 分离之后客户端不需要数据库驱动部署时只需要一个可执行 jar 加一个服务端地址演示方便。第三后续如果想改成多客户端同时在线服务端加线程池就行扩展路径清晰。代价也很明显调试变麻烦客户端报错可能根子在服务端对象序列化要注意版本一致端口占用、防火墙拦截这些破事都会遇到。我一般建议先把单机版跑通确认 JDBC 和界面没问题再拆成 C/S不然两个问题叠在一起排查起来很痛苦。2.2 档案实体的字段设计与建表语句档案管理系统的核心实体就是「档案」。字段不用多但要覆盖典型类型字符串、日期、枚举。下面是我根据常见实验要求整理的表结构可以直接拿去用。-- 档案主表存储档案基本信息 CREATE TABLE archive ( id INT PRIMARY KEY AUTO_INCREMENT, -- 自增主键 archive_no VARCHAR(32) NOT NULL UNIQUE, -- 档案编号业务唯一键 name VARCHAR(64) NOT NULL, -- 档案名称 category VARCHAR(32), -- 分类人事/财务/项目等 status TINYINT DEFAULT 1, -- 状态1在库 0借出 2销毁 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, -- 建档时间 remark VARCHAR(255) -- 备注 ); -- 借阅记录表一对多关系一个档案多条借阅记录 CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, archive_id INT NOT NULL, -- 外键关联 archive.id borrower VARCHAR(32) NOT NULL, -- 借阅人 borrow_time DATETIME DEFAULT CURRENT_TIMESTAMP, return_time DATETIME, -- 归还时间为空表示未还 FOREIGN KEY (archive_id) REFERENCES archive(id) );字段设计上有几个点值得注意。archive_no加了 UNIQUE 约束因为档案编号在业务上不能重复让数据库兜底比在代码里查重更可靠。status用 TINYINT 而不是 VARCHAR省空间且查询快但要在代码里维护一个枚举映射不然界面上显示 1、0、2 用户看不懂。borrow_record单独建表而不是在 archive 里加字段是因为借阅历史可能有多条塞进主表就违反了第一范式。2.3 服务端 Socket 监听与请求分发服务端是整个系统的心脏。它要做三件事监听端口、接收客户端请求、根据请求类型调用对应的 DAO 方法并返回结果。下面是一个精简但可运行的服务端主循环。public class ArchiveServer { private static final int PORT 9527; // 监听端口避开常用端口 public static void main(String[] args) throws IOException { ServerSocket serverSocket new ServerSocket(PORT); System.out.println(档案服务端已启动端口 PORT); // 线程池处理多客户端避免每个连接新建线程 ExecutorService pool Executors.newFixedThreadPool(10); while (true) { Socket socket serverSocket.accept(); // 阻塞等待连接 pool.execute(new ClientHandler(socket)); // 交给处理器 } } } class ClientHandler implements Runnable { private Socket socket; public ClientHandler(Socket socket) { this.socket socket; } Override public void run() { try (ObjectInputStream in new ObjectInputStream(socket.getInputStream()); ObjectOutputStream out new ObjectOutputStream(socket.getOutputStream())) { while (true) { Object req in.readObject(); // 读取客户端请求对象 if (req null) break; Object resp RequestDispatcher.dispatch(req); // 分发处理 out.writeObject(resp); out.flush(); } } catch (Exception e) { System.out.println(客户端断开 e.getMessage()); } } }这段代码的关键点在于用ExecutorService而不是每个连接new Thread()防止客户端多了之后线程爆炸用ObjectInputStream/ObjectOutputStream直接传 Java 对象省去手动拼 JSON 的麻烦但要求客户端和服务端的实体类包名、字段完全一致否则会抛ClassNotFoundException或序列化 ID 不匹配。RequestDispatcher是一个简单工厂根据请求对象里的action字段比如 ADD_ARCHIVE、QUERY_BY_NO路由到对应的 DAO 方法。这个分发器我一般写成 switch-case请求类型不超过十种的话没必要上反射。3. 客户端与数据库从界面到 SQL 的完整链路3.1 Swing 界面布局与事件绑定客户端用 Swing 写虽然丑但胜在 JDK 自带、不用配环境。主界面分三块顶部查询栏、中间表格、底部按钮组。表格用JTable配合DefaultTableModel查询结果直接刷 model 就能更新界面。public class MainFrame extends JFrame { private JTable table; private DefaultTableModel tableModel; private JTextField searchField; public MainFrame() { setTitle(小型档案管理系统); setSize(800, 500); setDefaultCloseOperation(EXIT_ON_CLOSE); setLayout(new BorderLayout()); // 顶部查询面板 JPanel topPanel new JPanel(); searchField new JTextField(20); JButton searchBtn new JButton(查询); searchBtn.addActionListener(e - doSearch()); // 绑定查询事件 topPanel.add(new JLabel(档案编号)); topPanel.add(searchField); topPanel.add(searchBtn); add(topPanel, BorderLayout.NORTH); // 中间表格 String[] columns {ID, 档案编号, 名称, 分类, 状态, 建档时间}; tableModel new DefaultTableModel(columns, 0); table new JTable(tableModel); add(new JScrollPane(table), BorderLayout.CENTER); // 底部按钮 JPanel bottomPanel new JPanel(); JButton addBtn new JButton(新增); addBtn.addActionListener(e - showAddDialog()); bottomPanel.add(addBtn); add(bottomPanel, BorderLayout.SOUTH); } private void doSearch() { String no searchField.getText().trim(); // 构造请求对象通过 Socket 发给服务端 ArchiveRequest req new ArchiveRequest(QUERY_BY_NO, no); ArchiveResponse resp SocketClient.send(req); tableModel.setRowCount(0); // 清空旧数据 for (Archive a : resp.getArchives()) { tableModel.addRow(new Object[]{a.getId(), a.getArchiveNo(), a.getName(), a.getCategory(), a.getStatus(), a.getCreateTime()}); } } }事件绑定用的是 Lambda 表达式比匿名内部类简洁。doSearch里先清空 tableModel 再逐行添加这是刷新表格的标准做法直接setModel新对象也行但会丢失列宽设置。SocketClient.send是一个静态工具方法内部维护一个长连接把请求对象写出去、读回响应对象。注意 Swing 的事件处理是在 EDT 线程里跑的如果网络请求耗时较长界面会卡住正规做法是SwingWorker异步处理但实验设计里数据量小同步调用也能接受。3.2 JDBC 工具类与 DAO 层实现服务端这边数据库操作统一走 DAO 层。先写一个 JDBC 工具类负责拿连接和关资源避免每个方法都写一遍 try-catch-finally。public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/archive_db?useSSLfalseserverTimezoneUTC; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); // 加载驱动 } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL驱动未找到, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 依次关闭每个都判空顺序不能反 try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (stmt ! null) stmt.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} } }连接串里的serverTimezoneUTC是 MySQL 8 的坑不加会报时区错误。useSSLfalse是关掉 SSL 警告本地实验环境没必要开。驱动类名是com.mysql.cj.jdbc.Driver旧版是com.mysql.jdbc.Driver用错会报找不到驱动。DAO 层以查询为例public class ArchiveDao { public ListArchive findByNo(String archiveNo) { ListArchive list new ArrayList(); String sql SELECT * FROM archive WHERE archive_no LIKE ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % archiveNo %); // 模糊匹配 try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Archive a new Archive(); a.setId(rs.getInt(id)); a.setArchiveNo(rs.getString(archive_no)); a.setName(rs.getString(name)); a.setCategory(rs.getString(category)); a.setStatus(rs.getInt(status)); a.setCreateTime(rs.getTimestamp(create_time)); list.add(a); } } } catch (SQLException e) { e.printStackTrace(); } return list; } }用PreparedStatement而不是Statement拼接字符串一是防 SQL 注入二是预编译效率高。LIKE查询的参数要自己加%不能写在 SQL 里否则占位符会失效。try-with-resources自动关闭 Connection 和 PreparedStatement但 ResultSet 也实现了 AutoCloseable嵌套一层 try 更稳妥。3.3 请求响应对象的序列化约定C/S 之间传对象必须实现Serializable接口并且显式声明serialVersionUID。不声明的话JVM 会根据类结构自动生成客户端和服务端只要编译环境有一点差异这个 ID 就不一样反序列化直接抛InvalidClassException。public class ArchiveRequest implements Serializable { private static final long serialVersionUID 1L; // 固定值两端必须一致 private String action; // 操作类型QUERY_BY_NO / ADD / DELETE / UPDATE private Object payload; // 携带的参数可以是 String、Archive 等 public ArchiveRequest(String action, Object payload) { this.action action; this.payload payload; } // getter/setter 省略 }action字段用字符串而不是枚举是为了两端解耦新增操作类型时不用同步改枚举类。payload声明为 Object实际用时再强转灵活但要注意类型安全转错了会抛ClassCastException。我一般会在服务端分发前先校验 action 是否在已知列表里不认识的直接返回错误响应避免走到强转那一步才崩。4. 避坑排查从端口占用到中文乱码的五个血泪经验4.1 端口被占用导致服务端启动失败现象运行服务端 main 方法控制台立刻抛java.net.BindException: Address already in use。原因9527 端口被其他程序占了可能是上次没关干净的服务端进程也可能是别的软件碰巧用了这个端口。解决Windows 下netstat -ano | findstr 9527找到 PID任务管理器结束进程或者直接把 PORT 常量改成 9528、9529。更省事的办法是在new ServerSocket(PORT)之前加一句serverSocket.setReuseAddress(true)允许端口复用但治标不治本还是建议先查占用。4.2 客户端反序列化报 InvalidClassException现象客户端发请求后读响应时抛java.io.InvalidClassException: ArchiveResponse; local class incompatible: stream classdesc serialVersionUID ...。原因客户端和服务端的实体类 serialVersionUID 不一致。常见于两端分开编译或者一端改了类结构没重新编译另一端。解决所有需要序列化的类都显式写private static final long serialVersionUID 1L;两端保持一致。改完类结构后两端都要重新编译打包不能只更新一边。4.3 中文写入数据库变成问号现象界面上输入中文档案名称存进数据库后查出来是???。原因数据库连接串没指定字符集或者数据库、表的字符集不是 utf8mb4。解决连接串加上characterEncodingutf8建库建表时指定DEFAULT CHARSETutf8mb4。如果已经建好了表用ALTER TABLE archive CONVERT TO CHARACTER SET utf8mb4;改。另外检查 MySQL 配置文件里的character-set-server是不是 utf8mb4这个改完要重启 MySQL 服务。4.4 Swing 界面在事件处理方法里卡死现象点击查询按钮后界面无响应按钮一直处于按下状态等几秒才恢复。原因网络请求在 EDT 线程里同步执行服务端处理慢或者网络延迟时整个界面事件循环被阻塞。解决把网络调用放到SwingWorker的doInBackground里done方法里再更新表格。实验数据量小的话也可以在请求前把按钮setEnabled(false)请求完再setEnabled(true)至少让用户知道系统在干活。4.5 服务端线程池满了之后新连接被拒绝现象多个客户端同时连接超过十个之后新客户端连不上服务端也不报错。原因Executors.newFixedThreadPool(10)线程池满了任务在队列里排队但队列是无界的不会拒绝只是新连接一直等不到处理。解决把线程池换成new ThreadPoolExecutor并设置有界队列和拒绝策略或者简单点把核心线程数调大。实验环境一般不会超过十个客户端但知道这个边界在哪报告里写一句「线程池容量为 10超出后排队」也是加分项。5. 进阶技巧用 Socket 长连接做心跳检测与断线重连基础版本跑通之后如果想让系统看起来更「工程化」可以加两个东西心跳检测和断线重连。这两个机制在实验报告里写出来比单纯增删改查有说服力得多。心跳检测的思路是客户端每隔 30 秒往服务端发一个HEARTBEAT请求服务端收到后回一个HEARTBEAT_ACK。如果客户端连续三次没收到 ACK就认为连接断了触发重连。服务端这边则维护一个最后活跃时间超过 90 秒没收到任何请求的连接主动关闭释放资源。// 客户端心跳线程 ScheduledExecutorService heartbeat Executors.newSingleThreadScheduledExecutor(); heartbeat.scheduleAtFixedRate(() - { try { ArchiveRequest req new ArchiveRequest(HEARTBEAT, null); ArchiveResponse resp SocketClient.send(req); if (resp null || !HEARTBEAT_ACK.equals(resp.getAction())) { failCount.incrementAndGet(); // 失败计数 } else { failCount.set(0); // 重置 } if (failCount.get() 3) { SocketClient.reconnect(); // 触发重连 failCount.set(0); } } catch (Exception e) { failCount.incrementAndGet(); } }, 0, 30, TimeUnit.SECONDS);scheduleAtFixedRate的第三个参数是间隔时间单位秒。failCount用AtomicInteger保证线程安全。reconnect方法里先关旧 socket再按配置的地址和端口重新new Socket()重连成功后要把当前界面上的数据重新拉一遍不然用户看到的是断线前的旧数据。服务端这边在ClientHandler的循环里加一个lastActiveTime记录每次收到请求就更新。另起一个定时任务扫描所有 handler超时的调用socket.close()。这里要注意并发修改问题handler 列表用ConcurrentHashMap存遍历时用迭代器安全删除。还有一个实用技巧是给 Socket 设置超时socket.setSoTimeout(5000)读操作超过 5 秒没数据就抛SocketTimeoutException避免线程永久阻塞。这个值不能设太小网络抖动会误判也不能太大断线了要等很久才发现。我一般设 5 到 10 秒配合心跳间隔使用。从那以后我每次做 C/S 结构的实验都会先把心跳和超时加上再写业务逻辑因为断线问题一旦在演示时出现现场排查根本来不及。希望帮到你。本文还有配套的精品资源点击获取