☰
Java实验设计实战:基于JDBC与Socket的小型档案管理系统
2026/10/16 1:51:38 网站建设 项目流程

简介:这是一份面向高校计算机专业学生的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。从那以后我每次交课程设计都强制走一遍“清库 → 建表 → 启动 → 全流程操作 → 检查日志”,这个习惯帮我省了至少三次答辩现场翻车。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询