简介:一套基于Java GUI的图书管理系统课程设计代码包,主要面向计算机相关专业学生、Java开发者以及需要完成毕业设计或课程设计的读者。系统实现了图书资料管理的完整闭环,涵盖系统管理、进书登记、图书入库编目、模糊查询、借书还书等模块,其中权限控制可将操作者分为管理员和普通读者,并支持借阅数量校验、续借判断与超期罚款计算。资源包共263个文件,包括93个Java源文件、109个class编译文件、26个依赖jar包,并附有sql数据库脚本、jpg/png界面截图、properties配置文件以及doc课程设计报告,总大小16.87MB,便于直接导入项目运行和对比学习。已有5589人学习下载;对想掌握Java Swing界面编程、理解数据库表设计或快速搭建同类管理系统的读者而言,其既提供了可运行的全套代码,也给出了从需求分析到报告撰写的完整参考,具有较高的实践价值。 图书管理系统几乎是每个Java初学者都会遇到的题目,从大作业到毕业设计,从课程设计到面试作品,总能见到它的身影。很多人觉得这题目太老、太简单,但我个人看法正好相反——一个用Java GUI实现、结构清晰、逻辑完整的图书管理系统,恰恰是检验你Java基础功最好的试金石。这篇文章我会以自己前段时间重写的一版为例,把从数据库设计、界面搭建到借书还书核心逻辑的完整链路拆开讲,适合正在做课程设计、毕业设计,或者想通过一个桌面项目把Java基础(集合、JDBC、事件监听、线程)串起来的人。
1. 为什么做图书管理系统不直接上Web,而是用Java GUI
1.1 GUI桌面应用的真实使用场景
很多人一上来就问:现在图书管理不都往Web、移动端走吗,做GUI是不是过时了?这个说法对,但也不完全对。校内图书馆、小型资料室、社区书屋、单位档案室,这类场景用户量不大,可能只有几个管理员加几十个读者,部署要求很简单——一台Windows电脑就够。这种情况下,一个Java Swing桌面程序反而比一套Spring Boot加Vue的系统更实用:不用装Tomcat、不用配Nginx、不用考虑跨域,双击jar包就能跑,数据落在本地MySQL里,备份直接把数据文件拷走就行。
所以我的建议是:动手之前先想清楚自己的核心诉求。如果是为了练后端接口、分布式、微服务那套,Web项目当然好;如果是为了把Java基础知识扎扎实实过一遍,验证自己对面向对象、集合框架、JDBC、事件驱动模型的理解,GUI版的图书管理系统是性价比非常高的选择。
当然,GUI版也有它要补充的学问:窗口布局、组件事件监听、表格数据模型、耗时任务与界面线程的关系。这些在Web开发里被框架掩盖的问题,在桌面端全都得自己处理,而这恰恰是这个项目最有价值的地方。
1.2 Swing还是JavaFX:我为什么选Swing
Java做桌面GUI主要两条路:Swing和JavaFX。我做这版的时候最终选了Swing配合FlatLaf外观库的方案,原因有三个:
- 零额外依赖:Swing是JDK自带的,不用引入任何外部框架,环境上省了很多麻烦,对刚接触Java的人尤其友好。
- 资料足够多:从Stack Overflow到各类技术社区,Swing相关问题基本都能搜到答案,踩坑了很快能找到解决方案。
- JavaFX从JDK 11开始被拆成独立模块:虽然可以引入,但对一个课程设计、毕设项目来说,增加的配置成本大于收益。
但是如果你的项目强调界面现代感、大量自定义组件和动画效果,我会建议考虑JavaFX。Swing的默认外观确实"老气",不过配合FlatLaf这个第三方外观库之后,界面观感会好很多,后面我详细说配置方法。
这版项目整体技术栈如下:
JDK 1.8+ Swing + FlatLaf MySQL 5.7 / 8.0 JDBC + 自封装DAO层 Druid连接池 Maven构建2. 表结构设计:从借书还书场景反推数据模型
很多人在设计表的时候习惯先把实体列出来,然后凭感觉加字段。我的做法不一样——先画业务流程:管理员登录、录入图书、读者办卡、读者借书、图书库存减少、读者还书、图书库存恢复、超期计算罚款。根据这些流程,表结构会非常清晰。
2.1 核心表和字段
这版系统总共设计了5张表:
| 表名 | 关键字段 | 说明 |
|---|---|---|
| category | category_id, category_name | 图书分类,分类单独提出来方便做统计 |
| book | book_id, isbn, book_name, author, publisher, category_id, total_stock, stock, location, price, status, create_time | 图书主表,stock是当前可借库存 |
| reader | reader_id, card_no, name, phone, email, status, create_time | 读者表,card_no作为借书证号,加唯一索引 |
| borrow | borrow_id, book_id, reader_id, borrow_time, due_time, return_time, status, fine | 借阅记录表,status 0在借、1已还 |
| admin | admin_id, username, password, real_name | 管理员表 |
一个容易被忽略的点:图书表里要有ISBN字段,虽然它不参与核心借还逻辑,但在图书查重和录入时非常有用。同一本书如果有多个副本,我采用的是"一条记录加库存数量"的模式,而不是一本书记录一行。这样展示列表时更直观,查询也更快。
2.2 借阅表里的唯一约束设计
借还书最容易出的bug是:同一个人同时借了同一本书两本。为了不让代码里堆满if判断,我的思路是在borrow表上做业务规则前置校验:同一读者、同一本书、status=0(未还)只能有一条记录。插入数据前先执行一次查询:
SELECT COUNT(*) FROM borrow WHERE book_id = ? AND reader_id = ? AND status = 0如果返回大于0,直接提示"你已借过这本书,不能重复借阅"。这个判断放在Service层而不是界面监听器里,后面界面怎么改都不用动业务规则,这也是分层设计的好处之一。
2.3 为什么不建议用存储过程
有同学喜欢把借书整个流程写成存储过程,一个CALL搞定。我的观点是:学习阶段不建议。存储过程把业务逻辑藏在了数据库内部,出了问题不好排查,而且换数据库时需要重写。把这套逻辑放进Java的Service层,用事务去控制,既能练到编程基本功,也方便在IDE里打断点调试。项目答辩时,面试官也更想看你在Java层面如何处理事务边界,而不是一条SQL写完。
3. 界面模块拆解:从登录窗体到主操作台的搭建顺序
3.1 登录窗口的设计
登录窗口虽然简单,但有两件事值得注意。
第一,密码不要明文存储。虽然这只是本地MySQL上的小系统,但从一开始就养成习惯,密码在入库前用MD5加盐哈希。JDK里的MessageDigest就能做,不需要额外引入加密框架。
第二,登录成功后不要手动new第二个窗口了事,最好做一个简单的窗口管理器。我的做法是定义一个静态方法,在切换窗口时统一处理:关闭当前窗口、设置新窗口居中、调用setVisible(true)。这个习惯在项目变大之后会省很多事——窗口多了之后,到处直接setVisible很容易出现多个窗口叠在一起的问题。
登录成功之后,主窗口我用的是JFrame配JDesktopPane和JInternalFrame的组合:顶部是菜单栏,左侧是功能导航,右侧内容区放各个内部子窗口。用JDesktopPane的好处是各个功能模块之间独立,用户能同时打开借阅列表和图书详情对比着看,操作体验比单面板结构自然很多。
3.2 JTable不直接塞数据,中间要过数据模型
很多新手写图书列表,都是拿到ResultSet之后逐行打印,或者直接把Vector塞进JTable。这样做的后果是:数据一旦变化,表格不刷新。正确做法是自定义一个继承AbstractTableModel的模型类,在里面持有数据集合,对外暴露getColumnCount、getRowCount、getValueAt等方法。每次数据变更之后调用fireTableDataChanged()通知表格刷新。
public class BookTableModel extends AbstractTableModel { private final String[] COLUMNS = {"编号", "书名", "作者", "出版社", "库存", "位置"}; private List<Book> bookList = new ArrayList<>(); public void setBooks(List<Book> books) { this.bookList = books; fireTableDataChanged(); // 关键:通知视图刷新 } @Override public int getRowCount() { return bookList.size(); } @Override public int getColumnCount() { return COLUMNS.length; } @Override public Object getValueAt(int rowIndex, int columnIndex) { Book b = bookList.get(rowIndex); switch (columnIndex) { case 0: return b.getBookId(); case 1: return b.getBookName(); case 2: return b.getAuthor(); case 3: return b.getPublisher(); case 4: return b.getStock(); case 5: return b.getLocation(); default: return ""; } } }这里有个性能细节:不要在Swing的事件回调线程里直接执行耗时操作,比如查数据库。如果图书表有几万条数据,在事件回调里同步查询,窗口会卡成"未响应"状态。我用SwingWorker把查询放到后台线程执行,查完之后再调用setBooks更新界面,窗口始终流畅。
如果项目只有几千条数据,同步查询问题不大;但一旦要做搜索、过滤、导出,线程模型早晚要用到,所以建议从一开始就把这类耗时任务用SwingWorker包一层。
4. 核心功能代码拆解:图书查询、借书、还书的判断逻辑
4.1 图书查询:模糊搜索要做的两件事
图书查询除了按书名精确匹配,我加了按书名模糊、按ISBN精确、按分类筛选三种方式。SQL用动态拼接:
StringBuilder sql = new StringBuilder("SELECT * FROM book WHERE 1=1"); List<Object> params = new ArrayList<>(); if (StringUtils.isNotBlank(bookName)) { sql.append(" AND book_name LIKE CONCAT('%', ?, '%')"); params.add(bookName); } if (StringUtils.isNotBlank(isbn)) { sql.append(" AND isbn = ?"); params.add(isbn); } if (categoryId != 0) { sql.append(" AND category_id = ?"); params.add(categoryId); }这里用1=1只是为了让拼接逻辑清晰,真正的SQL注入防御靠PreparedStatement的?占位符解决。不要图省事做字符串直接拼接,尤其是用户输入框的内容,这属于基本功,但每次都说每次依旧有人踩。
4.2 借书流程:事务边界和库存校验的顺序
借书的完整逻辑是:
- 查询图书,判断库存stock是否大于0
- 查询借阅表,判断读者是否重复在借同一本书
- 插入一条borrow记录,状态status=0,设置应还时间due_time
- 更新book表的stock减1
- 第2、3、4步任何一步失败,整体回滚
这里有一个容易踩坑的顺序问题:是先减库存还是先插入借阅记录?我的实践经验是先插入借阅记录,再减库存。原因是:如果先减库存后插入借阅记录,极端情况下两条操作中间程序崩溃,会出现"库存少了但借阅记录没生成"的现象;而先插入记录再减库存,即使崩溃,也只是多了一条在借记录,管理员还能通过后台人工处理。
事务控制放在Service层:
public boolean borrowBook(int bookId, int readerId, int borrowDays) throws Exception { Connection conn = dataSource.getConnection(); boolean originAutoCommit = conn.getAutoCommit(); try { conn.setAutoCommit(false); // 1. 校验库存 // 2. 校验重复借阅 // 3. 插入借阅记录 // 4. 更新库存 conn.commit(); return true; } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(originAutoCommit); conn.close(); } }注意finally里要把autocommit恢复成原值,否则连接归还到连接池后状态是错的,下一个请求会踩到非常隐蔽的坑,后面第5章我会详细说。
4.3 还书流程:逾期罚款的计算与展示
还书比借书多一些业务规则:
- 检查borrow表里status=0且book_id和reader_id匹配的记录
- 更新return_time为当前时间
- 计算是否逾期:如果returnTime大于dueTime,逾期天数等于当前时间减dueTime,罚款等于天数乘日罚款金额
- 更新图书stock加1
- 更新borrow记录的status为1,同时写入fine金额
日期比较逻辑要特别小心。我一开始用的是字符串比较dueDate,后来发现SimpleDateFormat在不同时区下格式化的结果不准确,干脆统一用java.time.LocalDate:
LocalDate dueDate = LocalDate.now().plusDays(borrowDays); if (LocalDate.now().isAfter(dueDate)) { long days = ChronoUnit.DAYS.between(dueDate, LocalDate.now()); fine = (int) days * DAILY_FINE; }用java.time包之后,日期加减、比较的代码简洁很多,也基本不用考虑线程安全问题,即使你的JDK还停留在1.8,LocalDate也是完全可用的。逾期状态在界面上别藏得太深,我的还书窗口会直接用表格展示"已逾期N天,罚款X元",管理员一眼就能看到。
5. 踩过的坑和调试经验:这4个问题最消耗时间
做这个项目过程中我记录了4个最典型的坑,每一个都至少花了我一晚上。
5.1 JTable点完按钮不刷新
最经典的问题:新增图书成功后,表格没有变化。原因就是前面说的,没有在Model层调用fireTableDataChanged()。有的资料会说用table.updateUI(),这个方法确实能强制性刷新,但它会让表头重新渲染、选中状态丢失,属于"大力出奇迹"的方案,实际项目里不建议。
正确的排查思路是:先确认数据是否真的写进数据库了,再确认Model层的集合是否更新了,最后确认是否调用了刷新通知方法。按这个顺序排查,三分钟就能定位。
5.2 数据库中文乱码
一条数据在MySQL命令行里插入正常,但Java程序插入后中文全变问号。这个问题的排查顺序是:
- 先看数据库、表字符集是否是utf8mb4:
SHOW CREATE TABLE book;- 再看连接URL是否指定编码:
jdbc:mysql://localhost:3306/library?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai- 最后看IDE控制台编码,Windows下如果是GBK,能看到日志里中文乱码。
我遇到的情况是连接URL漏了characterEncoding参数,导致从Java端写入的中文变成乱码,补上参数后重新插入就好了。
5.3 连接不关闭导致的连接池耗尽
这是很多同学会犯的错:定义一个DBHelper类,一个静态方法返回Connection,用的时候调用,用完不关。连接不关闭,轻则连接池耗尽报错,重则整个应用连接不上数据库。我的建议是封装一个通用的数据库操作模板,或者在每个DAO方法里用try-with-resources:
try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { // 处理结果 } catch (SQLException e) { // 处理异常 }Druid连接池本身有连接回收机制,但养成手动释放资源的习惯永远不会错,因为这套资源管理逻辑在别的框架里同样适用。
5.4 删除图书时外键约束冲突
一开始图书表没做外键,删除图书很顺利。后来加上外键约束后,删除一本被借过的书直接报错:
Cannot delete or update a parent row: a foreign key constraint fails遇到这种报错,很多人第一反应是把外键删掉。我的做法是:业务上定义"删除图书只能删除没有被借阅过的书",删除前先检查borrow表是否有关联记录。如果有,提示"该书存在在借或历史借阅记录,不允许删除,可以改为下架"。用逻辑删除代替物理删除,既保留了数据完整性,又不会让外键卡住日常操作。下架状态在book表里用status字段区分,0下架、1在架,这样比物理删行安全得多。
6. 从"能跑"到"能用"的进阶打磨方向
当一个图书管理系统已经能完成增删改查之后,我会建议继续往这几个方向打磨,答辩和面试时都很有话说:
- 导出功能:用Apache POI把借阅列表导出为Excel,这是管理类系统非常实用的功能,也是面试官很爱问的实战点。
- 图书条形码、借书证号生成:用Zxing生成二维码或条形码图片,让图书编号和借书证号可以扫码录入,项目质感瞬间提升一个档次。
- 统计报表:用JFreeChart画每月借阅量柱状图、图书分类占比饼图。统计功能看起来简单,但需要在SQL里写GROUP BY和聚合函数,能体现数据库功底。
- 界面主题切换:FlatLaf不只提供默认样式,还支持换主题。加一个深色模式切换,代码量不大,演示效果很好。
我个人在项目答辩里比较喜欢展示借书还书的事务一致性和并发借阅时的库存准确性问题,因为这两个点比界面好看更能体现你对业务的理解。比如两个人同时点击借同一本只剩一本的书,系统能否保证只有一个人成功,这就是一个非常实在的并发控制问题,也是把项目从"会写"提升到"会思考"的分水岭。
做完这个项目后,我最大的体会是:界面是面子,数据一致性是里子。Java GUI版图书管理系统是一个同时练到面子——Swing布局、组件事件,和里子——JDBC事务、并发控制、分层设计的好项目。做完一遍再回头去看Java面试题里的集合框架、异常处理、多线程相关内容,会有一种"哦,原来是这么回事"的顿悟感。这大概就是这个老项目到现在依然值得做一遍的原因。
本文还有配套的精品资源,点击获取