简介:这是一份基于Java开发的图书管理系统完整源代码,适合Java初学者、课程设计学生及需要快速搭建管理类项目的开发者。系统覆盖图书录入、查询、借阅、归还、续借及状态跟踪等核心业务,可辅助理解MVC分层、数据库设计与CRUD实现。压缩包共212个文件,约4.01MB,其中以class编译文件、java源文件为核心,辅以jpg界面截图、jar依赖库、db数据库文件及工程配置文件,便于直接导入IDE运行或参考学习。已有1873人下载学习。通过源码可掌握Spring/MyBatis等后端框架的实际用法、用户登录与权限控制逻辑,以及前后端交互和异常处理思路。内容预览显示包含图书借还、读者管理、图书登记等多个功能模块界面,结构清晰,适合结合数据库脚本进行二次开发或毕业设计参考。
1. 为什么 Java 图书管理系统还是值得跑的:从课设到简历的实用底座
很多人的第一门编程语言是 Java,但第一次把 Java 完整「跑起来」往往靠一个图书管理系统——课设要交、毕设要过、面试要讲项目,它都是最容易被追问到底的业务闭环。这份 Java 图书管理系统源代码,解决的不是「管书」本身,而是你手里有没有一份结构清晰、能改、能演示的完整工程:Swing 桌面界面、JDBC 数据库操作、管理员与读者双角色、借还书全流程。适合三类人:正在赶 Java 课设的学生、想快速补 JDBC 和 SQL 基础的新手、拿它当脚手架做二次改造的开发者。它不是花架子,跑通它的过程,就是把 Java 面向对象、集合框架、数据库操作全部串起来的过程。
2. 工程拆解:包结构、五张数据表与双角色权限设计
2.1 包结构一看就会:视图、DAO、实体三块怎么分工
第一次拿到这套源码,别急着点运行,先把包结构过一遍。合格的工程在 src 下按职责拆成几个包,各自只干一件事:
src ├─ com.demo.view # Swing 界面:LoginFrame、MainFrame、BookManageFrame ├─ com.demo.dao # 数据访问:UserDao、BookDao、BorrowDao、ReaderDao ├─ com.demo.model # 实体类:User、Book、Reader、BorrowRecord ├─ com.demo.util # 工具类:DBUtil 连接管理、StringUtil └─ com.demo.main # 程序入口:main 方法启动登录界面view 负责画窗口、接收鼠标键盘事件;dao 负责拼 SQL、执行查询、把 ResultSet 转成实体对象;model 只是带 getter/setter 的纯数据类,不掺逻辑。一次登录操作的数据流是:LoginFrame 拿到用户名密码 → 调 UserDao.login() → UserDao 查库返回 User 对象 → LoginFrame 根据 User 的 role 字段决定打开哪个主界面。
这样拆最直接的好处是排查问题不用全局翻代码:界面报错去 view 里看,SQL 报错去 dao 里看。很多课设工程把 SQL 直接写在按钮的事件里,表面看省事,实际上等于把数据库细节焊死在界面上——将来想换数据库、想加一套 Web 界面,全都要重写。调试的时候它就是个黑匣子,报错只告诉你「第 38 行有问题」,但你根本不知道那一行到底想干什么。
这里顺带解释一个新手常问的点:为什么没有 service 层?桌面程序通常把业务逻辑合并到 dao 层完成,比如借书时先查库存再插入记录,这一串动作写在 BorrowDao 里。严格意义的项目会加 service 层,但课设和毕设场景下,dao 直接承载业务是通行做法,答辩时能说清楚「为什么这么取舍」比机械堆层更加分。
2.2 数据库设计:五张核心表与字段为什么这么建
这套系统跑在 MySQL 上,初始化脚本里建了五张表,是图书管理领域最经典的那套模型。核心建表语句长这样:
CREATE DATABASE IF NOT EXISTS library_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE library_db; -- 用户表:管理员与读者共用的登录账号 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role TINYINT NOT NULL DEFAULT 0 COMMENT '0=管理员 1=读者', status TINYINT NOT NULL DEFAULT 1 COMMENT '1=启用 0=停用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 读者表:读者的档案信息 CREATE TABLE t_reader ( id INT PRIMARY KEY AUTO_INCREMENT, reader_no VARCHAR(20) NOT NULL UNIQUE COMMENT '借书证号', reader_name VARCHAR(50) NOT NULL, phone VARCHAR(20), max_borrow INT DEFAULT 5 COMMENT '最大可借数量', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 图书表 CREATE TABLE t_book ( id INT PRIMARY KEY AUTO_INCREMENT, book_no VARCHAR(20) NOT NULL UNIQUE COMMENT '索书号', book_name VARCHAR(100) NOT NULL, author VARCHAR(50), publisher VARCHAR(80), category VARCHAR(30), total INT DEFAULT 1 COMMENT '总藏书量', stock INT DEFAULT 1 COMMENT '当前在馆库存', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 借阅记录表 CREATE TABLE t_borrow ( id INT PRIMARY KEY AUTO_INCREMENT, reader_id INT NOT NULL, book_id INT NOT NULL, borrow_time DATETIME DEFAULT CURRENT_TIMESTAMP, due_time DATETIME COMMENT '应还时间', return_time DATETIME COMMENT '实际归还时间', status TINYINT DEFAULT 0 COMMENT '0=借出 1=已还', fine DECIMAL(6,2) DEFAULT 0.00 COMMENT '逾期罚金', INDEX idx_reader (reader_id), INDEX idx_book (book_id) );几个关键的建模决定值得你留心。t_user 和 t_reader 分开建,因为登录账号(带密码)和读者档案(借书证号、电话、限额)是两类数据,混在一张表里会出现大量空字段,改其中一个字段还得连带处理另一类记录。t_book 里 total 和 stock 拆开是一整套系统的灵魂:total 是总藏书量,任何时候都不该变;stock 是当前可借数量,借书减一、还书加一。如果只用一个字段,还书时你根本不知道这本书原本有几本。fine 用 DECIMAL(6,2) 存金额而不是 float,浮点数算钱有精度问题,答辩时被问到「为什么不用 double」这就是标准答案。
外键在这里故意不建。课设场景最怕的是批量导数据、删读者时被外键约束卡住,所以常见的做法是靠业务代码维护引用完整性:删读者之前先查他有没有未还的书,有就拦截。这个取舍如果你能主动讲出来,面试官会认为你不是只会照抄,而是真的考虑过边界场景。
2.3 双角色权限:登录后走两个不同的主界面
role 字段决定一切。管理员登录后进 AdminMainFrame,能做:图书的增删改查、读者档案管理、借书还书操作、查看全部借阅记录、统计逾期罚金。读者登录后进 ReaderMainFrame,只能做:检索图书、查看自己的借阅列表、归还自己借的书。两个界面是独立的 JFrame,登录成功后由主程序按角色 new 不同的窗体现在讲是最清楚的落地方式。
这个模块有一个特别容易翻车的点:读者登录后执行借书操作,界面上传的是 book_id,但真正插入 t_borrow 需要 reader_id。reader_id 从哪里来?得从登录时查到的 username 去 t_reader 反查。有些简化版系统把 reader_id 直接写死在一个全局变量里,演示时一切正常,换个人登录就借不了书。正确做法是登录时把 User 对象整个挂在 Session 或一个上下文类里,后续所有操作都从里面取当前用户,而不是二次查询。
3. 环境搭建与启动:JDK、MySQL、连接驱动的一次性选型
3.1 版本组合:JDK 8 为什么是后悔药
跑这套源码之前,先把环境组合定死,否则后面每一步都是坑。我按兼容性从稳到险排了三个组合:
| 组合 | JDK | MySQL | 驱动 jar | 适用场景 |
|---|---|---|---|---|
| 保守型 | JDK 8 | MySQL 5.7 | mysql-connector-java-5.1.49.jar | 老机器、教程最全、零配置 |
| 常见型 | JDK 8 | MySQL 8.0 | mysql-connector-java-8.0.22.jar | 现在笔记本默认装的多数是 8.0 |
| 新机型 | JDK 11+ | MySQL 8.0 | mysql-connector-j-8.0.33.jar | 机器只能装新 JDK,需留意模块问题 |
为什么 JDK 8 是后悔药?因为 Swing 和 JDBC 在 8 上没有任何模块化限制,老教程里的驱动、代码片段全部能用。JDK 9 开始引入模块系统,Swing 程序在 9+ 上跑通常会要求加一长串 --add-exports 参数,新手根本不知道去哪里加。MySQL 8.0 的驱动则强依赖 serverTimezone 参数,缺了直接报时区错。如果你拿到的源码本来就是基于 JDK 8 写的,就不要为了追新去装 17、21,跑通比版本新重要得多。
驱动类名是另一个高频翻车点:5.x 驱动用com.mysql.jdbc.Driver,8.x 驱动用com.mysql.cj.jdbc.Driver。拿 8.x 的 jar 配老类名,控制台会打一行 warning 但还能连;拿 5.x 的 jar 配新类名,直接 ClassNotFoundException。所以版本组合一选定,jar 和代码里的类名必须成套换。
3.2 数据库初始化:SQL 脚本导入的两种姿势
拿到源码后第一件事是把数据库建出来。推荐直接在命令行客户端导入,最可控:
# 先以 root 登录 MySQL 客户端 mysql -u root -p # 进入客户端后执行脚本文件 source D:/code/library/library.sql; # 验证是否导入成功 USE library_db; SHOW TABLES;命令逻辑很简单:source 后面跟的是脚本文件的绝对路径。注意路径里的反斜杠在 MySQL 客户端里没问题,但在部分工具里要转义,如果你用的是图形化客户端,直接「运行 SQL 脚本文件」选同一份 .sql 即可。这里最容易被忽略的是编码:脚本文件如果以 GBK 保存,而你库是 utf8mb4,导入后中文全变问号。保守做法是用--default-character-set=utf8mb4参数指定:
mysql -u root -p --default-character-set=utf8mb4 library_db < D:/code/library/library.sql如果你能稳定跑通这一步,后面所有问题都不叫问题。脚本里若还带初始账号数据,导入后直接能用 admin 登录,省去手动插入的麻烦。
3.3 连接配置:DBUtil 里那几行参数不能省
数据库建好后,代码里的连接配置要对得上。这套源码的连接工具类通常是这个形态:
package com.demo.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { // 注意:主机、端口、库名按你本机实际环境改 private static final String URL = "jdbc:mysql://localhost:3306/library_db" + "?useUnicode=true&characterEncoding=utf8" + "&useSSL=false" + "&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { // 8.x 驱动用 com.mysql.cj.jdbc.Driver Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError("MySQL驱动加载失败,请检查lib目录下是否有jar包"); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }这段代码有四个参数值得说明。useUnicode=true&characterEncoding=utf8 是中文不乱码的前提,少了它,界面输入的中文存进库里就变成问号或乱码。useSSL=false 是为了消掉客户端那一大段 SSL 警告——本地开发根本不需要加密连接,开着反而拖慢启动。serverTimezone=Asia/Shanghai 是 MySQL 8.0 专用参数,5.7 可以不加,但统一加上没有副作用。username 和 password 必须是本机 MySQL 的真实账号,源码里默认的 root/123456 只是占位,直接跑大概率报 access denied。
我一般会把这段配置外置到 properties 文件里,这样换数据库环境不用改 Java 代码,但课设场景直接改常量也能接受,答辩时如果能提到「配置与代码分离」,会显得你有工程意识。
3.4 工程导入与启动:从压缩包到界面弹出的完整路径
环境就绪后按下面四步走,每步都验证完再进下一步:
- 解压源码包,确认目录内包含 src(源码)、lib(jar 包)、sql(数据库脚本)三块,缺了任何一块都会在后面的步骤卡住。
- IDEA 里选 File → Open,找到解压目录打开;Eclipse 则是 Import → Existing Projects into Workspace。注意不要用 New Project 再手动拷源码,那会丢掉工程配置。
- 把 lib 下的 mysql-connector jar 加入依赖:IDEA 里 File → Project Structure → Modules → Dependencies → 加号 → JARs or directories,选中 jar。这一步漏了就会出现 ClassNotFoundException。
- 运行 com.demo.main 包下的入口类,main 方法里 new LoginFrame()。启动后输入初始化脚本里的默认管理员账号,看到主窗体说明连接成功。
有一个常见误解需要纠正:把 sql 建库脚本放在工程的 sql 目录里不意味着自动执行,它只是给你一个静态脚本文件。真正生效必须手动导一次库,或者后续启动时由代码帮你连库,除此之外没有第三条路。
4. 核心模块实战:借书、还书、检索的完整代码路径
4.1 登录校验:预处理语句与账号状态检查
登录是第一个要讲清楚的模块。源码里的 UserDao.login 一般是这个样子:
public User login(String username, String password) { String sql = "SELECT id, username, role, status " + "FROM t_user WHERE username = ? AND password = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { // 账号被停用:返回 null,由界面提示“账号已停用” if (rs.getInt("status") == 0) { return null; } User u = new User(); u.setId(rs.getInt("id")); u.setUsername(rs.getString("username")); u.setRole(rs.getInt("role")); return u; } } } catch (SQLException e) { e.printStackTrace(); } return null; }这里的几个设计点你不能只抄不记。PreparedStatement 而不是 Statement 拼接字符串,是因为' or '1'='1这类输入能通过拼接直接绕过密码校验,这是最经典的 SQL 注入演示素材,答辩时被问到「安全性」就靠它撑场面。查询结果里先查 status 再组装对象,逻辑上是「先验证账号能不能用,再返回用户数据」,如果写成先组装对象再判断,界面层就得额外做一次判空,反而绕。
返回值用 null 表示失败,这是一种约定俗成的写法,代价是调用方要判空:User u = login(name, pwd); if (u == null) { showError(); }。如果你想让错误更精确,可以改成抛业务异常区分「密码错」和「账号停用」,但课设里 null 判断完全够用。登录成功后,User 对象里带着 role,界面层根据这个值决定开哪个窗体,这就是上一章说的双角色分流落地代码。
4.2 借书流程:库存检查与事务提交
借书是整套系统里最值得写进简历的代码,因为它涉及一个完整事务。先看核心逻辑:
public boolean borrowBook(int readerId, int bookId) { String checkStock = "SELECT stock FROM t_book WHERE id = ? FOR UPDATE"; String insertBorrow = "INSERT INTO t_borrow (reader_id, book_id, due_time) " + "VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 30 DAY))"; String reduceStock = "UPDATE t_book SET stock = stock - 1 WHERE id = ?"; Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 第一步:查库存并加行锁 PreparedStatement ps = conn.prepareStatement(checkStock); ps.setInt(1, bookId); ResultSet rs = ps.executeQuery(); int stock = 0; if (rs.next()) { stock = rs.getInt("stock"); } if (stock <= 0) { conn.rollback(); return false; // 库存不足,回滚并返回失败 } // 第二步:插入借阅记录,应还时间自动算为 30 天后 PreparedStatement ps2 = conn.prepareStatement(insertBorrow); ps2.setInt(1, readerId); ps2.setInt(2, bookId); ps2.executeUpdate(); // 第三步:扣减库存 PreparedStatement ps3 = conn.prepareStatement(reduceStock); ps3.setInt(1, bookId); ps3.executeUpdate(); conn.commit(); // 全部成功再提交 return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { // 恢复自动提交,避免连接被复用后状态污染 try { if (conn != null) conn.setAutoCommit(true); } catch (SQLException ignored) {} } }三个动作必须当作一个整体:先查库存,够才插入借阅记录,再扣库存。如果中间任何一步失败,前面已经执行的操作必须撤销,否则会出现「记录插了但库存没减」或「库存减了但记录没了」的脏数据。这就是 setAutoCommit(false) 的意义。FOR UPDATE 是 MySQL 的行锁语法,作用是在查询到的行上加锁,防止两个人同时借最后一本书;不加锁的话,两个线程同时查到 stock=1,然后都去扣,最后 stock 变成 -1。
应还时间直接在 SQL 里用 DATE_ADD(NOW(), INTERVAL 30 DAY) 算好,比在 Java 代码里用 Calendar 算再传进来要少一长串代码,也避免了两边的日期格式不一致。finally 里把 autoCommit 恢复为 true 是很多人漏掉的细节:如果连接是从连接池拿的,不恢复的话下一次拿到这条连接的人会莫名继承事务状态。
4.3 还书流程:逾期判断与库存回补
还书是借书的逆操作,但多了一个逾期罚金的判断:
public boolean returnBook(int borrowId, int bookId) { String sqlBorrow = "UPDATE t_borrow SET return_time = NOW(), status = 1, " + "fine = IF(NOW() > due_time, DATEDIFF(NOW(), due_time) * 0.50, 0) " + "WHERE id = ? AND status = 0"; String sqlStock = "UPDATE t_book SET stock = stock + 1 WHERE id = ?"; Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 第一步:更新借阅记录,标记已还并计算罚金 PreparedStatement ps = conn.prepareStatement(sqlBorrow); ps.setInt(1, borrowId); int rows = ps.executeUpdate(); if (rows == 0) { conn.rollback(); return false; // 记录不存在或已归还过,防止重复还书 } // 第二步:回补库存 PreparedStatement ps2 = conn.prepareStatement(sqlStock); ps2.setInt(1, bookId); ps2.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { try { if (conn != null) conn.setAutoCommit(true); } catch (SQLException ignored) {} } }罚金计算是这条 SQL 的精髓:IF(NOW() > due_time, DATEDIFF(NOW(), due_time) * 0.50, 0)。IF 函数先判断当前时间是否过了应还时间,过了就用 DATEDIFF 算出超了多少天,乘 0.5 元/天;没过就是 0。这套逻辑放在 SQL 里比放在 Java 里简洁,但代价是罚金规则写死在 SQL 中,想改成每天 1 元就得改 SQL。课设里这是优点——答辩时你直接指 SQL 说「规则在这里,改一个数字就生效」,比翻半天 Java 代码直观。
WHERE 条件里的AND status = 0是防重复还书的关键。如果记录已经是已还状态,执行 UPDATE 影响行数为 0,代码里通过rows == 0判断并回滚,库存不会被重复加。这属于典型的「幂等」处理,虽然课设不会考这个词,但你主动用出来,面试时一提就是亮点。
4.4 图书检索:模糊查询与分页参数
检索模块用一条 LIKE 加分页搞定,代码结构是这四种语句里最常见的,但坑也不少:
public List<Book> searchBooks(String keyword, int page, int pageSize) { String sql = "SELECT id, book_no, book_name, author, publisher, category, stock, total " + "FROM t_book WHERE book_name LIKE ? OR author LIKE ? " + "ORDER BY id DESC LIMIT ?, ?"; List<Book> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { String like = "%" + keyword + "%"; // % 表示任意字符,前后都加是包含匹配 ps.setString(1, like); ps.setString(2, like); ps.setInt(3, (page - 1) * pageSize); // 偏移量算好再传 ps.setInt(4, pageSize); ResultSet rs = ps.executeQuery(); while (rs.next()) { Book b = new Book(); b.setId(rs.getInt("id")); b.setBookNo(rs.getString("book_no")); b.setBookName(rs.getString("book_name")); b.setAuthor(rs.getString("author")); b.setStock(rs.getInt("stock")); list.add(b); } } catch (SQLException e) { e.printStackTrace(); } return list; }LIKE 匹配的%是模糊匹配的通配符:%关键词%表示任意位置包含关键词,关键词%表示以关键词开头。这里注意一个心理误区——LIKE 查询效率比精确匹配低,因为它基本放弃索引走全表扫描。但图书管理系统的数据量在课设量级也就几百条,全表扫描毫无压力。你真要优化,应该在 category 或 publisher 上加索引,而不是纠结 LIKE 本身。
分页的 offset 计算是(page - 1) * pageSize:第一页显示前 10 条,offset 是 0;第二页跳过 10 条从第 11 条开始,offset 是 10。新手常犯的错误是直接把 page 传进去,结果每页都从第一条开始翻,或者直接传 page-1 忘了乘 pageSize。LIMIT 参数在 MySQL 里是从 start 开始数,不是「页数」语义,所以必须先算后传。ORDER BY id DESC 让新录入的书排前面,这也是一个可以主动说的「排序策略」——为什么不是默认排?因为图书列表场景下用户更想看到新书。
5. 常见问题排查:五条本地跑不通的真实踩坑记录
这一章的每一条都是回车敲下去才发现的真实问题,按「现象 → 原因 → 解决」给你捋清楚,照着排查能省下半天。
5.1 中文乱码:控制台和数据库一起乱
现象:从界面输入中文书名,点保存后再查库,显示成「???」或一串乱码;或者在 SQL 脚本里写了中文注释,导库后注释全变问号。
原因:字符集链路断了一环。连接 URL 缺少useUnicode=true&characterEncoding=utf8,Java 和 MySQL 之间传输中文时用错了编码;或者 SQL 脚本文件本身是 GBK 编码,但目标库是 utf8mb4,源和目标不一致。
解决:三处都检查一遍。连接 URL 加上两个参数;MySQL 初始化时建库语句指定DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;导入脚本时用--default-character-set=utf8mb4强制按 UTF-8 读取脚本。改完后重启程序再试一次,旧乱码数据要删掉重插,因为编码错乱的存量数据无法自动恢复。
5.2 驱动加载失败:ClassNotFoundException 与版本错配
现象:启动程序直接抛ClassNotFoundException: com.mysql.cj.jdbc.Driver,或者报ExceptionInInitializerError,控制台提示找不到驱动类。
原因:两个方向。要么 jar 根本没进 classpath,IDEA 里导入工程后没有把 lib 下的 jar 加入依赖;要么 jar 版本和类名不匹配——用 5.x 的 jar 却写 8.x 的驱动类名,类当然不存在。
解决:先看 lib 目录里到底是什么版本。jar 文件名里通常带版本号,比如 mysql-connector-java-8.0.22.jar,对应的驱动类名必须是com.mysql.cj.jdbc.Driver;如果是 5.1.49,类名改成com.mysql.jdbc.Driver。版本确定后,去 Project Structure → Modules → Dependencies 确认 jar 已经打勾。改完重跑,还有问题就把 IDE 缓存清一次。
5.3 MySQL 8 时区报错:Server time zone value unrecognized
现象:代码跑起来后抛 SQLException,提示类似The server time zone value '�й���ʱ��' is unrecognized,后面还跟一段... represents more than one timezone。
原因:MySQL 8.0 驱动要求明确指定时区,而本机 MySQL 的全局时区没配置,驱动读到一个本地化名称(中文系统下就是乱码)无法识别。这是 8.0 驱动加的新校验,5.7 时代根本没这个问题。
解决:最简单是连接 URL 末尾加&serverTimezone=Asia/Shanghai,一劳永逸。如果想从数据库侧解决,登录 MySQL 后执行SET GLOBAL time_zone = '+08:00';,但只对当前服务会话生效,MySQL 重启后要重新设置,还是 URL 参数最稳定。顺带提醒:报错里那段乱码不是数据乱码,是时区名在中文系统下的显示问题,别误判成字符集故障去改编码。
5.4 数据库连不上:端口占用与服务未启动
现象:报Communications link failure或Connection refused,提示连接 localhost:3306 失败,程序卡几秒后抛出异常。
原因:MySQL 服务没启动,或者 3306 端口被另一个残留实例占了。Windows 下经常遇到装了多个版本 MySQL,旧实例没退出还占着端口,新服务起不来。
解决:先确认服务状态,再查端口占用:
# 查看 3306 端口被谁占用 netstat -ano | findstr 3306 # 用上一行输出的 PID 查进程名 tasklist | findstr <PID>确认是残留进程就把那个进程结束掉,或者是另一套 MySQL 就改端口。改端口要三处同步:MySQL 配置文件、连接 URL 里的端口、以及启动脚本里的参数。三处不一致就会出现「代码说 3306,实例跑在 3307」的错位,这是比服务没启动更隐蔽的坑。
5.5 字段名与关键字撞车:SQL 报错但看起来没错
现象:某条 SQL 执行时抛SQLSyntaxErrorException,报错位置在near 'desc'或near 'order',但你检查字段名和小写拼写都没问题。
原因:字段名或表名撞上了 MySQL 保留字。desc(降序关键字)、order(排序关键字)、group这些都是保留字,作为列名或表名直接用,MySQL 解析器会误当成语法。
解决:两种办法。SQL 里用反引号包住:SELECT `desc` FROM t_borrow,这是最快但最丑的解法,每处用到都要包。推荐办法是改字段名,把 desc 改名成 description,把 order 改名成 orders。顺手提一个同类隐患:t_user 虽然能建,但 user 是 MySQL 系统库里自带的表名语义,有些工具会高亮警告,建议保持源码里的 t_user 前缀命名习惯,避开未来可能的权限混淆。
6. 答辩与演示前的一小时打磨:演示数据、罚金可视化与备份脚本
6.1 演示数据准备:让界面看起来「有货」
空库演示最尴尬,图书表里只有两条数据,借还操作一眼看到底。演示前插入一批贴近真实的数据:十几个分类各放几本书,书名用真实感强的名字;准备两个读者,其中一个有正在借阅的记录;再留一本库存为 0 的书,用来演示「库存不足」的拦截逻辑。数据脚本长这样:
INSERT INTO t_book (book_no, book_name, author, publisher, category, total, stock) VALUES ('J001', 'Java编程思想', '某作者', '某出版社', 'Java', 5, 5), ('J002', '深入理解Java虚拟机', '某作者', '某出版社', 'Java', 3, 3), ('W001', '算法导论', '某作者', '某出版社', '算法', 2, 0);注意库存为 0 的那本书一定也要插进去,专门留给演示「借书失败」分支。答辩时能主动演示异常分支,比只演示顺利流程得分高得多——这证明你写的代码不是只在 happy path 上能跑。
6.2 罚金规则可视化:把 SQL 算出来的数字摆到界面上
源码里罚金已经算好存在 fine 字段,但默认列表界面不一定展示。花半小时在借阅记录表上加一列「罚金」,用的是最土的 CSV 导出兜底,无第三方依赖:
public static void exportToCsv(String filePath, List<BorrowRecord> records) throws IOException { try (BufferedWriter writer = Files.newBufferedWriter(Paths.get(filePath), StandardCharsets.UTF_8)) { writer.write("\uFEFF"); // BOM 头,否则 Excel 打开中文会乱码 writer.write("读者,书名,借出时间,应还时间,状态,罚金"); writer.newLine(); for (BorrowRecord r : records) { writer.write(String.format("%s,%s,%s,%s,%s,%.2f", r.getReaderName(), r.getBookName(), r.getBorrowTime(), r.getDueTime(), r.getStatus() == 1 ? "已还" : "借出", r.getFine())); writer.newLine(); } } }这段代码唯一的坑是第一行那个 BOM 头:不加它,Excel 打开 UTF-8 的 CSV 中文全乱。你如果提前吃过这个亏,答辩现场演示导出文件正常打开,就能顺带讲一句「编码细节我处理过」,这种小细节是最容易让老师点头的地方。
6.3 备份脚本:改坏之前先留后悔药
答辩前改代码、改数据,最怕改完跑不起来又退不回去。养成一个习惯:动手前先把数据库备份一次:
mysqldump -u root -p --default-character-set=utf8mb4 library_db > library_backup_$(date +%Y%m%d_%H%M%S).sql这条命令把整个 library_db 导出成一个带时间戳的 SQL 文件,文件名叫library_backup_20250101_120000.sql这样。恢复时执行mysql -u root -p library_db < 备份文件名.sql就可以把数据库整个还原到备份时刻。时间戳保证每次备份不覆盖上次的,改一处备份一次,永远有后悔药。
从那以后我每次拿到别人的源码工程,都强制自己先跑通、再备份、最后才动手改代码。这套源码的 SQL 脚本、驱动 jar 和关键注释我都是按这个思路整理好的,导入配置好数据库就能跑,需要的话可以直接拿去对照练习,希望帮到你。
本文还有配套的精品资源,点击获取