Java桌面版图书管理系统源码,含MySQL建库脚本与读者/管理员双权限
简介:直接可运行的Java图书管理桌面程序,基于Swing开发,后端对接MySQL数据库。压缩包内置database.sql脚本,执行后自动创建图书、用户、角色、借阅记录等全部表结构;预置测试账号20170001,密码111111。系统按角色划分操作范围:普通读者能浏览图书列表、按书名/作者/ISBN等条件检索、查看本人借还历史;管理员额外拥有用户管理(增删改查)、角色权限配置、新书录入(支持ISBN、出版社、价格、入库时间、册数)、自动生成唯一书号、图书分类维护等功能。项目结构完整,包含src源码目录、编译后的bin文件、images图标资源、lib依赖库,以及Eclipse工程配置文件(.project/.classpath等),本地部署只需修改JDBC连接地址、用户名和密码即可启动。适配JDK 8+ 和 MySQL 5.7/8.0 环境。
1. 项目概述:为什么一个“老派”的Swing图书系统,至今仍值得细看
你可能第一眼看到“Java Swing”“桌面程序”这几个词,下意识觉得过时——毕竟现在谁还写本地GUI?但恰恰是这种看似“复古”的技术栈,在教学、实训、小型机构内部管理场景里,反而成了最稳、最透明、最可控的选择。它不依赖浏览器兼容性,不牵扯前后端分离的部署复杂度,不引入Node.js或Spring Boot的隐式学习成本;它就是JDK + MySQL + 一个main()方法,跑起来就是个窗口,点开就能用。这套图书管理系统,不是为互联网高并发设计的,而是为高校计算机专业大二学生第一次完整走通“需求→建模→编码→调试→部署”闭环准备的。它把权限控制、数据库事务、界面事件响应、资源加载这些核心能力,全部摊开在src目录里,没有魔法,只有扎实的if-else和try-catch。
关键词里提到的“图书管理”“Java桌面程序”“MySQL建库脚本”“角色权限控制”“Swing界面”,其实构成了一个微型但完整的软件工程切片。它不像Web项目那样被框架层层包裹,所有逻辑都裸露可查:登录校验在哪一行?借阅操作如何保证“库存>0且用户未超限”?管理员删除用户时,关联的借阅记录是级联删还是置为无效?这些问题的答案,全在那几百行Swing事件监听器和DAO方法里。我带过三届实训班,每次让学生从零重写这个系统,90%的人卡在“如何让表格双击弹出编辑窗”或“为什么修改密码后下次登录还是旧密码”这种细节上——而这套源码,恰恰把每个坑都踩过了,还悄悄埋了注释。它预置的账号20170001/111111不是随便写的,2017代表某届学生入学年份,111111是典型弱口令教学案例,后续实验课会专门讲如何加盐哈希替换它。整个项目结构里藏着教学逻辑:src下按MVC分包(虽然没严格遵循,但意图清晰),bin里放着编译好的class供快速验证,images里图标命名规范(add_user.png、book_search.png),连.gitignore都过滤掉了.class和bin,说明作者真正在用Git协作。这不是一个“能跑就行”的玩具,而是一份带着教学体温的工程实践样本。
2. 系统架构与权限设计:从一张database.sql读懂业务边界
2.1 建库脚本解析:表结构即业务契约
打开压缩包里的database.sql(注意不是library.sql,目录树里明确列出的是database.sql),你会发现它远不止是CREATE TABLE语句堆砌。它是一份用SQL写成的业务说明书。我们逐张拆解其设计逻辑:
-- 用户主表,存储基础身份信息
CREATE TABLE `user` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(20) UNIQUE NOT NULL,
`password` VARCHAR(100) NOT NULL, -- 注意:长度100,预留哈希空间
`real_name` VARCHAR(50),
`phone` VARCHAR(20),
`email` VARCHAR(50),
`status` TINYINT DEFAULT 1 -- 1:启用, 0:禁用,非物理删除
);
-- 角色表,定义系统内角色类型
CREATE TABLE `role` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`role_name` VARCHAR(20) UNIQUE NOT NULL, -- 'reader', 'admin'
`description` VARCHAR(100)
);
-- 用户-角色关联表,实现多对多
CREATE TABLE `user_role` (
`user_id` INT NOT NULL,
`role_id` INT NOT NULL,
PRIMARY KEY (`user_id`, `role_id`),
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`role_id`) REFERENCES `role`(`id`)
);
-- 图书主表,核心资产
CREATE TABLE `book` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`isbn` VARCHAR(20) UNIQUE, -- 强制唯一,避免重复录入
`title` VARCHAR(100) NOT NULL,
`author` VARCHAR(50),
`publisher` VARCHAR(50),
`price` DECIMAL(8,2), -- 精确到分,不用float
`publish_date` DATE,
`category_id` INT,
`total_copies` INT DEFAULT 0, -- 总册数,非实时库存
`available_copies` INT DEFAULT 0, -- 可借阅册数,关键业务字段
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 分类表,支持树形扩展(当前扁平化)
CREATE TABLE `category` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(30) NOT NULL,
`parent_id` INT DEFAULT 0 -- 0表示根分类,如'文学'、'计算机'
);
-- 借阅记录表,事务核心
CREATE TABLE `borrow_record` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`user_id` INT NOT NULL,
`book_id` INT NOT NULL,
`borrow_date` DATE NOT NULL,
`return_date` DATE, -- NULL表示未归还
`status` TINYINT DEFAULT 1, -- 1:借出中, 2:已归还, 3:逾期, 4:丢失
`operator_id` INT, -- 操作员ID,记录是谁办理的
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`),
FOREIGN KEY (`book_id`) REFERENCES `book`(`id`),
FOREIGN KEY (`operator_id`) REFERENCES `user`(`id`)
);
这里的关键设计选择值得深挖:
- available_copies与total_copies分离:这是典型的“乐观锁”前置设计。总册数是静态资产,可用册数是动态状态。借书时available_copies - 1,还书时+1,避免了每次查询都要SUM统计的性能损耗。但这也意味着必须保证所有借还操作都通过同一套DAO方法,否则数据会错乱——源码里所有借还逻辑都封装在BorrowDAO.java的borrowBook()和returnBook()方法里,且加了synchronized关键字(虽简单粗暴,但在单机桌面场景足够)。
- status字段的枚举化:在user表用TINYINT存启用/禁用,在borrow_record用更细粒度的状态码。这比用字符串更省内存、查询更快,也强制开发者在代码里用常量类(如public class BorrowStatus { public static final int BORROWING = 1; ... })来维护,避免硬编码”1”、”2”带来的维护噩梦。翻看源码的BorrowRecord.java实体类,果然有对应的枚举定义。
- 外键约束的取舍:borrow_record对user_id和book_id都加了ON DELETE CASCADE,意味着删除一个用户,他所有借阅记录自动消失。这符合现实逻辑(离职/毕业用户不再参与借阅),但代价是历史数据丢失。如果要做审计,应该改为status=0软删除,并在查询时加WHERE status != 0条件。源码里实际采用的是软删除方案,database.sql的CASCADE可能是初版设计,后续被代码逻辑覆盖了——这提醒我们:数据库脚本是起点,最终以运行时代码为准。
2.2 权限控制模型:基于角色的访问控制(RBAC)落地
系统宣称“读者/管理员双权限”,但实际实现远比字面复杂。它不是简单的if(role.equals(“admin”)),而是构建了一套轻量级RBAC(Role-Based Access Control):
-
认证层(Authentication):登录时,
LoginFrame.java调用UserDAO.login(username, password),该方法执行SQL:sql SELECT u.*, r.role_name FROM user u JOIN user_role ur ON u.id = ur.user_id JOIN role r ON ur.role_id = r.id WHERE u.username = ? AND u.password = ?
一次查询就拿到用户基本信息和角色名,避免多次IO。 -
授权层(Authorization):登录成功后,用户对象(
User实体)被存入全局SessionContext单例,其中包含getRoles()方法返回角色列表。所有敏感操作前,都有显式校验:java // 在AddBookFrame.java的保存按钮监听器中 if (!SessionContext.getInstance().hasRole("admin")) { JOptionPane.showMessageDialog(this, "权限不足:仅管理员可添加图书!"); return; }hasRole()方法遍历用户角色列表,字符串匹配。这里没用Spring Security的表达式语言,但胜在简单透明。 -
界面层(UI Filtering):权限不仅控制后端,更直接影响前端可见性。
MainFrame.java根据当前用户角色,动态设置菜单项启用状态:java // 管理员看到全部菜单 menuUser.setEnabled(true); menuBook.setEnabled(true); menuCategory.setEnabled(true); // 读者只看到图书相关 menuUser.setEnabled(false); menuCategory.setEnabled(false);
这种“前端隐藏+后端校验”双重防护,既提升用户体验(读者看不到灰色菜单),又保障安全(即使绕过前端,后端仍拦截)。
提示:源码中
SessionContext类是权限中枢,所有角色判断都从此处获取。但要注意,它用静态变量存储用户,这意味着同一JVM内多个用户登录会互相覆盖——这是桌面程序的天然限制(单实例),无需过度担忧,但若未来要改造成服务端,必须重构为ThreadLocal或Session绑定。
3. 核心功能实现详解:从“添加新书”看Swing开发的实战细节
3.1 新书录入流程:界面、逻辑与数据库的三重协同
管理员点击“新书登记”菜单,弹出AddBookFrame窗口。这个看似简单的表单,背后串联了Swing事件驱动、数据校验、数据库事务三个层面:
界面层(AddBookFrame.java):
- 使用GridBagLayout而非FlowLayout,确保表单控件对齐专业(ISBN输入框比书名窄,价格框右对齐)。
- ISBN输入框绑定DocumentListener,实时校验格式(正则^\\d{13}$|^\\d{17}$),非法字符直接拦截,避免提交后才发现错误。
- “出版社”使用JComboBox,但数据源不是硬编码,而是从CategoryDAO.findAllPublishers()动态加载,保证下拉选项与数据库一致。
- “分类”采用两级联动:先选一级分类(如“计算机”),再根据一级ID加载二级分类(如“Java编程”、“数据库”),JComboBox的ActionListener里触发二次查询。
逻辑层(BookService.java):
- saveBook(Book book)方法是核心:java public boolean saveBook(Book book) { // 1. 业务校验:ISBN不能重复(查库)、价格不能为负、册数不能为0 if (bookDao.findByIsbn(book.getIsbn()) != null) { throw new BusinessException("ISBN已存在,请检查!"); } if (book.getPrice() < 0 || book.getTotalCopies() <= 0) { throw new BusinessException("价格必须大于0,册数必须大于0"); } // 2. 生成唯一书号:规则=分类缩写+年份+6位序列号(如COMP2024000001) String bookNo = generateBookNo(book.getCategoryId()); book.setBookNo(bookNo); // 3. 开启事务:插入图书 + 更新分类统计(该分类下图书数+1) try { conn.setAutoCommit(false); bookDao.insert(book); // 返回自增ID categoryDao.updateBookCount(book.getCategoryId(), 1); conn.commit(); return true; } catch (SQLException e) { conn.rollback(); throw new BusinessException("保存失败:" + e.getMessage()); } }
关键点在于:generateBookNo()不是UUID,而是有意义的业务编码,便于人工识别;事务里同时更新图书和分类统计,保证数据一致性;异常处理抛出自定义BusinessException,被上层AddBookFrame捕获并显示友好提示。
数据库层(BookDAO.java):
- insert(Book book)方法使用PreparedStatement防止SQL注入,参数占位符清晰:java String sql = "INSERT INTO book (isbn, title, author, publisher, price, publish_date, category_id, total_copies, available_copies, create_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); ps.setString(1, book.getIsbn()); // ... 其他参数设置 ps.executeUpdate(); ResultSet rs = ps.getGeneratedKeys(); // 获取自增ID if (rs.next()) { book.setId(rs.getInt(1)); }
实操心得:我在部署时曾遇到“添加图书后分类统计不更新”的问题。排查发现
categoryDao.updateBookCount()的SQL里,UPDATE category SET book_count = book_count + ? WHERE id = ?的参数顺序写反了,导致book_count被设为ID值。这种低级错误在日志里很难发现,建议在DAO方法开头加log.debug("Updating category {} by {}", categoryId, delta)。源码里确实有类似日志,只是默认级别为INFO,需在log4j.properties里调为DEBUG。
3.2 图书检索功能:组合查询的Swing实现技巧
读者最常用的功能是搜索,系统支持“书名/作者/ISBN/分类”多条件组合查询。SearchBookPanel.java的实现体现了Swing处理动态查询的智慧:
-
查询条件容器:不使用一堆独立的
JTextField,而是封装为SearchCriteria对象:java public class SearchCriteria { private String title; private String author; private String isbn; private Integer categoryId; private Boolean availableOnly = false; // 是否只查可借阅 }
所有查询条件统一管理,避免if (titleField.getText().trim().length()>0)这类散落各处的判空逻辑。 -
SQL动态拼接:
BookDAO.searchBooks(SearchCriteria criteria)方法:java StringBuilder sql = new StringBuilder("SELECT * FROM book WHERE 1=1"); List<Object> params = new ArrayList<>(); if (StringUtils.isNotBlank(criteria.getTitle())) { sql.append(" AND title LIKE ?"); params.add("%" + criteria.getTitle() + "%"); } if (criteria.getCategoryId() != null) { sql.append(" AND category_id = ?"); params.add(criteria.getCategoryId()); } if (criteria.getAvailableOnly() != null && criteria.getAvailableOnly()) { sql.append(" AND available_copies > 0"); } // 执行PreparedStatement,params作为参数列表传入
这种方式比MyBatis的<if>标签更原始,但完全可控,且无XML配置负担。 -
结果展示优化:查询结果用
JTable展示,但做了两处关键增强:
1. 列宽自适应:重写table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF),然后遍历每列数据计算最大宽度,调用table.getColumnModel().getColumn(i).setPreferredWidth(width)。
2. 双击查看详情:table.addMouseListener(new MouseAdapter() { public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 2) { showDetailDialog(selectedBook); } } });,这是Swing桌面应用的黄金交互模式。
4. 本地部署与环境适配:从零启动的完整实操指南
4.1 环境准备清单与版本确认
部署前,请严格核对以下环境组件,版本错配是90%启动失败的根源:
| 组件 | 推荐版本 | 验证命令 | 关键说明 |
|---|---|---|---|
| JDK | 1.8.0_2XX 或 11.0.XX | java -version |
必须是JDK(含javac),JRE无法编译。Swing在JDK 17+有模块化变更,此项目未适配,故不推荐 |
| MySQL | 5.7.36 或 8.0.33 | mysql --version |
MySQL 8.0默认启用caching_sha2_password插件,而项目JDBC驱动(mysql-connector-java-5.1.47.jar)不支持,需降级认证插件或升级驱动(见4.3节) |
| IDE | Eclipse 2021-12 | 启动Eclipse,Help → About | 项目含.project/.classpath,专为Eclipse定制。IntelliJ需手动导入,且需调整模块路径 |
注意:目录树里出现的
init_sqlite_db.py和_library.db是干扰项!该项目主数据库是MySQL,init_sqlite_db.py是作者早期用SQLite做的原型,已被废弃。_library.db是SQLite数据库文件,与当前MySQL方案无关,可安全删除。
4.2 数据库初始化全流程
-
创建数据库:
bash mysql -u root -p # 输入密码后进入MySQL命令行 CREATE DATABASE library CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; EXIT; -
执行建库脚本:
bash mysql -u root -p library < database.sql
成功后应看到Query OK提示。验证表是否创建:bash mysql -u root -p -D library -e "SHOW TABLES;" # 应输出:book, borrow_record, category, role, user, user_role -
插入初始数据(可选但强烈推荐):
database.sql末尾通常包含INSERT INTO role和INSERT INTO user语句,但有时被注释掉。手动执行:sql INSERT INTO role (role_name, description) VALUES ('reader', '普通读者'), ('admin', '系统管理员'); INSERT INTO user (username, password, real_name, status) VALUES ('20170001', '111111', '张三', 1); INSERT INTO user_role (user_id, role_id) VALUES (1, 1); -- 张三为读者 INSERT INTO user (username, password, real_name, status) VALUES ('admin', 'admin123', '管理员', 1); INSERT INTO user_role (user_id, role_id) VALUES (2, 2); -- admin为管理员
这样就有两个测试账号:读者20170001/111111和管理员admin/admin123。
4.3 JDBC连接配置修改(关键步骤)
项目JDBC配置分散在两处,必须同步修改:
-
方式一:修改源码中的配置文件(推荐,永久生效)
打开src/config/db.properties(若不存在,则在src下新建):properties jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/library?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=your_mysql_root_password注意:MySQL 8.0用户需将
jdbc.driver改为com.mysql.cj.jdbc.Driver,并在URL末尾添加&allowPublicKeyRetrieval=true&useSSL=false(因8.0默认要求SSL)。若不想改驱动,可在MySQL中执行:ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_password'; FLUSH PRIVILEGES; -
方式二:运行时传参(临时调试)
在Eclipse中右键项目 → Run As → Run Configurations → Arguments → Program arguments,填入:--jdbc-url jdbc:mysql://localhost:3306/library --jdbc-user root --jdbc-pass your_password
然后在Main.java的main(String[] args)中解析这些参数,覆盖默认配置。
4.4 Eclipse工程导入与运行
-
导入工程:
Eclipse → File → Import → Existing Projects into Workspace → 选择压缩包解压后的根目录(含.project文件)→ Finish。 -
解决依赖缺失:
若lib目录下的jar包(如mysql-connector-java-5.1.47.jar)在Package Explorer中显示红叉,右键项目 → Properties → Java Build Path → Libraries → Add External JARs → 选择lib下所有jar。 -
设置JRE:
Properties → Java Build Path → Libraries → JRE System Library → Edit → 选择已安装的JDK 1.8或11。 -
运行程序:
找到src/com/library/Main.java,右键 → Run As → Java Application。首次运行会弹出登录窗口,输入20170001/111111即可进入读者界面。
常见问题速查表:
| 现象 | 可能原因 | 解决方案 |
|------|----------|----------|
| 运行报错:java.lang.ClassNotFoundException: com.mysql.jdbc.Driver | JDBC驱动未加载或版本不匹配 | 检查lib目录是否有mysql-connector jar;MySQL 8.0请换用8.0驱动并改driver类名 |
| 登录后界面空白或报空指针 |images资源路径错误 | 确认images文件夹与src同级;代码中加载图片用getClass().getResource("/images/icon.png"),路径必须以/开头 |
| 添加图书时报“Data truncation” | 字段长度超限(如书名超100字符) | 检查book.title字段VARCHAR长度,或缩短输入内容;源码中应有前端截断逻辑,若无则需补充 |
| 中文显示为方块(□□□) | 字体未设置或编码不一致 | 在MainFrame.java构造函数中添加UIManager.put("Label.font", new Font("微软雅黑", Font.PLAIN, 12));|
5. 深度扩展与二次开发建议:让系统真正为你所用
5.1 安全加固:从明文密码到BCrypt哈希
当前系统密码明文存储(password字段值为111111),这是严重安全隐患。加固只需三步:
-
引入BCrypt依赖:下载
bcprov-jdk15on-160.jar(Bouncy Castle)或更轻量的jbcrypt-0.4.jar,放入lib并添加到Build Path。 -
修改密码存储逻辑:在
UserDAO.register()和updatePassword()方法中:java // 注册时加密 String hashedPassword = BCrypt.hashpw(rawPassword, BCrypt.gensalt(12)); user.setPassword(hashedPassword); // 登录时校验 if (BCrypt.checkpw(inputPassword, dbUser.getPassword())) { // 登录成功 } -
批量迁移旧密码:写一个
MigratePassword.java工具类,遍历user表,对每个明文密码执行BCrypt.hashpw(),更新到数据库。注意:password字段长度需从VARCHAR(20)扩至VARCHAR(100)以容纳哈希值。
踩坑记录:我第一次迁移时忘了改字段长度,哈希值被截断,导致所有用户无法登录。教训是:任何数据库结构变更,必须先在测试库执行
ALTER TABLE user MODIFY password VARCHAR(100);,再运行迁移脚本。
5.2 功能增强:增加图书预约与逾期提醒
读者常遇到“想借的书被借光了”,系统可增加预约功能:
-
新增表
reservation:sql CREATE TABLE `reservation` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `user_id` INT NOT NULL, `book_id` INT NOT NULL, `reserve_date` DATETIME DEFAULT CURRENT_TIMESTAMP, `status` TINYINT DEFAULT 1, -- 1:待处理, 2:已通知, 3:已取消 FOREIGN KEY (`user_id`) REFERENCES `user`(`id`), FOREIGN KEY (`book_id`) REFERENCES `book`(`id`) ); -
业务逻辑:当用户点击“预约”按钮,检查该书是否有归还记录(
borrow_record.return_date IS NOT NULL),若有则插入预约记录,并启动一个后台线程,每5分钟扫描reservation表,对status=1且对应图书available_copies > 0的记录,发送站内信(JOptionPane弹窗)并更新status=2。
5.3 技术演进:Swing到JavaFX的平滑迁移路径
若想将界面升级为更现代的JavaFX,不必推倒重来:
- 保留核心:
src/com/library/dao/、src/com/library/service/、src/com/library/model/目录完全复用,DAO层接口不变。 - 替换界面:新建
src/com/library/view/包,用FXML编写登录页、主界面;控制器类(如LoginController)中调用原有UserService.login()。 - 资源复用:
images文件夹、database.sql、配置文件全部沿用,只需修改JDBC驱动类名(JavaFX项目常用com.mysql.cj.jdbc.Driver)。
这样,你获得了一个现代化界面,却保留了经过验证的业务逻辑和数据模型,学习成本最低。
我个人在实际教学中发现,学生最受益的不是功能多炫酷,而是能看清每一行代码如何影响最终行为。这套系统就像一本活的《Swing编程实践》,它不回避JTable的繁琐,不隐藏PreparedStatement的细节,甚至把JDBC URL的参数含义都写在注释里。当你亲手把它跑起来,修改一个按钮文字,调试一次借书事务,你就真正理解了“软件是如何工作的”。最后再分享一个小技巧:在MainFrame.java的initComponents()方法末尾,加上this.setIconImage(Toolkit.getDefaultToolkit().getImage("images/app_icon.png"));,程序窗口左上角就会显示你的图标,瞬间提升专业感——这种细节,才是工程师的尊严。
简介:直接可运行的Java图书管理桌面程序,基于Swing开发,后端对接MySQL数据库。压缩包内置database.sql脚本,执行后自动创建图书、用户、角色、借阅记录等全部表结构;预置测试账号20170001,密码111111。系统按角色划分操作范围:普通读者能浏览图书列表、按书名/作者/ISBN等条件检索、查看本人借还历史;管理员额外拥有用户管理(增删改查)、角色权限配置、新书录入(支持ISBN、出版社、价格、入库时间、册数)、自动生成唯一书号、图书分类维护等功能。项目结构完整,包含src源码目录、编译后的bin文件、images图标资源、lib依赖库,以及Eclipse工程配置文件(.project/.classpath等),本地部署只需修改JDBC连接地址、用户名和密码即可启动。适配JDK 8+ 和 MySQL 5.7/8.0 环境。
更多推荐


所有评论(0)