ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Java+MySQL图书管理系统:从课程大作业到完整落地

Java+MySQL图书管理系统:从课程大作业到完整落地 简介这是一套面向计算机相关专业学生的JavaMySQL图书管理系统课程设计/大作业完整源码包适合正在做期末大作业或需要项目实战练习的学习者参考使用。项目围绕图书信息、图书类型、用户登录与主界面等核心模块展开涵盖图书管理、类型维护、分页查询、登录验证等典型业务场景可直接作为课程设计提交或二次开发的基础。压缩包共133个文件约3.38MB包含16个java源文件、54个class编译文件、58个png界面截图、1个sql数据库脚本及schemaxml、classpath、project等工程配置文件源码、数据库与运行依赖齐全下载后按说明导入即可运行。目前已有227人学习下载项目经过调试能帮助读者快速理解Java桌面端与MySQL交互的完整实现思路节省从零搭建的时间适合作为大作业参考与实战练手素材。1. JavaMySQL 图书管理系统从课程大作业到能跑起来的完整落地路径课程设计周刚开始老师丢下一句“做个图书管理系统Java 连 MySQL交源码和数据库”很多人第一反应是去搜现成项目。搜到的压缩包解压后往往只有一堆.java文件和一句“导入即可运行”数据库脚本缺失、依赖版本对不上、连表结构都看不全。这篇笔记就围绕「基于 JavaMySQL 的图书管理系统源码数据库」这个典型大作业场景把从环境搭建、库表设计、核心增删改查到打包交付的完整链路拆开讲清楚。适合正在做 Java 课程设计的学生也适合想拿一个完整 CRUD 项目练手 Java 基础和 MySQL 的初学者。读完你能自己判断一份源码值不值得用也能从零把系统跑起来并改出属于自己的版本。2. 环境与选型为什么这套组合至今仍是课程设计主力2.1 JDK、MySQL、IDE 三件套的版本怎么定图书管理系统这类大作业技术栈越简单越不容易翻车。核心就三样JDK、MySQL、一个能跑 Java 的 IDE。版本选择上我一般建议 JDK 8 或 JDK 17 二选一MySQL 用 5.7 或 8.0。原因很直接网上能搜到的 Java 课程设计案例源码绝大多数是 JDK 8 时代写的用 JDK 17 跑老代码偶尔会遇到模块化相关的报错而 MySQL 8.0 和 5.7 在驱动类名和连接串上有差异选错了就是ClassNotFoundException或者时区报错。先确认本机环境命令行敲这几条java -version javac -version mysql --version三条命令分别看 JDK 运行时版本、编译器版本、MySQL 版本。如果java有输出但javac提示找不到命令说明只装了 JRE 没装 JDK得重新装 JDK 并把JAVA_HOME配好。MySQL 这边Windows 下装完记得把bin目录加进 PATH否则命令行调不到mysql。提示MySQL 8.0 的驱动类是com.mysql.cj.jdbc.Driver5.7 及以前是com.mysql.jdbc.Driver。连接串里 8.0 还要带serverTimezone参数否则会报时区错误。2.2 数据库连接池别再用 DriverManager 硬连了新手写课程设计最常见的写法是每次操作都DriverManager.getConnection()功能上没问题但代码里到处是重复的连接代码而且频繁开关连接性能差。稍微规范一点的做法是引入连接池。课程设计里最轻量的选择是 Druid 或者 HikariCP加一个 jar 包、写一个配置文件就能用。以 HikariCP 为例配置文件db.properties长这样jdbcUrljdbc:mysql://localhost:3306/library?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 usernameroot password你的密码 driverClassNamecom.mysql.cj.jdbc.Driver maximumPoolSize10jdbcUrl里的library是数据库名useSSLfalse关掉 SSL 省去证书麻烦serverTimezone必须设characterEncodingutf8保证中文书名不乱码。maximumPoolSize10对课程设计足够连接池太大反而占资源。读取配置并初始化连接池的代码import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import java.io.InputStream; import java.util.Properties; public class DBUtil { private static HikariDataSource dataSource; static { try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)) { Properties props new Properties(); props.load(in); HikariConfig config new HikariConfig(props); dataSource new HikariDataSource(config); } catch (Exception e) { throw new RuntimeException(数据库连接池初始化失败, e); } } public static HikariDataSource getDataSource() { return dataSource; } }这段代码用静态代码块在类加载时初始化连接池getResourceAsStream从 classpath 根目录读配置文件所以db.properties要放在src目录下。HikariConfig直接接收Properties对象字段名和配置文件里的 key 一一对应。初始化失败直接抛运行时异常避免后续拿到 null 连接。2.3 分层结构为什么大作业也要讲 MVC很多人觉得课程设计随便写写就行所有代码塞进一个类。但图书管理系统涉及图书、读者、借阅记录三块业务全堆一起后期改一个功能要翻几百行。常见做法是按 MVC 分三层entity放实体类dao放数据库操作service放业务逻辑ui或controller放界面和交互。这样每层职责清晰答辩时讲起来也有条理。目录结构大致如下src/ ├── entity/ Book.java, Reader.java, Borrow.java ├── dao/ BookDao.java, ReaderDao.java, BorrowDao.java ├── service/ BookService.java, BorrowService.java ├── util/ DBUtil.java ├── ui/ MainFrame.java └── db.properties实体类字段和数据库表字段一一对应DAO 层只负责 SQL 执行Service 层做借阅规则判断比如库存够不够、读者有没有超期UI 层只管展示和收集输入。这个结构不复杂但能让你的代码从“能跑”变成“能看”。3. 库表设计与建库脚本三张核心表撑起整个系统3.1 图书表、读者表、借阅表怎么设计图书管理系统的数据模型不复杂核心就三张表。图书表存书的基本信息和库存读者表存借书人信息借阅表记录谁在什么时候借了哪本书、还没还。三张表通过外键关联借阅表同时引用图书表和读者表的主键。建库建表脚本CREATE DATABASE IF NOT EXISTS library DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE library; CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE, title VARCHAR(100) NOT NULL, author VARCHAR(50), publisher VARCHAR(100), total INT NOT NULL DEFAULT 1, available INT NOT NULL DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE reader ( id INT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, phone VARCHAR(20), max_borrow INT NOT NULL DEFAULT 5, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE borrow ( id INT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL, reader_id INT NOT NULL, borrow_date DATE NOT NULL, due_date DATE NOT NULL, return_date DATE DEFAULT NULL, status TINYINT NOT NULL DEFAULT 0, CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book(id), CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES reader(id) ) ENGINEInnoDB;book表的total是总库存available是可借数量借出一本available减一归还加一。reader表的max_borrow控制每个读者最多能借几本。borrow表的status用 0 表示未归还、1 表示已归还return_date为空说明还没还。三个外键约束保证借阅记录不会指向不存在的书或读者。注意字符集用utf8mb4而不是utf8前者能存 emoji 和更多生僻字后者在 MySQL 里其实是残缺的 utf8。课程设计里书名带特殊符号的情况不少用 utf8mb4 省心。3.2 初始化数据与自增主键的坑建完表要插几条测试数据否则界面打开一片空白没法演示INSERT INTO book (isbn, title, author, publisher, total, available) VALUES (9787111213826, Java编程思想, Bruce Eckel, 机械工业出版社, 5, 5), (9787115546081, MySQL技术内幕, 姜承尧, 人民邮电出版社, 3, 3), (9787121362217, 深入理解Java虚拟机, 周志明, 机械工业出版社, 2, 2); INSERT INTO reader (card_no, name, phone, max_borrow) VALUES (R001, 张三, 13800000001, 5), (R002, 李四, 13800000002, 3);插入时不用指定id自增主键会自动分配。这里有个血泪经验如果你手动插入过指定id的记录比如INSERT INTO book (id, ...) VALUES (100, ...)MySQL 的自增计数器会跳到 101之后自动插入的id从 101 开始。课程设计演示时如果发现新增图书的 id 突然变成很大的数就是这个原因。解决办法是ALTER TABLE book AUTO_INCREMENT 1;重置但前提是表里没有 id 更大的记录。3.3 用 Navicat 还是命令行导入脚本建库脚本写好后导入方式有两种。命令行适合脚本化、可复现mysql -u root -p library.sql-u指定用户名-p表示需要输入密码把 sql 文件内容喂给 mysql 客户端执行。执行前确保library.sql里没有USE之外的库切换语句否则可能导到别的库。图形化工具比如 Navicat、MySQL Workbench 更适合边看边改。Navicat 里右键连接选“运行 SQL 文件”选中脚本执行即可。Workbench 里用 “File → Open SQL Script” 打开再点闪电图标执行。两种方式结果一样命令行更适合写进文档让别人复现图形化适合自己调试。提示导入前先确认脚本里的数据库名和你实际要用的名字一致。有些源码包里的脚本写死了library你本地如果已经有同名库导入会报错或覆盖先DROP DATABASE IF EXISTS library;再导入更干净。4. 核心功能实现增删改查与借阅逻辑的代码落地4.1 图书的增删改查从 DAO 到 Service图书管理最基础的就是增删改查。DAO 层用PreparedStatement防注入Service 层做参数校验。先看新增图书的 DAO 方法public boolean addBook(Book book) { String sql INSERT INTO book (isbn, title, author, publisher, total, available) VALUES (?, ?, ?, ?, ?, ?); try (Connection conn DBUtil.getDataSource().getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, book.getIsbn()); ps.setString(2, book.getTitle()); ps.setString(3, book.getAuthor()); ps.setString(4, book.getPublisher()); ps.setInt(5, book.getTotal()); ps.setInt(6, book.getTotal()); // 新增时 available 等于 total return ps.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } }PreparedStatement用占位符?传参避免 SQL 注入也省去手动拼字符串时引号和转义的处理。try-with-resources保证连接和语句自动关闭不用写 finally。executeUpdate返回受影响行数大于 0 说明插入成功。注意available初始值等于total因为新书全部可借。查询图书支持按书名模糊搜索public ListBook searchByTitle(String keyword) { String sql SELECT * FROM book WHERE title LIKE ? ORDER BY id DESC; ListBook list new ArrayList(); try (Connection conn DBUtil.getDataSource().getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Book b new Book(); b.setId(rs.getInt(id)); b.setIsbn(rs.getString(isbn)); b.setTitle(rs.getString(title)); b.setAuthor(rs.getString(author)); b.setPublisher(rs.getString(publisher)); b.setTotal(rs.getInt(total)); b.setAvailable(rs.getInt(available)); list.add(b); } } } catch (SQLException e) { e.printStackTrace(); } return list; }LIKE ?配合% keyword %实现包含匹配用户输入“Java”能搜到“Java编程思想”。ORDER BY id DESC让最新添加的书排前面。ResultSet遍历时按列名取值比按索引取值可读性好列顺序变了也不影响。Service 层在调用 DAO 前做校验public String addBook(Book book) { if (book.getIsbn() null || book.getIsbn().trim().isEmpty()) { return ISBN 不能为空; } if (book.getTotal() 0) { return 库存数量必须大于 0; } if (bookDao.findByIsbn(book.getIsbn()) ! null) { return 该 ISBN 已存在; } return bookDao.addBook(book) ? 添加成功 : 添加失败; }返回字符串而不是布尔值UI 层直接把结果弹给用户。ISBN 唯一性检查放在 Service 层DAO 层只管插入职责分明。findByIsbn是另一个查询方法按 ISBN 精确查返回Book或null。4.2 借书还书库存扣减与事务控制借书是这套系统里唯一需要事务的操作因为它同时改borrow表和book表。先插借阅记录再扣库存两步必须一起成功或一起失败。public String borrowBook(int bookId, int readerId) { Connection conn null; try { conn DBUtil.getDataSource().getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 检查库存 String checkSql SELECT available FROM book WHERE id ? FOR UPDATE; PreparedStatement checkPs conn.prepareStatement(checkSql); checkPs.setInt(1, bookId); ResultSet rs checkPs.executeQuery(); if (!rs.next() || rs.getInt(available) 0) { conn.rollback(); return 库存不足; } // 2. 检查读者借阅上限 String countSql SELECT COUNT(*) FROM borrow WHERE reader_id ? AND status 0; PreparedStatement countPs conn.prepareStatement(countSql); countPs.setInt(1, readerId); ResultSet countRs countPs.executeQuery(); countRs.next(); int borrowed countRs.getInt(1); // 假设 maxBorrow 从 reader 表查出这里简化为 5 if (borrowed 5) { conn.rollback(); return 已达借阅上限; } // 3. 插入借阅记录 String insertSql INSERT INTO borrow (book_id, reader_id, borrow_date, due_date, status) VALUES (?, ?, CURDATE(), DATE_ADD(CURDATE(), INTERVAL 30 DAY), 0); PreparedStatement insertPs conn.prepareStatement(insertSql); insertPs.setInt(1, bookId); insertPs.setInt(2, readerId); insertPs.executeUpdate(); // 4. 扣减库存 String updateSql UPDATE book SET available available - 1 WHERE id ?; PreparedStatement updatePs conn.prepareStatement(updateSql); updatePs.setInt(1, bookId); updatePs.executeUpdate(); conn.commit(); return 借阅成功; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return 借阅失败 e.getMessage(); } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }setAutoCommit(false)开启事务所有操作在同一个连接上执行。SELECT ... FOR UPDATE对图书行加排他锁防止两个读者同时借最后一本时超借。DATE_ADD(CURDATE(), INTERVAL 30 DAY)算出 30 天后的应还日期。任何一步失败就rollback全部成功才commit。finally 里恢复自动提交并关闭连接避免连接池里的连接带着未提交事务被复用。还书逻辑相对简单更新借阅记录状态和归还日期同时把库存加回去public String returnBook(int borrowId) { Connection conn null; try { conn DBUtil.getDataSource().getConnection(); conn.setAutoCommit(false); String findSql SELECT book_id FROM borrow WHERE id ? AND status 0; PreparedStatement findPs conn.prepareStatement(findSql); findPs.setInt(1, borrowId); ResultSet rs findPs.executeQuery(); if (!rs.next()) { conn.rollback(); return 借阅记录不存在或已归还; } int bookId rs.getInt(book_id); String updateBorrow UPDATE borrow SET status 1, return_date CURDATE() WHERE id ?; PreparedStatement ub conn.prepareStatement(updateBorrow); ub.setInt(1, borrowId); ub.executeUpdate(); String updateBook UPDATE book SET available available 1 WHERE id ?; PreparedStatement ubo conn.prepareStatement(updateBook); ubo.setInt(1, bookId); ubo.executeUpdate(); conn.commit(); return 归还成功; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return 归还失败; } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }先查借阅记录确认状态是未归还拿到book_id后更新借阅表再更新图书表。同样用事务包住保证数据一致。4.3 界面层怎么调 Service 并处理返回结果界面层不管是 Swing 还是 JavaFX核心就是收集输入、调 Service、根据返回字符串弹提示。以 Swing 的按钮点击为例addButton.addActionListener(e - { String isbn isbnField.getText().trim(); String title titleField.getText().trim(); String author authorField.getText().trim(); String publisher publisherField.getText().trim(); int total; try { total Integer.parseInt(totalField.getText().trim()); } catch (NumberFormatException ex) { JOptionPane.showMessageDialog(this, 库存必须是数字); return; } Book book new Book(isbn, title, author, publisher, total, total); String result bookService.addBook(book); JOptionPane.showMessageDialog(this, result); if (添加成功.equals(result)) { refreshTable(); // 重新加载表格数据 } });Integer.parseInt可能抛异常必须捕获否则用户输入非数字直接崩界面。Service 返回的字符串直接弹窗展示成功时刷新表格。refreshTable重新查一遍数据库填充JTable保证界面和数据一致。注意Swing 的事件处理在 EDT 线程上执行如果 Service 里有耗时操作比如大批量导入界面会卡死。课程设计数据量小一般没事但要知道这个边界。5. 避坑与排查课程设计里最容易翻车的五个点5.1 中文乱码从数据库到界面全链路排查现象数据库里书名显示正常但 Java 程序查出来是问号或乱码。原因字符集在某一环没对齐。常见的是数据库建库时用了latin1或者连接串没带characterEncodingutf8或者 JDK 默认编码和数据库不一致。解决三步确认。第一SHOW CREATE DATABASE library;看字符集是不是utf8mb4第二连接串加上characterEncodingutf8第三启动 JVM 时加-Dfile.encodingUTF-8。三处都对齐基本不会再乱码。5.2 时区报错MySQL 8.0 的 serverTimezone 必填现象连接 MySQL 8.0 时报The server time zone value ???ú±ê×??±?? is unrecognized。原因MySQL 8.0 驱动要求明确指定时区不指定就按服务器时区解析中文 Windows 上服务器时区名带特殊字符导致解析失败。解决连接串加serverTimezoneAsia/Shanghai或者serverTimezoneUTC。加完重启应用即可。这个坑在 MySQL 5.7 上不会出现所以从 5.7 换到 8.0 的人经常中招。5.3 驱动类找不到ClassNotFoundException 的三种可能现象启动时报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因一是 jar 包没加进 classpath二是 MySQL 5.7 的驱动类名是com.mysql.jdbc.Driver写成了 8.0 的类名三是用了 Maven 但依赖没下载成功。解决确认mysql-connector-java的 jar 在 classpath 里。5.7 用com.mysql.jdbc.Driver8.0 用com.mysql.cj.jdbc.Driver。Maven 项目检查pom.xml里依赖的 scope 是不是provided导致运行时没有。5.4 连接池耗尽连接没关导致的假死现象程序运行一段时间后所有数据库操作都超时重启才好。原因某处代码拿了连接没关连接池的 10 个连接被占满后续请求一直等。解决所有Connection、PreparedStatement、ResultSet都用 try-with-resources 包住。检查有没有在循环里反复getConnection()而不关的写法。连接池配置里可以加leakDetectionThreshold60000超过 60 秒没归还的连接会打日志方便定位泄漏点。5.5 外键约束导致删除失败现象删除一本图书时报Cannot delete or update a parent row: a foreign key constraint fails。原因这本书在borrow表里有借阅记录外键约束阻止删除。解决两种思路。一是先删借阅记录再删图书但会丢失历史二是给外键加ON DELETE CASCADE删图书时自动删关联借阅记录。课程设计里更稳妥的做法是逻辑删除给book表加is_deleted字段删除时置 1查询时过滤保留数据完整性。6. 从能跑到能交打包、演示与二次开发的实用技巧把系统跑通只是第一步课程设计还要交源码和数据库答辩时得演示。打包这块纯 Java 项目用 IDE 的 “Build Artifacts” 生成 jar或者 Maven 的mvn package。如果项目带了第三方 jar比如 MySQL 驱动、HikariCP要么打成 fat jar要么在 MANIFEST 里配 Class-Path。fat jar 更省事Maven 用maven-shade-pluginGradle 用shadow插件把依赖全塞进一个 jar双击就能跑。数据库交付别只给一个.sql文件就完事。我一般会在脚本开头加DROP DATABASE IF EXISTS library;再CREATE DATABASE保证别人导入时不会因为库已存在而失败。脚本末尾附上几条测试数据的INSERT导入完直接有数据可演示。文件名带上日期比如library_20250101.sql避免多个版本混淆。演示前有个习惯我保持了几年把数据库连接信息、默认账号密码写在一个README.txt里和源码放一起。答辩时老师问“怎么运行”直接指 README比现场翻代码找配置快得多。README 里写清楚 JDK 版本、MySQL 版本、导入脚本的命令、启动主类的全限定名。这几行字能省掉很多解释成本。二次开发方面这套系统最容易扩展的方向是加“预约”功能。读者想借的书库存为 0 时可以预约等有人还书时按预约顺序通知。实现上加一张reservation表字段包括book_id、reader_id、reserve_time、status。还书逻辑里判断有没有预约记录有就把available直接留给预约者而不是放回公共池。这个改动涉及还书事务的扩展是练手事务和状态机的好机会。另一个实用技巧是给查询加索引。borrow表的reader_id和status经常一起出现在查询条件里加一个联合索引idx_reader_status (reader_id, status)能明显加快“查某读者未还记录”的速度。book表的title如果经常模糊搜索可以考虑全文索引但 MySQL 5.7 的中文全文索引需要ngram分词器配置略麻烦课程设计数据量小的话LIKE够用。最后说个我踩过的坑有次答辩前把项目从笔记本拷到答辩教室的电脑上结果那边 MySQL 版本是 5.7我本地是 8.0驱动类名不匹配直接连不上。从那以后我养成了一个习惯交付前在 README 里写明“本项目在 MySQL 8.0 JDK 17 下测试通过”并且把驱动 jar 一起打包进去不依赖目标机器的环境。这个习惯让我后面几次交付都没再翻车。希望帮到你。本文还有配套的精品资源点击获取
返回列表