简介:这是一份面向高校计算机专业学生的Java综合实验设计资源,围绕C/S架构的小型档案管理系统展开,适合正在学习Java面向对象、Socket通信与多线程编程的读者用于课程设计或实验参考。压缩包共47个文件,约1.37MB,包含14个java源文件、15个class编译文件、2个sql建库脚本、1个MySQL驱动jar包以及readme说明和实验指导文档,覆盖客户端与服务器端两套工程结构。系统要求用户登录验证并按角色划分权限,支持档案录入、条件查询、文件上传下载与删除,用户信息与档案属性存入关系数据库,档案文件保存在服务器目录,通信基于Socket并由服务器端多线程响应多客户端请求。目前已有875人学习下载,读者可据此理解C/S分层设计、数据库连接与多线程并发处理的完整实现思路。
1. 从一次“实验课翻车”说起:为什么档案管理系统值得认真做
很多同学拿到“Java实验设计”这个题目时,第一反应是去网上找一个现成的增删改查项目改改交差。我带过几届课程设计,见过太多这样的作品:界面能跑,数据库能连,但一问“档案的借阅状态怎么流转”“并发修改同一条档案怎么办”“Socket 通信断线了怎么恢复”,就答不上来。小型档案管理系统看起来简单,实际上它同时踩中了 Java 实验设计里最核心的几个考点:JDBC 数据库操作、Swing 或控制台界面、Socket 网络通信、多线程与文件存储。把这几个点串成一个完整闭环,比堆十个零散的小实验更能体现工程能力。
这套方案面向两类人:一是需要交 Java 课程设计或实验报告的学生,二是想用一个真实场景练手 JDBC + Socket 的初学者。我会按“需求 → 设计 → 实现 → 验收”的顺序,把一个小型档案管理系统从零拆到能答辩的程度,包括表结构、模块划分、关键代码和常见翻车点。
2. 需求分析与技术选型:先想清楚档案到底怎么管
2.1 功能需求拆解
档案管理系统的核心不是“增删改查”四个字,而是档案的生命周期。我一般会把需求分成三层:
第一层是基础数据管理。档案本身包含编号、名称、类别、密级、存放位置、创建时间、状态等字段。用户需要能新增档案、修改档案信息、按条件查询、逻辑删除(不是物理删除,档案管理里物理删除是禁忌)。
第二层是借阅流转。档案可以被借出和归还,借出时要记录借阅人、借出时间、预计归还时间,归还时更新状态。这里涉及状态机:在库 → 借出 → 逾期 → 归还。状态流转必须用事务保证,否则会出现“档案显示已借出但借阅记录不存在”的脏数据。
第三层是操作审计。谁在什么时候对哪条档案做了什么操作,必须留痕。这一层很多同学会忽略,但恰恰是答辩时老师最爱问的点。
2.2 技术选型与理由
| 技术点 | 选型 | 理由 |
|---|---|---|
| 开发语言 | Java 8 或 11 | 课程环境通用,语法稳定 |
| 数据库 | MySQL 5.7 / 8.0 | 免费、资料多、JDBC 驱动成熟 |
| 数据访问 | 原生 JDBC + PreparedStatement | 实验设计通常要求手写 SQL,不用 ORM 框架 |
| 界面 | Swing(桌面端)或控制台菜单 | Swing 能体现事件驱动,控制台更聚焦逻辑 |
| 网络通信 | Socket + 多线程 | 实现客户端/服务端分离,满足“网络编程”实验要求 |
| 构建工具 | 纯 javac 或 Maven | 看课程要求,Maven 管理依赖更方便 |
选原生 JDBC 而不是 MyBatis,是因为实验设计的评分点往往在“你是否理解连接、语句、结果集的处理流程”。用框架反而掩盖了这些细节。Socket 部分我建议做成 C/S 结构:服务端持有数据库连接,客户端通过 Socket 发送请求指令,服务端处理后再返回结果。这样既练了网络编程,又避免了每个客户端直连数据库的安全问题。
提示:如果课程只要求单机版,可以把 Socket 层做成可选模块,但建议保留,答辩时是加分项。
2.3 数据库表设计
-- 档案主表 CREATE TABLE archive ( id INT PRIMARY KEY AUTO_INCREMENT, archive_no VARCHAR(32) NOT NULL UNIQUE COMMENT '档案编号', name VARCHAR(128) NOT NULL COMMENT '档案名称', category VARCHAR(32) COMMENT '类别', security_level TINYINT DEFAULT 1 COMMENT '密级 1-公开 2-内部 3-秘密', location VARCHAR(64) COMMENT '存放位置', status TINYINT DEFAULT 0 COMMENT '0-在库 1-借出 2-逾期', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, is_deleted TINYINT DEFAULT 0 COMMENT '逻辑删除标记' ); -- 借阅记录表 CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, archive_id INT NOT NULL, borrower VARCHAR(64) NOT NULL, borrow_time DATETIME NOT NULL, expect_return_time DATETIME, actual_return_time DATETIME, status TINYINT DEFAULT 0 COMMENT '0-借出中 1-已归还', FOREIGN KEY (archive_id) REFERENCES archive(id) ); -- 操作日志表 CREATE TABLE operation_log ( id INT PRIMARY KEY AUTO_INCREMENT, operator VARCHAR(64), action VARCHAR(32), target_id INT, detail VARCHAR(255), op_time DATETIME DEFAULT CURRENT_TIMESTAMP );这三张表是整个系统的骨架。archive 表用 is_deleted 做逻辑删除,borrow_record 表通过 archive_id 外键关联,operation_log 表记录所有写操作。注意 status 字段的取值要和借阅记录联动,这个联动逻辑必须放在 Service 层用事务包起来。
3. 总体架构与模块划分:三层结构怎么落地
3.1 分层设计
系统按经典三层架构组织:
- 表现层(View):Swing 窗口或控制台菜单,负责接收用户输入、展示结果。不写任何 SQL。
- 业务层(Service):处理借阅规则、状态流转、权限校验。所有事务控制在这一层。
- 数据层(DAO):封装 JDBC 操作,每个方法对应一条或一组 SQL。只做数据存取,不做业务判断。
包结构建议这样组织:
com.archive ├── view // 界面类 ├── service // 业务逻辑 ├── dao // 数据访问 ├── model // 实体类 ├── net // Socket 通信 └── util // 数据库连接池、工具类3.2 数据库连接管理
不要每次操作都 DriverManager.getConnection,那样性能差且容易泄漏。写一个简单的连接工具类:
public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/archive_db?useSSL=false&serverTimezone=UTC"; private static final String USER = "root"; private static final String PASSWORD = "your_password"; 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) {} } }这段代码的关键点是静态块加载驱动、统一关闭方法。参数说明:URL 里的 serverTimezone 必须加,否则 MySQL 8 会报时区错误;useSSL=false 在本地实验环境可以关掉。
3.3 借阅业务的事务控制
借阅操作要同时做两件事:更新档案状态为“借出”,插入一条借阅记录。这两步必须在一个事务里:
public boolean borrowArchive(int archiveId, String borrower) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 检查档案是否在库 Archive archive = archiveDao.findById(conn, archiveId); if (archive == null || archive.getStatus() != 0) { conn.rollback(); return false; } // 2. 更新档案状态 archiveDao.updateStatus(conn, archiveId, 1); // 3. 插入借阅记录 borrowDao.insert(conn, archiveId, borrower); conn.commit(); return true; } catch (SQLException e) { try { if (conn != null) conn.rollback(); } catch (SQLException ignored) {} e.printStackTrace(); return false; } finally { DBUtil.close(conn, null, null); } }注意 DAO 方法都接收 Connection 参数,这样才能共用同一个事务。如果每个 DAO 方法自己获取连接,事务就失效了。这是 JDBC 事务最常见的翻车点。
4. Socket 通信与多线程:让客户端和服务端说上话
4.1 通信协议设计
客户端和服务端之间用简单的文本协议,一行一条指令,字段用|分隔:
LOGIN|admin|123456 QUERY|archive_no=A001 BORROW|archive_id=5|borrower=张三 RETURN|archive_id=5服务端解析指令后调用对应的 Service 方法,返回OK|结果或FAIL|原因。这种设计简单直观,调试时用 telnet 就能测。
4.2 服务端多线程处理
public class ArchiveServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(8888); System.out.println("服务端启动,监听8888端口"); 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 (BufferedReader in = new BufferedReader( new InputStreamReader(socket.getInputStream())); PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) { String line; while ((line = in.readLine()) != null) { String response = CommandParser.parse(line); out.println(response); } } catch (IOException e) { System.out.println("客户端断开:" + e.getMessage()); } } }线程池用 newFixedThreadPool(10),避免每个连接都新建线程导致资源耗尽。ClientHandler 里用 try-with-resources 自动关闭流。CommandParser 是解析指令的类,根据指令前缀分发到不同的 Service 方法。
4.3 客户端连接与超时处理
public class ArchiveClient { private Socket socket; private PrintWriter out; private BufferedReader in; public boolean connect(String host, int port) { try { socket = new Socket(); socket.connect(new InetSocketAddress(host, port), 3000); // 3秒超时 out = new PrintWriter(socket.getOutputStream(), true); in = new BufferedReader(new InputStreamReader(socket.getInputStream())); return true; } catch (IOException e) { System.out.println("连接服务端失败:" + e.getMessage()); return false; } } public String sendCommand(String cmd) throws IOException { out.println(cmd); return in.readLine(); // 阻塞等待响应 } }连接超时设 3 秒,避免网络不通时界面卡死。sendCommand 是同步阻塞的,简单场景够用。如果要做异步,可以把读取响应的部分放到单独线程里,通过回调更新界面。
5. 避坑与常见问题排查
5.1 中文乱码
现象:档案名称存入数据库变成问号,或 Socket 传输后中文显示乱码。
原因:数据库字符集不是 utf8mb4,或 Socket 流没有指定编码。
解决:建库时用CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;Socket 流用new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)明确指定编码。
5.2 事务不生效
现象:借阅操作中更新状态成功但插入记录失败,数据不一致。
原因:DAO 方法各自获取 Connection,不在同一个事务里。
解决:Service 层获取 Connection 后传给 DAO,所有操作共用这个连接,最后统一 commit 或 rollback。
5.3 资源泄漏
现象:程序运行一段时间后报“Too many connections”。
原因:Connection、Statement、ResultSet 没有在 finally 里关闭。
解决:用 try-with-resources 或在 finally 中调用 DBUtil.close()。养成“谁打开谁关闭”的习惯。
5.4 Socket 断线后客户端卡死
现象:服务端重启后,客户端发送指令一直无响应。
原因:Socket 连接已断开,但客户端仍在等待 readLine() 返回。
解决:设置 socket.setSoTimeout(5000),超时后抛出 SocketTimeoutException,捕获后提示用户重连。
5.5 并发修改同一条档案
现象:两个客户端同时借阅同一档案,都显示成功。
原因:检查状态和更新状态之间有间隙,没有加锁。
解决:在 SQL 层面用UPDATE archive SET status=1 WHERE id=? AND status=0,根据返回的受影响行数判断是否成功。这是乐观锁的思路,比在 Java 里加 synchronized 更可靠。
6. 验收与答辩:怎么证明你的系统真的能跑
验收不是演示一遍增删改查就完事。我一般会按下面这个清单逐项过:
| 验收项 | 操作 | 预期结果 |
|---|---|---|
| 数据库连接 | 启动服务端 | 控制台输出“服务端启动”,无异常 |
| 新增档案 | 客户端输入完整信息 | 数据库 archive 表新增一条,is_deleted=0 |
| 重复编号 | 用相同 archive_no 新增 | 返回失败,提示编号已存在 |
| 借阅档案 | 对在库档案执行借阅 | archive.status 变为 1,borrow_record 新增记录 |
| 重复借阅 | 对已借出档案再次借阅 | 返回失败,提示档案不在库 |
| 归还档案 | 对借出档案执行归还 | archive.status 变回 0,borrow_record 更新归还时间 |
| 逻辑删除 | 删除一条档案 | is_deleted=1,查询列表不再显示 |
| 操作日志 | 执行任意写操作 | operation_log 表有对应记录 |
| 断线重连 | 关闭服务端再启动 | 客户端提示超时,重连后恢复正常 |
| 并发借阅 | 两个客户端同时借同一档案 | 只有一个成功,另一个提示失败 |
答辩时老师最爱问的三个问题,提前准备好答案:第一,“你的事务是怎么控制的?”——答 Service 层统一管理 Connection,DAO 共用。第二,“Socket 通信为什么用多线程?”——答避免单线程阻塞导致其他客户端无法连接。第三,“档案删除为什么不做物理删除?”——答档案管理需要留痕和可追溯,逻辑删除保留数据完整性。
最后说一个我自己的习惯:每次提交前,我都会把数据库清空,从建表脚本开始重新跑一遍完整流程。因为开发过程中手动改过数据,很容易掩盖初始化脚本的 bug。从那以后我每次交课程设计都强制走一遍“清库 → 建表 → 启动 → 全流程操作 → 检查日志”,这个习惯帮我省了至少三次答辩现场翻车。希望帮到你。
本文还有配套的精品资源,点击获取