
简介这是一份基于 JavaMySQL 的 GUI 图书管理系统课程设计项目面向正在完成数据库或 Java 课程设计的本科、高职学生也适合想通过完整项目梳理 Swing 界面开发与 JDBC 数据操作的初学者。系统核心功能包括管理员身份验证、图书信息录入与修改、用户资料检索、图书借阅与归还以及按书名或作者查找图书通过这些模块可以直观掌握面向对象分层设计、SQL 增删改查语句、数据库表结构规划以及桌面端交互界面与后端逻辑的衔接方式。压缩包共 187 个文件以 java 源码、class 编译文件、png 界面截图和 sql 数据库脚本为主同时包含 docx/doc 说明文档、md 笔记与 license 文件整体大小约 16.25MB便于导入 IDE 运行查看也可结合文档理解项目结构。目前已有 998 人学习下载。除了可运行代码资源还附带了界面预览图与数据库初始化脚本方便对照界面还原功能流程文档类文件对撰写课程设计报告也有直接参考价值整体上是一份结构完整、便于二次开发的 JavaMySQL 课设方案。1. 不是课程设计是 Java 后端入门的第一个完整闭环图书管理系统GUI是 Java 开发者绕不开的一个起点Title 里的 Java、MySQL、GUI 三个词本质上构成了一条完整的业务链路用户界面负责录入和展示MySQL 负责数据的持久化Java 在中间处理业务逻辑和数据库交互。这套组合几乎是 Java 后端入门的“最小可行性产品”——它不涉及分布式、不涉及中间件但把 JDBC 连接、CRUD、事务边界、异常处理、GUI 事件模型全部串在了一起。很多有 5 年以上经验的开发者回头看这个题目会低估它的价值。实际招聘中经常能看到这样的场景候选人能熟练说出 Spring Boot 注解但让他手写一个 JDBC 连接、处理事务提交回滚却无从下手。原因就在于框架把底层封得太死很多人没真正理解过 Java 和数据库之间的这条通道是怎么建立起来的。本文就顺着这个标题把一套完整可落地的方案拆开讲从 MySQL 表结构设计讲起到 JDBC 连接、DAO 层搭建、Swing GUI 实现、事务与多条件检索再到最后的性能与维护技巧。不使用框架、不引入第三方组件纯 JDK 自带能力 MySQL 实现适合 Java 初学者建立完整的工程认知也适合有经验的开发者回头审视自己在基础层面的盲区。2. 先立数据模型基于 JavaMySQL 的表结构设计与 JDBC 连接池思路2.1 图书管理系统最少需要几张表做系统第一步不是写界面是设计数据库。图书管理系统的核心实体只有两个图书Book和借阅记录Borrow Record。用户User表在纯图书管理的场景下可以后置但为了业务闭环一般会设计三张表图书表、读者表、借阅记录表。图书表至少要包含book_id主键、book_name、author、publisher、isbn、category、price、stock、total_stock。这里有两个字段容易被新手忽略一个是 stock当前可借数量一个是 total_stock库藏总量。借出时递减 stock还书时递增 stock而 total_stock 保持不变用来统计库存变化和图书损耗。如果只设计一个字段后面做借阅逻辑时会发现数据修正非常痛苦。借阅记录表则是整个系统的事务核心。字段设计如下字段类型说明borrow_idINT AUTO_INCREMENT主键book_idINT外键关联图书表reader_idINT外键关联读者表borrow_dateDATE借出日期due_dateDATE应还日期return_dateDATE实际归还日期NULL 表示未还statusTINYINT0-借出中 1-已归还 2-逾期注意due_date和return_date是两个不同的字段。很多人只用一个字段存归还日期导致逾期判断无从下手。status字段不要用字符串用 TINYINT 枚举即可配合 Java 端的常量类做映射。SQL 建表语句中字符集和存储引擎绝对不能用默认值。MySQL 8.0 的默认存储引擎是 InnoDB这没问题但字符集必须显式指定。图书数据大概率包含中文建议使用utf8mb4而不是utf8。原因是utf8在 MySQL 中是utf8mb3的别名最多支持 3 字节而某些特殊字符如表情符号是 4 字节写入会报错。虽然图书管理系统中不太可能存表情但表格字段的备注里可能涉及特殊符号统一用utf8mb4是更严谨的习惯。建表语句参考CREATE DATABASE IF NOT EXISTS library_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE library_system; CREATE TABLE book ( book_id INT PRIMARY KEY AUTO_INCREMENT, book_name VARCHAR(200) NOT NULL, author VARCHAR(100) DEFAULT 佚名, publisher VARCHAR(200), isbn VARCHAR(20) UNIQUE, category VARCHAR(50), price DECIMAL(10,2), stock INT DEFAULT 0, total_stock INT DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, KEY idx_category (category), KEY idx_book_name (book_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段 SQL 里有两个细节值得说明第一isbn设置了 UNIQUE 唯一约束这是业务层面的数据质量保证——同一本书的 ISBN 重复录入时JDBC 层会抛出DuplicateKeyException代码里捕获后转成友好提示即可。第二stock和total_stock没有使用UNSIGNED这是有意为之。UNSIGNED字段在 JDBC 里取值范围会变成 0 到 4294967295Java 端的getInt()返回值在超界时可能出现负数排查起来很麻烦。业务上库存应该由代码逻辑保证不小于 0而不是靠数据库类型强约束。2.2 JDBC 连接的两种写法直连与池化图书管理系统通常是一个桌面应用使用者数量有限连接池不是刚需但理解连接的生命周期仍然必要。最基础的写法是每次操作都获取连接用完即关。这种方式的缺点是频繁创建和销毁数据库连接在 GUI 应用高频操作时会出现明显的卡顿。我一般会建议在这个项目里手写一个极简连接池不需要引入 HikariCP 或 Druid。核心思路是用一个 LinkedList 保存固定数量的物理连接每次获取时从池中取出归还时放回队尾。底层仍然是 JDBC 的DriverManager.getConnection()只是加了一个容器做复用。先展示最直接的 JDBC 连接代码作为理解基础import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/library_system?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD your_password; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }连接串里有三个参数必须解释清楚。useSSLfalse是让本地开发环境跳过 SSL 握手否则 MySQL 8.0 默认开启 SSL 会导致连接阶段额外耗时serverTimezoneAsia/Shanghai指定时区否则DATETIME类型的读写会相差 8 小时characterEncodingutf8确保中文在传输过程中不出现乱码。这三个参数几乎覆盖了 Java 连 MySQL 时 80% 的编码和时区问题。MySQL 8.0 之后驱动类名从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver。如果你的 pom 或 lib 里引的是 5.x 版本的驱动两个都能用但如果用的是 8.x 驱动旧的类名虽然不报错但会打印一条 deprecation 日志强迫症应该直接改新类名。2.3 PreparedStatement 为什么不能用 Statement 替代在 DAO 层的增删改查中SQL 的拼接方式决定了系统的安全基线。Statement用字符串拼接参数形如String sql SELECT * FROM book WHERE book_name bookName ;这种写法在图书管理系统的登录场景下会直接翻车。比如读者输入的书名是 OR 11拼接后的 SQL 变成SELECT * FROM book WHERE book_name OR 11条件恒为真查询返回全部图书。如果是 UPDATE 或 DELETE 语句后果更严重。SQL 注入不是理论概念在这种带 GUI 的系统中文本框是天然的注入入口。PreparedStatement 的预编译机制从根源上解决了这个问题。参数用通配符?占位提交时驱动会将参数值作为纯数据传递给 MySQL 服务端不参与 SQL 语句的解析。来看一个新增图书的 DAO 层方法public int addBook(Book book) { String sql INSERT INTO book (book_name, author, publisher, isbn, category, price, stock, total_stock) VALUES (?, ?, ?, ?, ?, ?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, book.getBookName()); ps.setString(2, book.getAuthor()); ps.setString(3, book.getPublisher()); ps.setString(4, book.getIsbn()); ps.setString(5, book.getCategory()); ps.setBigDecimal(6, book.getPrice()); ps.setInt(7, book.getStock()); ps.setInt(8, book.getTotalStock()); return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return 0; } }注意这段代码使用了 try-with-resources 语法连接、PreparedStatement 都实现了AutoCloseable接口方法结束后自动关闭不需要手动写finally块。这是 Java 7 之后的标准写法比传统方式更为简洁。参数从 1 开始编号和 SQL 中通配符的顺序一一对应。setBigDecimal对应数据库的DECIMAL类型不能用setDouble或setFloat否则价格精度会丢失。3. Swing GUI 与 Java 事件模型图书管理界面从布局到交互3.1 选 Swing 而不是 JavaFX 的理由标题指定了 GUIJava 生态里桌面图形界面的主流选择是 Swing 和 JavaFX。在这个场景下我倾向于 Swing原因很实际Swing 是 JDK 自带的不需要额外配置 JavaFX 的 SDKSwing 的组件模型成熟稳定资料和教学案例最多对于图书管理系统这种 CRUD 密集型应用Swing 的 JTable TableModel 模型比 JavaFX 的 TableView 更容易理解。Swing 的核心是事件驱动模型。用户点击按钮、输入文本、选择表格行都会生成对应的事件对象事件监听器接口负责捕获并处理。GUI 开发新手最容易犯的错误是把业务逻辑直接写在事件监听器里比如在按钮的actionPerformed方法里直接写 JDBC 代码。这个习惯导致后期维护困难。更合理的分层方式见下。3.2 分层原则UI、Service、DAO 的职责边界用一个简单的三层结构来组织代码界面层View只负责创建组件和绑定事件业务层Service处理逻辑规则数据层DAO操作数据库。以借书操作为例界面层拿到界面录入的 bookId 和 readerId传给 Service 层Service 层先检查图书当前库存再检查借阅记录是否已存在然后在一个事务中完成库存扣减和记录插入。这种分层的好处在于每个类的职责单一测试和排错时能快速定位问题。图书管理系统虽然小但分层习惯在之后迁移到 Spring Boot 时可以直接对应上 Controller、Service、Mapper 的概念。先看下面的完整示例。主窗口骨架import javax.swing.*; import java.awt.*; public class MainFrame extends JFrame { private CardLayout cardLayout new CardLayout(); private JPanel cardPanel new JPanel(cardLayout); public MainFrame() { setTitle(图书管理系统); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setSize(900, 600); setLocationRelativeTo(null); cardPanel.add(new BookManagePanel(), bookManage); cardPanel.add(new BorrowPanel(), borrow); add(cardPanel, BorderLayout.CENTER); } public static void main(String[] args) { SwingUtilities.invokeLater(() - { new MainFrame().setVisible(true); }); } }SwingUtilities.invokeLater()是一个必须养成的习惯。Swing 组件不是线程安全的所有 UI 更新必须在事件分发线程EDT中执行。通过invokeLater将窗口创建任务派发到 EDT 上执行避免主线程直接操作 UI 组件造成界面闪烁或死锁。CardLayout实现的是多页面切换。在图书管理系统中图书管理和借阅管理是两个功能面板用卡片布局切换比用多个 JFrame 弹窗更合理——弹窗容易造成窗口管理混乱而卡片布局天然是一种容器切换模型。3.3 核心业务面板给 GUI 接上 MySQL图书管理面板是整个系统最常用的界面。上面的工具栏提供添加修改删除刷新四个按钮中间的 JTable 展示图书列表底部的文本框和下拉框用于录入图书信息。JTable 本身不存数据它依赖TableModel作为数据源。把数据库查询结果转换成表格模型代码逻辑如下public class BookTableModel extends AbstractTableModel { private final String[] columns {图书ID, 书名, 作者, 出版社, ISBN, 分类, 价格, 库存}; private ListBook books new ArrayList(); Override public int getRowCount() { return books.size(); } Override public int getColumnCount() { return columns.length; } Override public String getColumnName(int column) { return columns[column]; } Override public Object getValueAt(int rowIndex, int columnIndex) { Book book books.get(rowIndex); switch (columnIndex) { case 0: return book.getBookId(); case 1: return book.getBookName(); case 2: return book.getAuthor(); case 3: return book.getPublisher(); case 4: return book.getIsbn(); case 5: return book.getCategory(); case 6: return book.getPrice(); case 7: return book.getStock(); default: return null; } } public void setBooks(ListBook books) { this.books books; fireTableDataChanged(); } }表格模型写好后查询按钮的处理逻辑就是把 DAO 查询结果塞进模型并刷新 JTableprivate void searchBooks() { String keyword searchField.getText().trim(); ListBook books bookDao.findByKeyword(keyword); tableModel.setBooks(books); }这段代码里trim()很关键。GUI 文本框获取到的内容会带上用户无意中输入的前后空格直接拼进 SQL 会导致查不出结果。数据库层面虽然可以用LIKE % ? %进行模糊匹配但空格如果不清除% 文学 %和%文学%就是两种完全不同的模式。所以在文本进入查询层之前先用trim()清理一遍是基本操作。表格新增、修改这两步都依赖JTable.getSelectedRow()获取用户当前选中的行号。一个常见的坑是用户先点击按钮再点击表格行时选中状态可能已经丢失getSelectedRow()返回 -1直接用就会抛出IndexOutOfBoundsException。所以代码里必须先判断返回值int selectedRow bookTable.getSelectedRow(); if (selectedRow 0) { JOptionPane.showMessageDialog(this, 请先选择要操作的图书); return; } int bookId (int) tableModel.getValueAt(selectedRow, 0);这里再次强调 GUI 编程的铁律任何从界面获取的数据都是不可信的。用户可能没有选中任何一行、可能输入了非法字符代码需要先做校验再执行后续操作这是成熟工程师和课程设计之间最显著的差别。4. 事务与多条件检索借书还书的高阶攻略与 MySQL 查询边界4.1 借书流程的事务边界图书管理系统的核心流程是借书和还书。这一借一还涉及多张表的修改需要用事务保证数据一致性。以借书为例完整操作包含两步从 book 表中扣减库存stock stock - 1向 borrow_record 表插入一条借阅记录status 0操作图书管理系统的用户规模不大并发量可能不高但事务的编程范式必须正确。如果不加事务控制在第一步成功、第二步失败时库存被扣了但借阅记录缺失图书就“凭空消失”了。在 JDBC 中事务控制分为三个环节关闭自动提交、执行操作、提交或回滚。核心代码如下public boolean borrowBook(int bookId, int readerId, int days) { String updateStock UPDATE book SET stock stock - 1 WHERE book_id ? AND stock 0; String insertRecord INSERT INTO borrow_record (book_id, reader_id, borrow_date, due_date, status) VALUES (?, ?, CURDATE(), DATE_ADD(CURDATE(), INTERVAL ? DAY), 0); Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); PreparedStatement ps1 conn.prepareStatement(updateStock); ps1.setInt(1, bookId); int rows ps1.executeUpdate(); if (rows 0) { conn.rollback(); return false; } PreparedStatement ps2 conn.prepareStatement(insertRecord); ps2.setInt(1, bookId); ps2.setInt(2, readerId); ps2.setInt(3, days); ps2.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }有几个点是面试时经常被追问的。WHERE book_id ? AND stock 0这里的条件不是冗余而是利用数据库行锁实现并发保护。当两个用户同时借同一本书时第一个事务执行 UPDATE 后该行会被行锁锁定第二个事务进入阻塞状态只有等第一个事务提交释放锁后第二个事务才能执行。此时 stock 可能已经为 0UPDATE 影响行数为 0直接回滚。这个机制避免了超卖问题。setAutoCommit(false)必须在任何 SQL 执行之前调用。如果先执行了一条 SELECT 或 UPDATE再关闭自动提交之前已自动提交的操作无法回滚。事务边界必须从第一条 SQL 之前开始划定。回滚要放在 catch 块里而不是 commit 失败后处理。conn.commit()本身可能抛异常此时数据仍处于未提交状态如果不回滚连接归还连接池后残留了未完成的事务状态下一个人拿着这个连接执行 SQL 时会出现莫名其妙的结果。4.2 多条件检索的 SQL 拼接规范图书管理系统的列表页还需要按书名、作者、分类、ISBN 进行组合查询。多个条件可能为空也可能同时存在。比较直观的做法是拼动态 SQL但拼接顺序和参数绑定有讲究。两种常见方案对比方案写法风险方案一WHERE 11 AND book_name LIKE ? AND author LIKE ?11 恒真存在被注入的可能性且优化器有时无法高效利用索引方案二StringBuilder拼接单独处理 WHERE可维护参数和条件一一对应推荐推荐第二种方案。先看正确写法public ListBook findByCondition(BookQuery query) { StringBuilder sql new StringBuilder(SELECT * FROM book WHERE 11); ListObject params new ArrayList(); if (query.getBookName() ! null !query.getBookName().isEmpty()) { sql.append( AND book_name LIKE ?); params.add(% query.getBookName() %); } if (query.getAuthor() ! null !query.getAuthor().isEmpty()) { sql.append( AND author LIKE ?); params.add(% query.getAuthor() %); } if (query.getCategory() ! null !query.getCategory().isEmpty()) { sql.append( AND category ?); params.add(query.getCategory()); } try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql.toString())) { for (int i 0; i params.size(); i) { ps.setObject(i 1, params.get(i)); } try (ResultSet rs ps.executeQuery()) { ListBook books new ArrayList(); while (rs.next()) { Book book new Book(); book.setBookId(rs.getInt(book_id)); book.setBookName(rs.getString(book_name)); book.setAuthor(rs.getString(author)); book.setPublisher(rs.getString(publisher)); book.setIsbn(rs.getString(isbn)); book.setCategory(rs.getString(category)); book.setPrice(rs.getBigDecimal(price)); book.setStock(rs.getInt(stock)); book.setTotalStock(rs.getInt(total_stock)); books.add(book); } return books; } } catch (SQLException e) { e.printStackTrace(); return Collections.emptyList(); } }代码实现中有两个细节值得注意。使用ListObject保存参数循环里统一setObject避免为每种类型单独写 setter。但setObject对BIGINT UNSIGNED类型的字段可能有精度问题普通场景没问题遇到特殊类型还是要用setLong或setBigDecimal做精确转换。SQL 中书名、作者使用LIKE分类使用。分类字段通常是精确枚举值计算机、文学、历史用能命中索引。书名和作者属于模糊匹配LIKE %关键词%写法在 MySQL 里会放弃索引全表扫描。数据量小几千本时无所谓数据量上百万后查询会变慢那时考虑全文索引或引入 Elasticsearch。这是搜索引擎方案的范畴不在本文讨论范围内。4.3 事务中的隔离级别怎么选JDBC 中setTransactionIsolation有五个可选级别其中 READ_UNCOMMITTED 可能读到其他事务未提交的数据在借书场景中会导致库存判断失真属于最不安全的级别。SERIALIZABLE 是将整个表加锁安全但有性能瓶颈。图书管理系统建议使用TRANSACTION_READ_COMMITTED即读已提交。这个级别保证读到的数据是已提交的避免脏读同时并发性能不会太差。MySQL 的默认隔离级别是 REPEATABLE_READ它在一段时间内多次读取同一数据时结果保持一致。JDBC 层不显式设置的话默认使用数据库配置。两种级别在单条 UPDATE 和 INSERT 操作上没有太大区别区别体现在事务中连续两次 SELECT 之间REPEATABLE_READ 会保持第一次查询的快照而 READ_COMMITTED 每次读的是最新提交的数据。GUI 应用中事务应该尽量短。不要在事务里弹出模态对话框等待用户确认否则连接占用期间数据库连接池会被耗尽。点击“借书”按钮后所有数据校验、库存判断都应在事务之前完成事务内只执行 UPDATE 和 INSERT让事务窗口尽可能窄。5. 批处理与库存校验图书管理系统数据库操作的两个进阶技巧5.1 用批量插入初始化数据图书管理系统在开发测试阶段需要初始化大量图书数据。一条条 INSERT 的效率无法接受这时要用 JDBC 的批处理机制。在数据量不大、数据本身是业务数据的情况下批处理可以显著提升初始化速度。public void batchInsertBooks(ListBook books) { String sql INSERT INTO book (book_name, author, publisher, isbn, category, price, stock, total_stock) VALUES (?, ?, ?, ?, ?, ?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { conn.setAutoCommit(false); for (Book book : books) { ps.setString(1, book.getBookName()); ps.setString(2, book.getAuthor()); ps.setString(3, book.getPublisher()); ps.setString(4, book.getIsbn()); ps.setString(5, book.getCategory()); ps.setBigDecimal(6, book.getPrice()); ps.setInt(7, book.getStock()); ps.setInt(8, book.getTotalStock()); ps.addBatch(); } int[] results ps.executeBatch(); conn.commit(); for (int i 0; i results.length; i) { if (results[i] Statement.EXECUTE_FAILED) { System.err.println(第 (i 1) 条插入失败); } } } catch (BatchUpdateException e) { System.err.println(批处理异常影响行数 e.getUpdateCounts().length); } catch (SQLException e) { e.printStackTrace(); } }批量执行时addBatch()只负责将参数加入队列真正执行发生在executeBatch()。它返回一个int[]数组每个元素代表对应 SQL 的影响行数值为Statement.EXECUTE_FAILED就说明该条失败。BatchUpdateException专门用于处理部分成功的情况异常对象持有getUpdateCounts()可以定位到具体哪条失败。批处理需要手动把autoCommit关闭让所有 INSERT 在同一个事务中执行最后一次性提交。如果每条插入都是独立事务InnoDB 层要写 1000 次 WAL 日志和 redo log耗时大约是批处理的 10 倍以上。5.2 库存不为负的兜底策略借书流程中已经通过WHERE stock 0做了数据库层保护。还有一种情况是管理端手动调整库存如果管理员把库存改成负数业务数据就失效了。MySQL 5.7 开始支持 CHECK 约束ALTER TABLE book ADD CONSTRAINT chk_stock_non_negative CHECK (stock 0);注意 MySQL 8.0.16 之前的版本CHECK 约束只做语法检查不实际生效。8.0.16 之后默认开启严格约束。如果使用 MySQL 5.7需要在 Java 端每次setInt前校验代码层拦截明显比数据库层更易出错。升级版本或使用触发器是更强的方案。5.3 界面卡顿与耗时操作的排查思路GUI 应用常见的问题是执行数据库操作时界面假死。原因是数据库查询在主线程中运行事件分发线程被阻塞用户界面无法响应鼠标和键盘事件。解决办法是使用 SwingWorker 把耗时操作放到后台线程。class SearchWorker extends SwingWorkerListBook, Void { private final String keyword; SearchWorker(String keyword) { this.keyword keyword; } Override protected ListBook doInBackground() { return bookDao.findByKeyword(keyword); } Override protected void done() { try { ListBook books get(); tableModel.setBooks(books); } catch (Exception e) { JOptionPane.showMessageDialog(MainFrame.this, 查询失败 e.getMessage()); } } }doInBackground()在后台线程执行done()回到事件分发线程更新 UI。这是 Swing 多线程模型的正确用法在图书管理系统中接入后查询和界面更新互不阻塞体验完全不同。最后一个容易被忽略的点是关闭窗口时释放资源。EXIT_ON_CLOSE会结束 JVM后台线程随之终止看上去不用做清理。但如果系统嵌入了连接池或定时任务必须有显式的关闭钩子Runtime.getRuntime().addShutdownHook(new Thread(() - { ConnectionPoolManager.closeAll(); }));这样可以保证 JVM 退出前连接池中的所有物理连接被正确释放MySQL 侧不会堆积断开的连接也不会因为应用进程强制退出而堆积大量 TIME_WAIT 状态的连接。把这个钩子写好应用反复启停之后SHOW PROCESSLIST的结果会依然干净——这是一个可以留给自己验证的检查项。本文还有配套的精品资源点击获取