Java Swing MySQL图书管理系统实战指南
简介:本资源是一套基于Java Swing与MySQL开发的GUI图书管理系统,面向高校计算机专业学生及Java初学者,适用于课程设计、实训项目与GUI+数据库综合实践。系统采用双角色架构(学生/管理员),完整覆盖登录、借阅管理、读者与图书增删改查等核心业务流程,技术栈清晰,代码含详细注释并附带课程报告文档。压缩包共184个文件,含35个Java源码、101个编译后Class文件、18张PNG与15张JPG界面资源图、3个SQL建表与初始化脚本、以及Jar依赖、配置文件和Word报告等,整体24.47MB,结构规范便于分模块学习。目前已有601人下载学习,读者可直接导入Eclipse运行调试,快速掌握Swing事件驱动编程、MySQL连接池配置、多角色权限控制逻辑及典型C/S架构实现思路。
1. 为什么现在还要用 Java + Swing + MySQL 做图书管理系统?——不是怀旧,是控场刚需
你可能刚刷完 Spring Boot + Vue 的图书管理 demo,转身就被安排重写一个“纯 Java 桌面版”:不许联网、不许装 Tomcat、客户只认 Windows 7 上双击就能跑的 .jar;管理员不会配 Nginx,但能熟练拖拽 Excel 表格进文件夹;借阅记录要实时写入本地数据库,且断电重启后不能丢一条——连 SQLite 都被否了,理由是“怕别人用 DB Browser 看走数据”。这就是 Java + Swing + MySQL 图书管理系统的典型生存现场:它不是技术栈的退化,而是对环境不可控性、数据主权确定性、运维零门槛三重约束下的精准解法。它适合中小学校图书室、社区文化站、企业内部资料室这类场景:没有专职 IT 运维,但要求系统像 Word 一样“打开即用、关机即停、重装系统后数据还在”。Swing 不是过时,是把 UI 渲染逻辑锁死在 JVM 里;MySQL 不是大材小用,是用事务隔离级别(REPEATABLE READ)兜住并发借还的原子性;Java 不是慢,是让 classloader 能把 JDBC 驱动、连接池、业务逻辑全打包进一个 jar——这才是真正意义上的“单文件部署”。如果你正被这类需求堵在工位上,这篇笔记就是你今晚能跑通、明天能交付、下周能加功能的实操路径。
2. 从零搭起可运行骨架:JDK 11+MySQL 8.0+Swing 工程结构设计
2.1 JDK 与 MySQL 版本选型:为什么必须是 JDK 11 和 MySQL 8.0?
别用 JDK 8 —— 它默认禁用 TLS 1.2,而 MySQL 8.0 默认强制 TLS 加密握手,连不上会卡在Communications link failure;也别用 MySQL 5.7 —— 它的utf8mb4排序规则默认是utf8mb4_general_ci,而 Java String 的compareTo()在中文排序时会和 MySQLORDER BY结果不一致(比如“张三”排在“李四”前面,但 Swing JTable 显示却相反)。我们锁定JDK 11.0.22(LTS) + MySQL 8.0.33(LTS)组合:JDK 11 内置java.net.http可替代老旧的 Apache HttpClient,MySQL 8.0 的utf8mb4_0900_as_cs排序规则能严格匹配 Java 的 Unicode 排序逻辑。安装时注意两个硬性检查点:
java -version输出必须含11.0.22,且JAVA_HOME指向 JDK 根目录(非 JRE);mysql --version输出必须含8.0.33,且my.cnf中明确配置:
[mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_0900_as_cs default-authentication-plugin = mysql_native_password提示:MySQL 8.0 默认认证插件是
caching_sha2_password,而 JDBC Connector/J 8.0.33 以下版本不兼容,必须显式切回mysql_native_password,否则DriverManager.getConnection()会抛Access denied for user。
2.2 工程目录结构:拒绝“src 下堆满 .java”的野蛮生长
一个可维护的图书管理系统,目录必须按职责分层,而非按技术分层。我坚持用以下结构(Maven 项目):
src/ ├── main/ │ ├── java/ │ │ └── com/example/library/ # 根包名,按公司/项目命名 │ │ ├── model/ # POJO:Book、BorrowRecord、User │ │ ├── dao/ # 数据访问:BookDao、BorrowDao(含 SQL 字符串) │ │ ├── service/ # 业务逻辑:BookService(含借阅校验、库存扣减) │ │ ├── ui/ # Swing 组件:MainView、BookListPanel、LoginDialog │ │ └── util/ # 工具类:DBUtil(连接池封装)、DateUtil(格式化) │ └── resources/ │ ├── db.properties # 数据库连接参数(driver、url、user、password) │ └── log4j2.xml # 日志配置(避免 System.out.println 污染控制台) └── test/ └── java/... # 单元测试(重点测 Service 层事务边界)关键设计原则:
- DAO 层不暴露 Connection:所有 DAO 方法接收
Connection参数(由 Service 层统一管理事务),避免new BookDao().save()导致连接泄漏; - UI 层不碰数据库:
BookListPanel只负责渲染List<Book>,数据由BookService.listAll()提供,禁止在actionPerformed()里直接写INSERT INTO; - 资源文件外置化:
db.properties不打进 jar,运行时通过-Dconfig.path=conf/db.properties指定路径,方便不同环境切换账号密码。
2.3 最小可运行 Swing 主窗体:12 行代码启动带菜单栏的框架
不要一上来就画借书界面,先确保 Swing 线程模型和事件调度正确。以下是最简主窗体(MainView.java),它解决三个核心问题:EDT 线程安全、窗口关闭行为、菜单栏占位:
public class MainView extends JFrame { public MainView() { setTitle("图书管理系统 v1.0"); setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); // 关键!禁用默认关闭,交由业务逻辑处理 setLayout(new BorderLayout()); // 创建菜单栏(占位,后续填充功能) JMenuBar menuBar = new JMenuBar(); JMenu fileMenu = new JMenu("文件"); JMenuItem exitItem = new JMenuItem("退出"); exitItem.addActionListener(e -> { if (JOptionPane.showConfirmDialog(this, "确认退出?", "提示", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) { System.exit(0); } }); fileMenu.add(exitItem); menuBar.add(fileMenu); setJMenuBar(menuBar); // 设置窗口大小与居中 setSize(1024, 768); setLocationRelativeTo(null); setVisible(true); } public static void main(String[] args) { // 强制 Swing 在 EDT 线程启动 SwingUtilities.invokeLater(() -> new MainView()); } }逻辑说明:
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE)是血泪经验——若设为EXIT_ON_CLOSE,用户点右上角 × 会直接杀进程,未保存的借阅记录、未提交的数据库事务全部丢失;SwingUtilities.invokeLater()确保所有 Swing 组件创建在事件调度线程(EDT),避免多线程修改 UI 导致IllegalStateException;setSize(1024, 768)是最低安全分辨率,低于此值会导致JTable列宽计算异常(Swing 在 DPI 缩放下对像素计算不敏感,固定尺寸最稳)。
3. 数据库建模与 JDBC 连接池:用 HikariCP 把 MySQL 连接管到死
3.1 图书核心表设计:为什么 borrow_record 表必须有 borrow_time 和 return_time 两字段?
很多初学者把借阅记录设计成“一张表存借还”,结果在统计“当前未归还图书数”时翻车。正确做法是:borrow_record 表只存借阅动作,归还动作另存一条记录。以下是经生产验证的三张核心表:
| 表名 | 字段(关键部分) | 说明 |
|---|---|---|
book | id BIGINT PRIMARY KEY AUTO_INCREMENT,isbn VARCHAR(13) UNIQUE,title VARCHAR(200) NOT NULL,author VARCHAR(100),stock INT DEFAULT 0 CHECK(stock >= 0) | stock必须带CHECK约束,防止手动 UPDATE 导致负库存 |
user | id BIGINT PRIMARY KEY AUTO_INCREMENT,card_no VARCHAR(20) UNIQUE NOT NULL,name VARCHAR(50) NOT NULL,role ENUM('admin','librarian','reader') DEFAULT 'reader' | card_no作为读者唯一标识,比username更符合图书馆实际(学生证号/工号) |
borrow_record | id BIGINT PRIMARY KEY AUTO_INCREMENT,book_id BIGINT NOT NULL,user_id BIGINT NOT NULL,borrow_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,return_time DATETIME NULL,status TINYINT DEFAULT 0 COMMENT '0:借出,1:已还' | return_time允许 NULL,status用于快速筛选,避免WHERE return_time IS NULL全表扫描 |
注意:
borrow_record表必须建复合索引INDEX idx_book_user_status (book_id, user_id, status),否则“查询某读者所有未归还记录”会触发全表扫描。
3.2 HikariCP 连接池初始化:5 行代码搞定高并发下的连接复用
别手写Connection管理——Swing 桌面应用虽无百万并发,但借书高峰期(如开学季)10 个管理员同时操作,连接泄漏会直接导致 MySQLmax_connections耗尽。HikariCP 是目前性能最优的连接池,初始化代码如下:
public class DBUtil { private static HikariConfig config; private static HikariDataSource dataSource; static { config = new HikariConfig("src/main/resources/db.properties"); // 自动加载 properties config.setConnectionTestQuery("SELECT 1"); // MySQL 8.0 必须设健康检查 SQL config.setConnectionTimeout(3000); // 获取连接超时 3 秒 config.setIdleTimeout(600000); // 连接空闲 10 分钟后回收 config.setMaxLifetime(1800000); // 连接最大存活 30 分钟(避开 MySQL wait_timeout) dataSource = new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); // 复用连接池,非每次 new } }参数说明:
setConnectionTestQuery("SELECT 1"):MySQL 8.0 的validationQuery必须是SELECT 1,SELECT 1;或其他带分号的写法会报语法错误;setMaxLifetime(1800000):MySQL 默认wait_timeout=28800(8 小时),但桌面应用常驻内存,连接存活太久易被防火墙中断,设为 30 分钟最稳妥;setIdleTimeout(600000):Swing 应用用户操作间隔长,空闲连接不及时回收会堆积,10 分钟是平衡点。
3.3 DAO 层 CRUD 模板:用 PreparedStatement 防止 SQL 注入的最小实践
Swing 输入框内容直接拼 SQL 是自杀行为。以下BookDao的listByKeyword方法展示如何安全传参:
public class BookDao { public List<Book> listByKeyword(String keyword) throws SQLException { String sql = "SELECT id, isbn, title, author, stock FROM book " + "WHERE title LIKE ? OR author LIKE ? OR isbn LIKE ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { // 占位符赋值:自动转义特殊字符(% _ \) String likeParam = "%" + keyword.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + "%"; ps.setString(1, likeParam); ps.setString(2, likeParam); ps.setString(3, likeParam); ps.setString(4, "\\"); // 设置转义符 try (ResultSet rs = ps.executeQuery()) { List<Book> books = new ArrayList<>(); while (rs.next()) { Book book = new Book(); book.setId(rs.getLong("id")); book.setIsbn(rs.getString("isbn")); book.setTitle(rs.getString("title")); book.setAuthor(rs.getString("author")); book.setStock(rs.getInt("stock")); books.add(book); } return books; } } } }关键细节:
ps.setString()自动处理'单引号转义,无需手动replaceAll("'", "''");LIKE查询必须手动处理%_转义,否则用户搜a_b会匹配a1ba2b;try-with-resources保证ResultSet和PreparedStatement在 finally 块自动关闭,避免游标泄漏。
4. Swing UI 与业务逻辑解耦:用 Observer 模式让借书按钮不写 SQL
4.1 MVC 拆分:为什么 BookListPanel 不能持有 BookService 实例?
常见错误是让 UI 类直接调用bookService.borrowBook(bookId, userId),结果导致:
- 测试困难:
BookListPanel依赖BookService,单元测试需 mock 整个服务链; - 状态混乱:借书成功后
JTable刷新逻辑散落在actionPerformed()里,下次加“预约功能”就得重写按钮逻辑。
正确做法是引入Observer 模式:UI 作为观察者,Service 作为被观察者,状态变更通过事件通知:
// 定义事件类型 public enum BusinessEventType { BOOK_BORROWED, BOOK_RETURNED, BOOK_ADDED } // 事件对象 public class BusinessEvent { private BusinessEventType type; private Object data; // 携带 Book 或 BorrowRecord 对象 // getter/setter... } // 观察者接口 public interface BusinessEventListener { void onBusinessEvent(BusinessEvent event); } // BookListPanel 实现监听器 public class BookListPanel extends JPanel implements BusinessEventListener { private JTable table; private BookTableModel tableModel; // 自定义 TableModel,封装 List<Book> @Override public void onBusinessEvent(BusinessEvent event) { if (event.getType() == BusinessEventType.BOOK_BORROWED) { Book borrowedBook = (Book) event.getData(); // 仅更新 UI:刷新表格、弹提示框、禁用借书按钮 tableModel.refreshData(BookService.getInstance().listAll()); // 重新查库 JOptionPane.showMessageDialog(this, "借阅成功:" + borrowedBook.getTitle()); } } }这样,BookService.borrowBook()方法只需专注业务:校验库存、扣减 stock、插入 borrow_record,完成后发事件new BusinessEvent(BusinessEventType.BOOK_BORROWED, book),UI 自动响应。
4.2 JTable 表格渲染优化:解决中文列宽自动撑开、数字右对齐的玄学问题
SwingJTable默认对中文支持极差:列宽计算基于英文字体宽度,导致“作者”列显示不全;数字列左对齐看着像 bug。解决方案是重写TableCellRenderer:
public class BookTableRenderer extends DefaultTableCellRenderer { @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { Component c = super.getTableCellRendererComponent( table, value, isSelected, hasFocus, row, column); // 所有列:字体统一为微软雅黑,解决中文模糊 c.setFont(new Font("Microsoft YaHei", Font.PLAIN, 12)); // 第 4 列(库存):数字右对齐 if (column == 4 && value instanceof Number) { setHorizontalAlignment(RIGHT); } else { setHorizontalAlignment(LEFT); } // 奇数行背景色(提升可读性) if (!isSelected) { c.setBackground(row % 2 == 0 ? Color.WHITE : new Color(245, 245, 245)); } return c; } }应用方式:
JTable table = new JTable(tableModel); table.setDefaultRenderer(Object.class, new BookTableRenderer()); // 关键:设置列宽自动调整(非手动 setColumnWidth) table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); for (int i = 0; i < table.getColumnCount(); i++) { TableColumn col = table.getColumnModel().getColumn(i); col.setPreferredWidth(col.getHeaderValue().toString().length() * 12 + 20); // 中文字宽≈12px }提示:
AUTO_RESIZE_OFF是必须的,否则setPreferredWidth()无效;中文列宽按字符数 × 12px 计算,比getPreferredSize()更稳定。
4.3 登录对话框的线程安全:为什么 JOptionPane.showInputDialog 不能替代 LoginDialog?
新手常犯错误:用JOptionPane.showInputDialog弹登录框,结果输入密码时按回车,整个 Swing 界面卡死。根本原因是showInputDialog是模态对话框,但其内部未做 EDT 线程保护。正确做法是自定义LoginDialog:
public class LoginDialog extends JDialog { private JTextField usernameField; private JPasswordField passwordField; private volatile boolean loginSuccess = false; public LoginDialog(Frame owner) { super(owner, "用户登录", true); // true 表示模态 setLayout(new GridLayout(3, 2, 10, 10)); add(new JLabel("用户名:")); usernameField = new JTextField(); add(usernameField); add(new JLabel("密码:")); passwordField = new JPasswordField(); add(passwordField); JButton loginBtn = new JButton("登录"); loginBtn.addActionListener(e -> { // 后台线程校验,避免阻塞 EDT new SwingWorker<Boolean, Void>() { @Override protected Boolean doInBackground() throws Exception { String username = usernameField.getText().trim(); String password = new String(passwordField.getPassword()); return UserService.validateLogin(username, password); } @Override protected void done() { try { if (get()) { loginSuccess = true; dispose(); // 关闭对话框 } else { JOptionPane.showMessageDialog(LoginDialog.this, "用户名或密码错误"); } } catch (Exception ex) { JOptionPane.showMessageDialog(LoginDialog.this, "登录失败:" + ex.getMessage()); } } }.execute(); }); add(loginBtn); pack(); setLocationRelativeTo(owner); } public boolean isLoginSuccess() { return loginSuccess; } }核心机制:
SwingWorker将耗时的数据库校验放到后台线程,EDT 保持响应;volatile boolean loginSuccess保证多线程读写可见性;dispose()安全关闭对话框,比setVisible(false)更彻底(释放资源)。
5. 避坑指南:Swing + MySQL 图书管理系统上线前必踩的 4 个坑
5.1 现象:Swing 界面在 Windows 10 高 DPI 屏幕上文字模糊、按钮错位
原因:Swing 默认不启用 DPI 感知,JVM 以 96 DPI 渲染,Windows 强制缩放导致像素拉伸。
解决:启动时添加 JVM 参数-Dsun.java2d.uiScale=1.0,并在main()开头强制设置:
public static void main(String[] args) { // 必须在 SwingUtilities.invokeLater 前设置 System.setProperty("sun.java2d.uiScale", "1.0"); SwingUtilities.invokeLater(() -> new MainView()); }注意:
-Dsun.java2d.uiScale=1.0参数在 JDK 9+ 有效,JDK 8 需用-Dsun.java2d.dpiaware=true,但后者兼容性差,建议升级 JDK。
5.2 现象:MySQL 插入中文时报错Incorrect string value: '\xE4\xB8\xAD\xE5\x9B\xBD...' for column 'title'
原因:MySQL 表字符集是utf8(实际是utf8mb3),不支持 emoji 和部分生僻汉字(如“䶮”)。
解决:
- 修改表字符集:
ALTER TABLE book CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_as_cs; - JDBC URL 添加参数:
jdbc:mysql://localhost:3306/library?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai - 验证:执行
SHOW VARIABLES LIKE 'character_set%';确保character_set_client、character_set_connection、character_set_database全为utf8mb4。
5.3 现象:借书操作后,JTable 数据没刷新,但数据库已更新
原因:Swing 的TableModel未触发fireTableDataChanged(),或JTable未绑定新数据源。
解决:
- 若用
DefaultTableModel,调用model.setRowCount(0); for (Book b : books) model.addRow(...)后必须model.fireTableDataChanged(); - 若用自定义
AbstractTableModel,在setData(List<Book> newData)方法末尾加fireTableDataChanged(); - 关键检查点:确认
JTable.setModel()只调用一次(在构造时),后续只更新TableModel内部数据,而非反复setModel(new Model())。
5.4 现象:打包成 jar 后,程序启动报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver
原因:Maven 打包时未将 MySQL 驱动加入 jar,或MANIFEST.MF中Class-Path未指向驱动 jar。
解决:
- Maven
pom.xml中使用maven-shade-plugin打 fat jar:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.4.1</version> <executions> <execution> <phase>package</phase> <goals><goal>shade</goal></goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.example.library.ui.MainView</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin>- 执行
mvn clean package,生成target/library-1.0-SNAPSHOT.jar,该 jar 已内嵌mysql-connector-java-8.0.33.jar。
6. 进阶技巧:用 JXLayer 实现借书操作的全局遮罩与防重复提交
6.1 为什么需要遮罩层?——解决“用户狂点借书按钮导致多条重复记录”
Swing 没有 Web 的disabled属性天然防抖,用户网络稍慢时连续点击“借书”,actionPerformed()会被触发多次,而BookService.borrowBook()若未加分布式锁,就会插入多条borrow_record。最佳实践是用JXLayer(来自 SwingX 库)在操作期间覆盖整个窗体:
<!-- pom.xml 添加 SwingX 依赖 --> <dependency> <groupId>org.swinglabs.swingx</groupId> <artifactId>swingx-all</artifactId> <version>1.6.5-1</version> </dependency>遮罩实现代码:
public class OverlayUtils { public static void showOverlay(Component parent, String message) { JLayer<JPanel> layer = new JLayer<>(parent, new OverlayPainter(message)); layer.setPreferredSize(parent.getSize()); layer.setBounds(parent.getBounds()); parent.getParent().add(layer, Integer.MAX_VALUE); // 置顶 parent.getParent().repaint(); } private static class OverlayPainter extends LayerUI<JPanel> { private String message; public OverlayPainter(String message) { this.message = message; } @Override public void paint(Graphics2D g2, JComponent comp, int width, int height) { super.paint(g2, comp, width, height); // 半透明黑色背景 g2.setColor(new Color(0, 0, 0, 150)); g2.fillRect(0, 0, width, height); // 白色文字居中 g2.setColor(Color.WHITE); g2.setFont(new Font("Microsoft YaHei", Font.BOLD, 16)); FontMetrics fm = g2.getFontMetrics(); int x = (width - fm.stringWidth(message)) / 2; int y = (height - fm.getAscent() + fm.getDescent()) / 2; g2.drawString(message, x, y); } } }在借书按钮点击时调用:
borrowBtn.addActionListener(e -> { OverlayUtils.showOverlay(mainFrame, "正在处理借书请求..."); new SwingWorker<Void, Void>() { @Override protected Void doInBackground() throws Exception { BookService.getInstance().borrowBook(selectedBookId, currentUserId); return null; } @Override protected void done() { try { get(); // 等待完成 JOptionPane.showMessageDialog(mainFrame, "借阅成功!"); } catch (Exception ex) { JOptionPane.showMessageDialog(mainFrame, "借阅失败:" + ex.getMessage()); } finally { // 移除遮罩(需反射获取 JLayer) Component[] comps = mainFrame.getContentPane().getComponents(); for (Component c : comps) { if (c instanceof JLayer) { mainFrame.getContentPane().remove(c); break; } } mainFrame.getContentPane().repaint(); } } }.execute(); });6.2 防重复提交的终极方案:数据库唯一约束 + 业务层幂等判断
遮罩层只是 UI 层防护,真正的防线在数据库。在borrow_record表加唯一索引:
ALTER TABLE borrow_record ADD UNIQUE INDEX uk_user_book_active (user_id, book_id, status) WHERE status = 0; -- MySQL 8.0 支持函数索引,只对 status=0 的记录去重同时,BookService.borrowBook()方法增加幂等校验:
public void borrowBook(Long bookId, Long userId) throws SQLException { // 1. 检查是否已借未还 if (borrowDao.hasActiveBorrow(bookId, userId)) { throw new BusinessException("您已借阅此书,尚未归还"); } // 2. 检查库存 if (bookDao.getStockById(bookId) <= 0) { throw new BusinessException("图书库存不足"); } // 3. 扣减库存(UPDATE ... WHERE stock > 0,返回影响行数=0则失败) if (bookDao.decreaseStock(bookId) == 0) { throw new BusinessException("库存已被他人借走,请刷新重试"); } // 4. 插入借阅记录(唯一索引会拦截重复) borrowDao.insertRecord(bookId, userId); }这样,即使用户绕过遮罩层用脚本发请求,数据库唯一索引也会返回Duplicate entry错误,业务层捕获后转为友好提示。
我做这个系统时,在社区文化站部署后,管理员反馈“比以前的 Excel 表格快多了,而且不怕删错行”。后来加了个导出 Excel 功能,用 Apache POI 直接写.xlsx,他们说:“这下连打印都省了。”——技术的价值从来不在炫技,而在让真实的人少点焦虑、多点确定性。希望帮到你。
本文还有配套的精品资源,点击获取