简介:这份资源面向学习Java网络编程与数据库应用的学生,提供一套可直接运行的C/S架构小型档案管理系统实验设计源码,帮助理解Socket通信、多线程与关系数据库的综合运用。系统分为客户端与服务器端,支持用户登录验证、按角色区分权限,普通用户可查询修改个人信息,管理员可增删用户并调整权限;档案录入人员能上传文件,浏览人员可按条件检索并下载档案,用户信息与档案属性存入数据库,文件实体保存在服务器目录。压缩包共47个文件,以14个java源文件与15个class编译文件为核心,辅以sql建库脚本、txt说明文档、jar数据库驱动及实验指导书,整体约1.37MB,目录按client与server分模块组织。已有876人学习,适合作为课程实验参考与Socket多线程编程的练手项目。
1. 从一份“能跑起来的实验设计”说起:小型档案管理系统到底解决什么问题
很多计算机专业的学生在做 Java 课程实验时,都会遇到一个尴尬局面:需求文档写得像模像样,代码却只能跑个控制台增删改查,交上去自己都不好意思。这份“Java 实验设计-实现一个小型档案管理系统”的源码包,恰好卡在一个很实用的位置上——它不是玩具级的System.out.println演示,而是一个带 SQL 持久化、带 Socket 通信、能真正把“档案录入、查询、修改、借阅归还”跑通的完整工程。适合谁?正在做 Java 课程设计、需要一份结构清晰可二次开发的参考实现、或者想搞明白“桌面端 + 服务端 + 数据库”三层怎么用原生 Java 拼起来的开发者。关键词 java 源码、实验设计、sql、socket 在这份资源里不是标签,是实打实的技术骨架。
2. 技术选型与整体架构:原生 Java 三层结构怎么搭
2.1 为什么是 Swing + Socket + JDBC 这套组合
拿到这份源码,第一眼会注意到它没有用 Spring Boot,也没有用 MyBatis,而是清一色的原生 Java 技术栈。这不是偷懒,而是实验设计场景下的合理选择。课程实验的核心考察点是“你能不能把 Java 基础、网络编程、数据库操作串起来”,而不是“你会不会配框架”。用 Swing 做客户端界面,好处是零额外依赖,JDK 自带,编译完直接双击 jar 就能看到窗口;用 Socket 做客户端与服务端通信,能把“序列化、协议设计、连接管理”这些底层概念暴露出来,而不是被 HTTP 框架封装掉;用 JDBC 直连数据库,SQL 语句全部手写,方便老师检查你对增删改查和事务的理解。
常见做法是把这三层拆成三个独立的包:client负责界面和用户交互,server负责业务逻辑和数据库访问,common放两边共用的实体类和通信协议。我一般会建议读者先看common包,因为它是理解整个系统数据流的钥匙。实体类比如Archive(档案)、User(用户)、BorrowRecord(借阅记录)定义了数据长什么样;通信协议类比如Request和Response定义了客户端发什么、服务端回什么。把这两个看明白,后面读客户端和服务端代码就不会迷路。
2.2 目录结构逐层拆解
一份结构清晰的源码,目录本身就是文档。这份工程的典型布局如下:
archive-system/ ├── src/ │ ├── common/ # 共用实体与协议 │ │ ├── Archive.java │ │ ├── User.java │ │ ├── BorrowRecord.java │ │ ├── Request.java │ │ └── Response.java │ ├── server/ # 服务端 │ │ ├── ServerMain.java │ │ ├── ClientHandler.java │ │ ├── dao/ │ │ │ ├── ArchiveDao.java │ │ │ ├── UserDao.java │ │ │ └── BorrowDao.java │ │ └── util/ │ │ └── DBUtil.java │ └── client/ # 客户端 │ ├── ClientMain.java │ ├── ui/ │ │ ├── LoginFrame.java │ │ ├── MainFrame.java │ │ └── ArchivePanel.java │ └── net/ │ └── ServerConnection.java ├── sql/ │ └── init.sql # 建库建表脚本 └── lib/ └── mysql-connector-java.jar这个布局的价值在于职责边界清楚:dao包只碰数据库,ClientHandler只处理网络请求分发,ui包只负责界面渲染。二次开发时想换数据库,只动dao和DBUtil;想换通信协议,只动common里的Request/Response和两端的序列化逻辑。新手最容易犯的错是把 SQL 语句直接写在 Swing 按钮的监听器里,那样改一个字段要翻三个文件,这份源码没有踩这个坑。
2.3 一次请求的完整链路
理解架构最有效的方式是跟踪一条请求。以“按档案编号查询”为例:用户在ArchivePanel的搜索框输入编号,点击查询按钮,客户端把操作类型和参数封装成Request对象,通过ObjectOutputStream写到 Socket 输出流;服务端ClientHandler在while循环里用ObjectInputStream读到这个对象,根据request.getAction()的值分发到ArchiveDao.queryById();DAO 用 JDBC 执行SELECT,把结果集映射成Archive对象,再包进Response写回;客户端读到Response,取出数据刷新表格模型。整条链路没有魔法,全是原生 API,这正是它适合教学和二次开发的原因——每一行你都能看懂,也能改。
3. 核心模块代码拆解:从数据库连接到 Socket 通信
3.1 数据库连接与建表脚本
先看持久化层的地基。DBUtil通常是一个单例工具类,负责加载驱动、获取连接、关闭资源。写法上有个细节值得注意:连接参数不要硬编码在 Java 里,而是读配置文件或者至少抽成常量,方便换环境。
public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/archive_db?useSSL=false&characterEncoding=utf8"; 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("数据库驱动加载失败", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 按 ResultSet -> Statement -> Connection 顺序关闭,每个都判空 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里的characterEncoding=utf8不能省,否则中文档案名存进去变问号,这是血泪经验。static块保证驱动只加载一次。close方法按结果集、语句、连接的顺序关闭,顺序反了在某些驱动下会抛异常。建表脚本init.sql一般包含三张核心表:user存登录账号和角色,archive存档案主信息(编号、名称、类别、存放位置、状态),borrow_record存借阅流水(档案 ID、借阅人、借出时间、归还时间)。外键关系上,borrow_record.archive_id引用archive.id,这样查询借阅记录时能关联出档案名称。
3.2 DAO 层的增删改查实现
DAO 层是 SQL 出现最密集的地方。以档案查询为例,常见做法是用PreparedStatement防注入,用ResultSet手动映射实体:
public List<Archive> queryByKeyword(String keyword) throws SQLException { String sql = "SELECT * FROM archive WHERE name LIKE ? OR code LIKE ?"; List<Archive> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); ps.setString(2, "%" + keyword + "%"); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Archive a = new Archive(); a.setId(rs.getInt("id")); a.setCode(rs.getString("code")); a.setName(rs.getString("name")); a.setCategory(rs.getString("category")); a.setStatus(rs.getInt("status")); // 0 在库 1 借出 list.add(a); } } } return list; }这里用 try-with-resources 自动关闭连接和语句,比手动finally干净。LIKE查询两处占位符都要设值,漏一个就会抛参数越界。status用整型而不是字符串存,查询和更新都更快,界面显示时再转成“在库/借出”文案。借阅操作涉及两张表,必须用事务包起来:先UPDATE archive SET status=1,再INSERT INTO borrow_record,两步都成功才commit,任何一步失败就rollback。这是实验设计里少数能体现“事务”考点的地方,别写成两条独立 SQL。
3.3 Socket 服务端的请求分发
服务端主循环是理解并发模型的关键。ServerMain起一个ServerSocket监听端口,每来一个连接就丢给一个线程处理:
public class ServerMain { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(8888); System.out.println("服务端已启动,监听 8888"); while (true) { Socket socket = serverSocket.accept(); new Thread(new ClientHandler(socket)).start(); } } }accept()是阻塞的,所以主线程一直挂在这里等连接。每个连接开一个线程,简单直接,实验规模下几十个并发完全够用。ClientHandler里维护ObjectInputStream和ObjectOutputStream,循环读取Request,用switch分发到不同 DAO 方法,把结果塞进Response写回。注意ObjectOutputStream在服务端要先flush()再构造ObjectInputStream,否则两端互相等待对方先发头信息,会直接卡死——这个坑几乎每个写 Socket 序列化的人都踩过。
3.4 客户端界面与通信封装
客户端把网络通信收拢到ServerConnection一个类里,界面层只调方法不碰流:
public class ServerConnection { private Socket socket; private ObjectOutputStream out; private ObjectInputStream in; public void connect(String host, int port) throws IOException { socket = new Socket(host, port); out = new ObjectOutputStream(socket.getOutputStream()); out.flush(); // 必须先 flush,再建输入流 in = new ObjectInputStream(socket.getInputStream()); } public Response send(Request request) throws Exception { out.writeObject(request); out.flush(); return (Response) in.readObject(); } }send方法是同步的:发一个请求,阻塞等一个响应。界面上的按钮监听器里调用send,拿到Response后判断success字段,成功就刷新表格,失败就弹提示框。这种同步模型在实验场景下足够,但如果要做“服务端主动推送”,就得改成异步加监听器,那是二次开发可以发挥的地方。Request里通常有action(字符串,如"QUERY_ARCHIVE")、params(Map<String,Object>或直接放实体),Response有success、message、data三个字段,结构简单但够用。
4. 跑起来与避坑排查:环境、配置和那些卡住新手的点
4.1 从零启动的完整步骤
先把环境对齐:JDK 8 或以上,MySQL 5.7/8.0,一个能跑 Java 的 IDE。步骤按顺序来:第一步,在 MySQL 里执行sql/init.sql建库建表,确认archive_db存在;第二步,把lib下的 JDBC 驱动 jar 加到项目构建路径,IDEA 里是 Project Structure 的 Libraries,Eclipse 里是 Build Path;第三步,改DBUtil里的URL、USER、PASSWORD为你本机实际值;第四步,先运行ServerMain,看到“服务端已启动”再运行ClientMain;第五步,用init.sql里预置的账号登录,通常是admin/123456这类。顺序不能反,客户端先起会直接连不上抛异常。
4.2 常见问题排查清单
现象:启动客户端报Connection refused。原因:服务端没起,或者端口被占。解决:先确认ServerMain控制台有启动日志,再用netstat -ano | findstr 8888看端口是否被别的进程占用,占了就改ServerMain和ServerConnection两处的端口号,必须一致。
现象:登录时卡住不动,界面无响应。原因:ObjectOutputStream没flush就构造ObjectInputStream,两端死等。解决:检查服务端和客户端建流顺序,输出流构造后立刻flush(),再建输入流。这个顺序在两端都要对。
现象:中文档案名存进去变问号。原因:连接 URL 缺characterEncoding=utf8,或者数据库表字符集不是 utf8。解决:URL 补参数,建表时显式指定DEFAULT CHARSET=utf8mb4,已有表用ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4。
现象:借阅后档案状态没变,但借阅记录多了一条。原因:两条 SQL 没包在事务里,第二条失败第一条已提交。解决:把setAutoCommit(false)、两条 SQL、commit放进同一个 try,catch 里rollback。
现象:查询结果表格里全是null。原因:ResultSet映射时列名写错,比如数据库列叫archive_name代码里写name。解决:对照建表脚本逐列核对rs.getString("...")的字符串,大小写不敏感但拼写必须一致。
提示:改完
DBUtil的密码后,如果 IDE 有缓存,记得重新编译,否则跑的还是旧 class。
5. 二次开发改造思路:从实验作业到能用的工具
这份源码作为起点,最值得动手改的有三个方向。第一个是通信层升级:现在的同步send在查询大量数据时界面会假死,改造思路是把网络调用放到SwingWorker里,doInBackground发请求,done里回主线程刷新表格,界面就不会卡。第二个是权限控制:user表里加role字段,登录后根据角色决定MainFrame里哪些按钮可见,普通用户只能查和借,管理员才能删和改,这个改动集中在登录成功后的初始化逻辑,不碰 DAO。第三个是查询条件组合:现在只支持关键字模糊查,可以给ArchiveDao加一个接收多个可选条件的方法,用StringBuilder拼动态 SQL,注意每个条件仍用PreparedStatement占位符,别为了省事拼字符串。
验证改造是否成功,我习惯用一张对照表逐项过:
| 改造点 | 验证方法 | 通过标准 |
|---|---|---|
| 异步查询 | 连续点查询按钮 10 次 | 界面不卡,结果不串 |
| 角色权限 | 用普通账号登录 | 删除按钮不可见或点击被拒 |
| 组合查询 | 只填类别不填关键字 | 返回该类别全部档案 |
| 事务回滚 | 借阅时手动让第二条 SQL 失败 | 档案状态不变,无脏记录 |
最后说个我自己的习惯。早年做这类 Socket 实验,我最容易在“流顺序”和“端口一致”上翻车,调试半天发现是客户端连了 8888 服务端监听 9999。从那以后我每次改完网络相关代码,都强制先只跑服务端看日志,再跑客户端,两端端口号用同一个常量类管理,绝不各写各的。这份源码的结构已经帮你把该分的层分好了,剩下的就是在这个骨架上填你自己的业务。希望帮到你。
本文还有配套的精品资源,点击获取