简介Java编写的图书管理系统面向Java初学者与有意巩固开发流程的学习者完整覆盖图书信息增删改、用户管理、借阅归还、条件查询与借阅统计等核心功能。项目涉及Swing图形界面、集合框架、JDBC数据库连接及MVC分层思想帮助理解实际业务系统从界面到数据层的组织方式。压缩包共38个文件、约47KB包含14个Java源文件、16个class字节码文件、3个文本数据文件以及Eclipse工程配置、欢迎界面图片等资源导入开发环境即可阅读和运行也便于对照源码与编译产物。已有1525人学习下载适合课程设计参考或自学练手。通过阅读代码可掌握Java面向对象设计、数据库交互和GUI开发的完整实现路径是一份轻量而实用的入门实践资料。1. Java 写的图书管理系统为什么这么多课程设计里只有这份值得下载“Java 写的图书管理系统”几乎是每个计算机专业学生听得最多的题目可网上流传的版本要么只有控制台菜单要么数据库脚本根本建不出来要么代码里到处都是写死的路径。这份源码把图书管理系统该有的登录、图书管理、借阅管理、读者管理和统计报表完整串在了一起界面用 Swing数据层用 MySQL代码拿过来改一改连接配置就能跑。它适合做课程设计、应付答辩也想读一个完整 Java 分层工程的入门开发者。它解决的就是最经典的那个痛点源码看起来完整但就是跑不起来。这份资源能帮你把大部分调试成本省掉。2. 技术选型Swing MySQL JDBC 这套组合的理由和代价把压缩包解压后我习惯先扫一遍目录结构确认它是不是“能跑”的工程而不是零散文件。这里能直接看到pom.xml、src/main/java、src/main/resources说明它是一个用 Maven 管理的标准工程。界面代码收在ui包业务逻辑收在service包数据库访问收在dao包入口类App.java放在根目录。这套结构不是多先进但对图书管理系统来说正好是能看懂、能改、能答辩的组合。2.1 界面层为什么选 Swing 而不是 JavaFX 或 Web选 Swing 的理由很现实它不依赖额外运行时。JDK 装好之后JFrame、JTable、JOptionPane这些组件直接能用不会因为 JavaFX 模块没配置好而报一堆奇怪的ModuleNotFoundException。很多课程设计环境里老师的机器还停留在老版本 JDKJavaFX 这一关就会卡住不少人。举个典型例子图书列表页在 Swing 里需要自己封装一套表格模型。资源里的BookTableModel大概是这个样子public class BookTableModel extends AbstractTableModel { private String[] columns {ID, 书名, 作者, 分类, 库存}; private ListBook books; public void setBooks(ListBook books) { this.books books; fireTableDataChanged(); } Override public int getRowCount() { return books null ? 0 : books.size(); } Override public int getColumnCount() { return columns.length; } Override public Object getValueAt(int rowIndex, int columnIndex) { Book b books.get(rowIndex); switch (columnIndex) { case 0: return b.getId(); case 1: return b.getTitle(); case 2: return b.getAuthor(); case 3: return b.getCategoryName(); case 4: return b.getRemainStock(); default: return null; } } }这段代码的逻辑是把内存里的ListBook适配成 Swing 表格需要的行列结构。fireTableDataChanged()的作用是通知界面数据变了要重绘所以新增或删除图书后只要调用setBooks(...)表格就会自动刷新。类似这种小封装在 Web 项目里可能由框架完成但在这份源码里都是手写的正好适合拿来学习事件驱动和模型-视图分离。2.2 数据层文件方案和数据库方案的分界线在哪图书管理系统最容易被做成“文本文件存储版”但那只能应付打分应付不了真实使用。文本文件没有事务两个人同时借书就可能写坏数据查询也只能全表扫做个模糊搜索得遍历每一行。所以这份源码选了 MySQL 加 JDBC不引入 MyBatis 或 Hibernate是为了把学习成本压在可控范围。我把两种方案的界限列一个简单的表存储方式优点缺点适合场景txt / 序列化文件零配置、解压即用无并发控制、不适合复杂查询极限演示、控制台作业SQLite单文件、轻量Java 驱动配置麻烦、并发较弱单机小工具MySQL事务完善、支持并发、查询灵活需要额外安装服务和初始化脚本本资源的目标场景网上不少人下载到数据库版源码后第一反应是“我机器上没装 MySQL能不能改文件存储”。我的建议是别改。借阅记录涉及图书、读者、借书时间、还书时间多个关联文件系统很难处理关联查询。资源里已经提供init.sql装好 MySQL 执行一遍脚本就行这种成本可以接受。JDBC 连接配置集中在resources/db.properties内容类似jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456 jdbc.pool.maxSize10jdbc.url里的三个参数是高频踩坑点。useUnicodetrue和characterEncodingutf8解决中文写入乱码serverTimezoneAsia/Shanghai解决数据库时间与本地时间对不上的问题。jdbc.pool.maxSize是连接池上限课程设计场景 10 个已经足够。2.3 分层结构一个请求从点按钮到写数据库到底走了几步这份源码采用经典的三层结构跟大多数企业项目的思路一致。ui层只负责收集输入和展示结果service层处理业务校验dao层只做增删改查。我把包结构简化成下面这样src/main/java/com/library ├── App.java ├── ui // Swing 界面 │ ├── LoginFrame.java │ └── MainFrame.java ├── service // 登录校验、借书、还书、统计 │ ├── LoginService.java │ └── BorrowService.java └── dao // 数据访问 ├── AdminDao.java ├── BookDao.java └── BorrowDao.java这三层的调用关系是界面按钮触发service方法service解析参数并调用daodao返回结果后逐层向上传。好处是以后想把 Swing 换成 JavaFX只要ui包整体重写service和dao不需要动。答辩时老师问“你这是什么架构”你直接把这层关系画出来比任何形容词都有说服力。实际处理中我看到很多同学把 SQL 写在界面类里按钮监听器里直接拼 SQL刚开始能跑后来越改越乱。这份源码至少包结构是干净的值得照着这个思路去组织自己的代码。3. 核心模块拆解登录、图书管理和借阅流程的代码实现我拆项目时最关心的就是三个功能写没写全登录、图书维护、借书还书。这三个功能串起来图书管理系统的主干就成了。如果这三个模块的代码能看明白剩下的“读者管理”和“统计报表”都是锦上添花。3.1 登录模块加盐哈希和权限校验登录模块没有用明文密码而是给密码加了一个随机盐再拼接后做 MD5 哈希。代码结构上ui层拿到输入后交给LoginService由service层去查库并校验。简化后的逻辑是这样public class LoginService { private AdminDao adminDao new AdminDao(); public boolean login(String username, String password) { if (username null || username.trim().isEmpty()) { return false; } Admin admin adminDao.findByUsername(username); if (admin null) { return false; } // 数据库里存的是“盐 密码”拼接后的 MD5 String digest Md5Util.md5Hex(password admin.getSalt()); return digest.equals(admin.getPassword()); } }这里的核心不是 MD5 本身而是“加盐”操作。直接对密码做哈希所有用户很容易被枚举碰撞把用户的盐拼进去之后相同的密码也会产生不同的摘要。Md5Util.md5Hex接收参数是密码原文加盐后的拼接值返回 32 位十六进制字符串。这个工具类在项目里是静态方法可以直接复用。权限校验通常在MainFrame里做。比如管理员账号role1能看到“系统设置”普通操作员role0只能看到借还书按钮。这种校验要放在service层而不是界面层否则绕过前端也能直接调用。3.2 图书管理模块分页查询和多条件组合图书管理最怕的是在一个表里塞几千条数据然后SELECT *一次性加载。资源里的BookDao.search做了分页而且支持书名、作者、分类多条件组合查询。核心是一个动态拼接 SQL 的方法public ListBook search(String keyword, Integer categoryId, int page, int size) { StringBuilder sql new StringBuilder(SELECT * FROM book WHERE 11 ); ListObject params new ArrayList(); if (keyword ! null !keyword.trim().isEmpty()) { sql.append(AND (title LIKE ? OR author LIKE ?) ); params.add(% keyword %); params.add(% keyword %); } if (categoryId ! null) { sql.append(AND category_id ? ); params.add(categoryId); } // 分页参数 sql.append(LIMIT ? OFFSET ? ); params.add(size); params.add((page - 1) * size); return jdbcTemplate.queryList(sql.toString(), params.toArray()); }这里的WHERE 11看起来有点傻气但它是个很实用的技巧后续每个条件都直接拼AND ...不需要判断前面是否已有条件。如果去掉11每次拼接前都要检查where关键字是不是已经写过了代码会笨重许多。LIMIT ? OFFSET ?是 MySQL 的分页写法第一个问号是每页条数第二个问号是跳过多少条记录。比如第 2 页每页 10 条OFFSET就是 10。翻页之后要显示“共多少条”所以这里会配套一个countByCondition(keyword, categoryId)方法逻辑一样但不带LIMIT只返回总数。界面层拿到总数后计算总页数传给表格组件。3.3 借阅与归还事务边界和状态字段借书操作不能只在borrow_record表里插一条记录就完事必须同时把book表的库存减一。这两步必须在一个事务里否则可能出现“借阅记录有了但库存没减”或“库存少了但查不到谁借的”。常规写法是用Connection手动控制事务public void borrowBook(int bookId, int readerId) throws Exception { Connection conn DBManager.getConnection(); try { conn.setAutoCommit(false); // 第一步判断库存是否足够 Book book bookDao.getById(conn, bookId); if (book.getRemainStock() 0) { throw new BusinessException(图书库存不足); } // 第二步库存减 1 bookDao.decreaseStock(conn, bookId); // 第三步插入借阅记录状态默认未归还 borrowDao.insert(conn, bookId, readerId); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } }这里最关键的是把conn传给每一个dao方法保证它们用的是同一个数据库连接。如果每个方法内部自己getConnection()事务就失效了因为不是同一个会话。setAutoCommit(false)之后只有调用commit()才是真正落库。任何一步报错rollback()会把库存和借阅记录全部恢复。还书逻辑更简单但也最容易写错。正确做法是把borrow_record里的return_time更新为当前时间把status从“借出”改成“已还”再把图书库存加一。很多初学者会直接删掉这条借阅记录这会让历史统计完全失真这个问题后面会专门说。4. 数据库设计六张表串联图书借阅的完整关系我原本以为这种课程设计只有三四张表实际看下来资源里给了六张表分别是admin、reader、book_category、book、borrow_record、penalty。其中penalty表主要用来记录逾期罚款不算复杂但多这张表会让报表功能更有说服力。4.1 表结构定义与建表语句book_category先建因为book表外键依赖它。我挑重点表展示一下设计思路CREATE TABLE book_category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL ); CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL, title VARCHAR(100) NOT NULL, author VARCHAR(50), category_id INT, total_stock INT NOT NULL DEFAULT 0, remain_stock INT NOT NULL DEFAULT 0, FOREIGN KEY (category_id) REFERENCES book_category(id) ); CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL, reader_id INT NOT NULL, borrow_time DATETIME NOT NULL, due_time DATETIME NOT NULL, return_time DATETIME DEFAULT NULL, status TINYINT NOT NULL DEFAULT 0, FOREIGN KEY (book_id) REFERENCES book(id), FOREIGN KEY (reader_id) REFERENCES reader(id), INDEX idx_reader_status (reader_id, status) );borrow_record是整个系统的核心表。borrow_time是借出时间due_time是应还时间return_time是实际归还时间。status字段我建议用数字0表示借出未还1表示已归还。这里没有直接删记录所以任何一条历史借阅行为都能被追踪到。额外加了一个组合索引idx_reader_status因为最常查的问题是“某读者当前借了哪些书”这正好命中reader_id status的检索条件。数据量不大时感受不到区别但这是建立正确习惯的好时机。4.2 初始化数据与测试账号资源里附带的sql/init.sql不仅建表还插入了测试数据。管理员表的结构是CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(32) NOT NULL, salt VARCHAR(16) NOT NULL, role TINYINT NOT NULL DEFAULT 1 ); INSERT INTO admin (username, password, salt, role) VALUES (admin, e10adc3949ba59abbe56e057f20f883e, abc123, 1);这段初始化数据的重点是即使你看到别人源码里默认密码是123456也不能直接拿去跑因为密码字段里存的是加了盐的 MD5。INSERT语句里的e10adc3949ba59abbe56e057f20f883e是用盐abc123拼接123456之后算出来的所以登录校验必须走同一套加密逻辑不能单靠字段值判断密码。测试读者和图书各插了几条量不大但足够覆盖“模糊搜索”“借阅新增”“库存减少”这几条主要链路。打开资源后第一步不是急着写新功能而是用这份测试数据把登录、借书、还书点一遍确认环境 OK。4.3 JDBC 连接参数的完整配置数据库配置集中在db.properties我前面提过一半这里补全并解释每个参数的用途jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://192.168.1.10:3306/library?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password123456192.168.1.10是服务器地址本地跑就改成localhost。useSSLfalse是为了避免本地开发环境没配证书时报警告。如果你的 MySQL 版本是 8.x驱动类名必须用com.mysql.cj.jdbc.Driver如果是早期 5.x可以兼容旧类名com.mysql.jdbc.Driver。但这几年装 MySQL 基本都会装 8.x所以资源里直接写了新类名。连接池初始化一般在DBManager静态代码块中完成。翻开源码你会看到类似这样的话Class.forName(driver)用来注册驱动然后DataSource或自建队列负责管理连接。课程设计阶段不用追求超过 20 个连接反而容易把数据库连接数打满。5. 避坑指南从中文乱码到借阅记录消失的四个高频问题这套源码本身能跑但环境不一样翻车点几乎都集中在编码、驱动、事务设计和时区四件事上。我自己拿到后跑了一遍也帮别人看过这类问题下面每条都是真实遇到过的现象按“现象 → 原因 → 解决”的顺序整理。5.1 中文乱码界面、文件、数据库三层都要查现象登录进去之后图书列表里的中文标题全部变成问号或者按钮上的“借阅”两个字变成乱码。原因大概率是编码链路不一致。源代码文件是 UTF-8 保存但编译时用了系统默认的 GBK或者db.properties里没写characterEncodingutf8数据库连接用默认字符集接收中文。解决先统一源文件编码Maven 工程在pom.xml里加上properties project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties再用命令行编译时指定javac -encoding UTF-8。数据库连接串里确认有useUnicodetruecharacterEncodingutf8。最后检查 MySQL 表本身的字符集最好在建库时用CREATE DATABASE library DEFAULT CHARSET utf8mb4否则前面全对表还是老编码照样乱。5.2 驱动版本ClassNotFoundException 的真相现象明明已经把 mysql 驱动 jar 包扔进lib目录运行还是报ClassNotFoundException: com.mysql.jdbc.Driver。原因MySQL 官方在 5.x 版本里驱动类名是com.mysql.jdbc.Driver到 8.x 改成了com.mysql.cj.jdbc.Driver。很多课程设计要求用新版驱动但代码里还写着旧类名自然找不到。解决优先把驱动类名改成com.mysql.cj.jdbc.Driver。同时不推荐手动往lib里拖 jar建议用 Maven 管理依赖明确指定一个实际版本。比如pom.xml里写mysql-connector-j的8.x版本然后执行mvn clean package由构建工具去下载和关联依赖避免路径问题。5.3 借阅记录消失不要在还书时 DELETE现象还书操作后界面上确实显示“归还成功”图书库存也加回来了但打开历史借阅记录这条数据查不到了。原因因为还书代码用的是DELETE FROM borrow_record WHERE id?。这种硬删除把借阅历史抹掉了后面的逾期统计和借阅次数分析全部失去数据来源。解决把还书改成更新语句UPDATE borrow_record SET return_time NOW(), status 1 WHERE id ?;status从0变成1return_time记录实际归还时刻。这样既保留历史痕迹又不影响当前库存统计。所有统计报表都不用担心“少数据”。5.4 数据库时间差借书时间显示的却是昨天现象借书成功后界面上显示的borrow_time是凌晨两三点跟当前时间差了好几个小时。原因MySQL 驱动和 JVM 的时区不一致。数据库服务器或连接串没有指定时区驱动会用默认时区去解释DATETIME导致显示偏差。解决在jdbc.url里加上serverTimezoneAsia/Shanghai。如果代码部署到不同时区的服务器不要写成固定字符串可以改成serverTimezoneHongkong或根据服务器环境动态设置。还有一个办法是数据库连接建立后执行SET time_zone 08:00但最省事的还是在连接串里直接写明。6. 让系统具有交付感导出逾期报表与一键打包功能全做完项目还差最后一步把结果能交付给使用人。这里分享两个我常用的收尾操作一是导出逾期报表二是打包成可执行 jar。6.1 用一条 SQL 生成逾期明细并导出 CSV统计“哪些读者逾期未还”是图书管理系统的常见需求。核心 SQL 是这样SELECT reader.name AS reader_name, book.title AS book_title, borrow_record.due_time FROM borrow_record JOIN reader ON borrow_record.reader_id reader.id JOIN book ON borrow_record.book_id book.id WHERE borrow_record.status 0 AND borrow_record.due_time NOW();查出来的结果可以直接写入 CSV 文件用 Java 写一个简单导出方法。注意写入时统一用 UTF-8但 Excel 打开 CSV 可能用 ANSI 编码所以习惯给文件名加上时间戳避免重复覆盖。6.2 用 Maven 打包成可双击运行的 jar课程设计交源码没问题但如果你想把程序直接给别人演示最好打成可执行 jar。使用maven-shade-plugin可以把 MySQL 驱动等依赖都打进同一个 jar 包避免对方还需要手动配classpath。在pom.xml中配置好主类为com.library.App执行mvn clean package java -jar target/library-system.jar命令拆分来看clean清理上一次编译产物package执行打包java -jar启动。这里要注意如果你的 MySQL 服务不在本机运行前仍需修改db.properties里的jdbc.url否则对方双击后只会看到连接失败弹窗这是最容易被误以为“没打包好”的场景。我拿到这类项目的第一反应是先看resources下的配置文件和 SQL 脚本再找入口类而不是上来就点 Run。以前我吃过亏看到代码就直接运行结果中文乱码和数据库连接失败交替出现白折腾了一个晚上。从那以后我每次检查项目都会强制走一遍“先看依赖、再看配置、最后跑入口”的顺序这套项目的坑基本都能提前拦下来。希望这个流程也能帮到你。本文还有配套的精品资源点击获取