ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Java Swing + MySQL 教材管理系统:从JDBC连接到事务处理与批量导入

2026/9/13 4:05:29 拓冰建站 浏览量
Java Swing + MySQL 教材管理系统:从JDBC连接到事务处理与批量导入 简介一份面向学校教材管理场景的Java桌面应用完整源码包适合Java初学者或课程设计人员学习GUI开发与数据库交互。系统基于JavaSwingMySQL实现涵盖教材信息维护、班级与教师管理、教材分配等核心模块并体现DAO、MVC、事件驱动编程等常见设计思想登录与权限管理兼顾不同角色的操作范围关键操作有确认提示整体设计规范。压缩包共53个文件包含18个Java源码、对应class文件、MySQL建表与初始数据脚本、Swing界面运行所需的图片素材以及项目配置文件与依赖jar包整体约3.39MB。已有1725人学习/下载可用于毕业设计、课程实训或二次开发参考。通过阅读源码可掌握Swing组件布局、JDBC数据访问、事务与异常处理等实用技能资源目录清晰便于按模块拆解学习对提升桌面端管理系统开发能力很有帮助。1. 学校教材管理系统为什么还在用 Swing 做桌面端学校教材管理这类内部系统看起来不如互联网项目光鲜但需求非常明确教材入库、班级领用、库存盘点、余量统计。很多学校的信息中心至今保留着局域网内运行的桌面客户端因为管理员不需要浏览器、不需要部署 Web 服务器双击就能用。Java 搭配 Swing 做界面、MySQL 存数据是这个场景下最稳的组合之一。Java 的跨平台特性让系统可以同时跑在 Windows 和国产 Linux 办公电脑上Swing 虽然原生外观不出彩但胜在控件稳定、资料多、不依赖额外前端构建链MySQL 则负责把教材书目、库存、领用记录这些关系型数据管理得清清楚楚。本文按一条完整的实现路径展开先搭起 JDBC 连接和主窗口骨架再设计数据表并完成入库、领用、统计三个核心功能接着处理中文乱码、事务回滚这类必踩的坑最后聊批量导入和打包分发的实用技巧。适合正在做课设、毕业设计或者要给学校信息中心交付一个可维护小系统的开发者参考。2. JDBC 连接 MySQL 与 Swing 主界面搭建2.1 环境准备与技术选型开发一个 Swing MySQL 的教材管理系统常见做法是 JDK 8 或 JDK 11 配合 MySQL 5.7 或 8.0。JDK 8 对 Swing 的支持最成熟很多学校的机房还在用这个版本MySQL 8.0 引入了 caching_sha2_password 认证插件JDBC 驱动需要用 8.x 版本才能兼容。依赖方面不需要 Maven 也没问题直接把 mysql-connector-java 的 jar 包放进项目的 lib 目录再添加到 classpath 即可这对没有外网环境的学校机房尤其友好。连接层我一般会单独封装一个工具类把数据库地址、用户名、密码集中管理后续调整连接参数只需要改一处。Swing 界面则按照「主窗体 功能面板」的方式组织左侧是导航菜单右侧是内容区域这样后续新增统计报表模块时不需要重绘整体布局。2.2 编写数据库连接工具类先写一个标准的 JDBC 连接工具类代码里包含驱动加载、获取连接和资源关闭三个核心方法import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBUtil { // 按实际环境修改localhost、3306、数据库名、账号、密码 private static final String URL jdbc:mysql://localhost:3306/book_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; 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); } // 统一关闭资源避免连接泄漏 public static void close(ResultSet rs, Statement stmt, Connection conn) { if (rs ! null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (stmt ! null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }代码逻辑说明URL 里拼接了 useUnicodetrue 和 characterEncodingutf8 两个参数目的是让 Java 程序写入 MySQL 的中文不乱码useSSLfalse 是在本机或内网环境关闭 SSL 加密握手减少连接耗时。serverTimezoneAsia/Shanghai 处理 MySQL 8.0 对时区的严格校验。静态代码块里写 Class.forName 是确保驱动类只加载一次后续每次 getConnection 都在同一个类加载器上下文中创建新的连接对象。2.3 构建 Swing 主窗口骨架主窗口采用 BorderLayout 布局左侧 JList 作为导航右侧 JTabbedPane 承载不同业务面板。教材管理、领用登记、库存查询、统计报表四个 Tab 页对应四个独立面板类之后每个功能都在自己的面板中实现便于维护。import javax.swing.*; import java.awt.*; public class MainFrame extends JFrame { public MainFrame() { setTitle(学校教材管理系统); setSize(1000, 700); setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); setLocationRelativeTo(null); // 左侧导航 JPanel leftPanel new JPanel(new BorderLayout()); leftPanel.setPreferredSize(new Dimension(180, 0)); String[] items {教材管理, 领用登记, 库存查询, 统计报表}; JListString navList new JList(items); navList.setFont(new Font(微软雅黑, Font.PLAIN, 14)); leftPanel.add(new JLabel(功能导航, SwingConstants.CENTER), BorderLayout.NORTH); leftPanel.add(new JScrollPane(navList), BorderLayout.CENTER); // 右侧功能页 JTabbedPane tabbedPane new JTabbedPane(); tabbedPane.addTab(教材管理, new BookPanel()); tabbedPane.addTab(领用登记, new ReceivePanel()); tabbedPane.addTab(库存查询, new StockPanel()); tabbedPane.addTab(统计报表, new ReportPanel()); // 导航切换时联动 Tab navList.addListSelectionListener(e - { if (!e.getValueIsAdjusting()) { tabbedPane.setSelectedIndex(navList.getSelectedIndex()); } }); add(leftPanel, BorderLayout.WEST); add(tabbedPane, BorderLayout.CENTER); } public static void main(String[] args) { SwingUtilities.invokeLater(() - { new MainFrame().setVisible(true); }); } }这段骨架代码里有几个值得注意的细节。navList 加了 ListSelectionListener点击左侧导航时右侧 Tab 自动切换省去用户在两个组件间来回操作的麻烦。SwingUtilities.invokeLater 保证界面创建放在事件分发线程 EDT 上程序启动更快也避免多线程操作界面组件时的并发问题。面板类 BookPanel、ReceivePanel 等尚未实现连编时会报错可以先用空 JPanel 占位后续逐个填充具体业务逻辑。2.4 使用 JTable 展示数据库记录教材书目列表是系统最常用的数据展示方式JTable 配合 DefaultTableModel 是最经典的做法。从 MySQL 查出数据后填充到二维数组里再交给 TableModel。import javax.swing.*; import javax.swing.table.DefaultTableModel; import java.awt.*; import java.sql.*; import java.util.Vector; public class BookPanel extends JPanel { private JTable table; private DefaultTableModel model; public BookPanel() { setLayout(new BorderLayout()); model new DefaultTableModel(); table new JTable(model); add(new JScrollPane(table), BorderLayout.CENTER); loadData(); } private void loadData() { String sql SELECT id, book_name, isbn, publisher, price, stock FROM t_book ORDER BY id DESC; try (Connection conn DBUtil.getConnection(); Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(sql)) { model.setColumnIdentifiers(new String[]{ID, 教材名称, ISBN, 出版社, 价格, 库存}); while (rs.next()) { VectorObject row new Vector(); row.add(rs.getInt(id)); row.add(rs.getString(book_name)); row.add(rs.getString(isbn)); row.add(rs.getString(publisher)); row.add(rs.getBigDecimal(price)); row.add(rs.getInt(stock)); model.addRow(row); } } catch (SQLException e) { JOptionPane.showMessageDialog(this, 查询教材列表失败: e.getMessage()); } } }loadData 方法每次调用都重新查询数据库这在数据量几千条的教材表上完全可行。try-with-resources 语法让 Connection、Statement、ResultSet 都自动关闭省去手动调 close 的繁琐。DefaultTableModel 的 setColumnIdentifiers 方法设置表头列名addRow 逐行加入数据。列名里不需要展示的字段比如 price 的精度问题可以在查询 SQL 里直接处理也可以用 TableColumn 设置单元格渲染器。参数名推荐值作用useUnicodetrue启用 Unicode 字符集支持characterEncodingutf8客户端与服务端交互使用 UTF-8useSSLfalse内网环境关闭 SSL降低延迟serverTimezoneAsia/Shanghai解决 MySQL 8.0 时区差异rewriteBatchedStatementstrue批量插入时提升性能后续导入功能会用到参数说明rewriteBatchedStatements 这一项后续在做 Excel 批量导入时能明显提升效率MySQL 驱动默认逐条执行 addBatch开启该参数后会在服务端合并为多条 VALUES 语句一次提交。3. 核心业务流程入库、领用登记与库存扣减3.1 数据表设计教材表、领用表与学期字段教材管理系统最少需要两张表教材表 t_book 记录教材元数据和总库存领用表 t_receive 记录每一次班级领用的明细。考虑到学校按学期管理教材t_book 表可以加上 semester 字段表示适用学期t_receive 表加上 receive_date 记录领用时间便于期末统计消耗量。CREATE TABLE t_book ( id INT AUTO_INCREMENT PRIMARY KEY, book_name VARCHAR(100) NOT NULL COMMENT 教材名称, isbn VARCHAR(20) COMMENT ISBN号, publisher VARCHAR(100) COMMENT 出版社, price DECIMAL(10,2) DEFAULT 0 COMMENT 定价, stock INT DEFAULT 0 COMMENT 当前库存, semester VARCHAR(20) COMMENT 适用学期如2025-2026-1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_receive ( id INT AUTO_INCREMENT PRIMARY KEY, book_id INT NOT NULL COMMENT 外键关联t_book.id, class_name VARCHAR(50) NOT NULL COMMENT 班级名称, receive_count INT NOT NULL COMMENT 领用数量, receive_date DATE NOT NULL COMMENT 领用日期, operator VARCHAR(50) COMMENT 操作人, CONSTRAINT fk_receive_book FOREIGN KEY (book_id) REFERENCES t_book(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段设计说明book_id 设置外键约束保证领用记录必须指向存在的教材这是数据完整性的基础。t_receive 表不冗余教材名称字段查询时通过 JOIN 关联 t_book 获取避免教材名称修改后历史记录出现不一致。DECIMAL(10,2) 比 DOUBLE 更适合存储价格不会出现浮点误差。utf8mb4 比 utf8 多了对生僻字和 emoji 的支持教材名称里偶尔出现的特殊符号也能正常存。3.2 教材入库事务保证库存原子更新入库操作分两种情况新教材直接 INSERT已存在的教材在原有库存上增加数量。推荐的做法是先按 ISBN 查询存在则执行 UPDATE 累加库存不存在则 INSERT 新记录。这两个步骤要放在同一个事务里避免出现查询完还没写入时其他操作插入了同一本教材。代码实现public void addBookOrIncreaseStock(String bookName, String isbn, String publisher, BigDecimal price, int count) { String selectSql SELECT id FROM t_book WHERE isbn ?; String updateSql UPDATE t_book SET stock stock ? WHERE id ?; String insertSql INSERT INTO t_book(book_name, isbn, publisher, price, stock) VALUES(?,?,?,?,?); try (Connection conn DBUtil.getConnection()) { conn.setAutoCommit(false); // 开启事务 try (PreparedStatement psSelect conn.prepareStatement(selectSql)) { psSelect.setString(1, isbn); try (ResultSet rs psSelect.executeQuery()) { if (rs.next()) { try (PreparedStatement psUpdate conn.prepareStatement(updateSql)) { psUpdate.setInt(1, count); psUpdate.setInt(2, rs.getInt(id)); psUpdate.executeUpdate(); } } else { try (PreparedStatement psInsert conn.prepareStatement(insertSql)) { psInsert.setString(1, bookName); psInsert.setString(2, isbn); psInsert.setString(3, publisher); psInsert.setBigDecimal(4, price); psInsert.setInt(5, count); psInsert.executeUpdate(); } } } conn.commit(); } catch (SQLException e) { conn.rollback(); throw new RuntimeException(入库失败事务已回滚: e.getMessage()); } finally { conn.setAutoCommit(true); } } catch (SQLException e) { e.printStackTrace(); } }代码逻辑解析setAutoCommit(false) 开启手动事务commit 前任何一步抛出异常都会进入 rollback避免出现 UPDATE 成功而 INSERT 未执行这类半成品状态。PreparedStatement 使用 ? 占位符可以有效防止 SQL 注入同时 MySQL 服务端对预编译语句有缓存重复执行同一 SQL 时性能更高。setBigDecimal 将价格直接以 BigDecimal 类型传入驱动数据库端 DECIMAL 字段能精确匹配。库存扣减不建议使用UPDATE t_book SET stock stock - ? WHERE id ?之外的方式比如先 SELECT 再在 Java 里计算新值然后 UPDATE这在多客户端同时操作时会产生丢更新问题。SQL 中的stock stock - ?由数据库行锁保证原子性天然安全。3.3 领用登记扣库存与写记录必须同事务班级领用教材涉及两次写操作向 t_receive 插入领用记录、扣减 t_book 的库存。这两步必须在一个事务内完成否则可能扣了库存但没记录或者记了领用但库存没变。领用逻辑里还有一个前置校验检查当前库存是否足够。public boolean receiveBooks(int bookId, String className, int count, String operator) { String checkSql SELECT stock FROM t_book WHERE id ? FOR UPDATE; String insertSql INSERT INTO t_receive(book_id, class_name, receive_count, receive_date, operator) VALUES(?,?,?,CURDATE(),?); String updateSql UPDATE t_book SET stock stock - ? WHERE id ?; try (Connection conn DBUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement psCheck conn.prepareStatement(checkSql)) { psCheck.setInt(1, bookId); try (ResultSet rs psCheck.executeQuery()) { if (!rs.next()) { throw new SQLException(教材ID不存在); } int stock rs.getInt(stock); if (stock count) { throw new SQLException(库存不足当前库存: stock); } } try (PreparedStatement psInsert conn.prepareStatement(insertSql)) { psInsert.setInt(1, bookId); psInsert.setString(2, className); psInsert.setInt(3, count); psInsert.setString(4, operator); psInsert.executeUpdate(); } try (PreparedStatement psUpdate conn.prepareStatement(updateSql)) { psUpdate.setInt(1, count); psUpdate.setInt(2, bookId); psUpdate.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { conn.rollback(); JOptionPane.showMessageDialog(null, 领用失败: e.getMessage()); return false; } finally { conn.setAutoCommit(true); } } catch (SQLException e) { e.printStackTrace(); return false; } }checkSql 中FOR UPDATE是行级锁在当前事务提交前锁定这条教材记录。两个客户端同时为不同班级领用同一本教材时后到达的事务会阻塞等待直到前一个事务 commit 后才读到最新库存。如果没有 FOR UPDATE两个事务同时读到库存 100各自领用 80最终库存变成 20 而不是正确的 -60 或 0这就是经典的并发丢失更新场景。3.4 库存查询与学期统计 SQL教材查询界面按教材名称或 ISBN 模糊搜索统计报表按学期分组汇总领用总量。两个功能都只需要只读 SQL查询结果直接填充到 JTable 中。-- 库存查询支持模糊匹配 SELECT id, book_name, isbn, publisher, price, stock FROM t_book WHERE book_name LIKE CONCAT(%, ?, %) OR isbn LIKE CONCAT(%, ?, %) ORDER BY stock ASC; -- 统计报表按教材和学期汇总领用量 SELECT b.book_name, r.receive_date, SUM(r.receive_count) AS total_receive FROM t_receive r JOIN t_book b ON r.book_id b.id WHERE b.semester ? GROUP BY b.book_name, r.receive_date ORDER BY total_receive DESC;第一句 SQL 的 LIKE 模糊搜索配合 CONCAT 拼接占位符比直接在 Java 里拼%到字符串中更安全。第二句统计 SQL 用 JOIN 把两表关联起来GROUP BY 按教材和日期分组SUM 聚合每个分组的领用总数。这里的 semester 参数从学期下拉框传入用户选择一个学期就能看到该学期内消耗量最大的教材排行这对下学期的采购预判参考价值极高。业务层面还有一个容易被忽略的点退书。学生转学或教材损坏需要退还时操作与领用相反——插入一条 receive_count 为负数的记录同时库存加回。领用表设计时不需要单独的退书表用正负数区分领用与退还即可统计时 SUM 自动抵销。4. 排错清单乱码、事务回滚与连接池取舍4.1 中文乱码的排查顺序Swing 界面乱码和 MySQL 存储乱码是两类问题排查方向完全不同。如果界面按钮、标签上的汉字显示成方框大概率是 JDK 字体配置或操作系统缺少中文字体但绝大多数 Windows 系统不会出现这种情况更常见的是数据库里存进去的就是乱码读出来自然就是乱码。排查顺序建议先看 MySQL 服务端字符集登录后执行SHOW VARIABLES LIKE character_set%;如果 database 和 server 的值不是 utf8mb4需要修改 my.cnf 的 [mysqld] 段加上character-set-serverutf8mb4后重启 MySQL。再看建表语句确认使用了 utf8mb4 编码最后看 JDBC URL 是否带了 characterEncodingutf8 参数。三层都对齐后新写入的数据才会正常。历史脏数据需要先清空再重新导入或者用 ALTER TABLE 转换字符集后手动 UPDATE 修复。4.2 事务回滚的逻辑分析与边界条件事务的 rollback 不是万能药它只能回滚当前连接中未提交的 DML 操作DDL 语句CREATE TABLE、ALTER TABLE和已提交的事务无法回滚。教材管理系统中事务边界要尽量短避免在事务内执行耗时操作否则会长时间占用数据库连接和行锁拖垮其他客户端的响应速度。// 反例事务中夹带界面刷新操作 conn.setAutoCommit(false); // 执行SQL... Thread.sleep(1000); // 刷新表格 loadData(); conn.commit();反例代码里 Thread.sleep 模拟了耗时业务逻辑但实际上没人会在事务里写 sleep真正的问题是有人会在事务里调用网络请求或文件读取。正确的做法是事务只包围数据库写操作读操作在 commit 之后再执行。留意上面代码里 loadData 调用时 conn 仍然处于手动事务状态如果 loadData 内部有写操作会被归入未提交的当前事务引发难以定位的锁等待。4.3 连接资源泄漏的高频产生点Swing 应用生命周期长用户可能从上班开到下班连接泄漏在这种场景下后果严重。除了 try-with-resources 之外还有几个隐蔽的连接泄漏点值得留意泄漏场景原因解决方案JTable 渲染时执行 SQLTableCellRenderer 里频繁 getConnection 未关闭改用内存缓存渲染不查库定时刷新 TimerTask每次刷新创建新连接却不 closefinally 块或 try-with-resources模态对话框内操作对话框关闭时连接未释放对话框窗口关闭事件中执行关闭异常分支遗漏某个 catch 后直接 return统一在 finally 中关闭连接池在这里有用吗学校教材管理系统一般是单机或小局域网部署并发量极低连接池带来的性能提升不明显反而增加了配置复杂度。直接把 DBUtil 的 getConnection 每次新建连接配合 try-with-resources 关闭就足够可靠了。只有当在线用户超过 50 人时才需要考虑引入 HikariCP 或 Druid。5. 进阶收尾Excel 批量导入与可执行 jar 打包教材管理员手里往往有教务处提供的教材清单 Excel 文件逐条手输到系统里效率太低批量导入是这类系统最实用的功能。OpenCSV 处理 CSV 比读取 xlsx 更简单但教务处的表格多半是 xlsx 格式推荐用 Apache POI 的 XSSFWorkbook 解析。导入的第一行通常是表头从第二行开始逐条读取每条数据复用前面写的 addBookOrIncreaseStock 方法。导入过程要在事务中逐条处理记录成功条数和失败原因最后一次性展示结果而不是遇到一条脏数据就中断整个导入。// 伪代码示意读取xlsx并逐行入库 try (Workbook wb new XSSFWorkbook(new FileInputStream(books.xlsx))) { Sheet sheet wb.getSheetAt(0); int successCount 0; StringBuilder errorMsg new StringBuilder(); for (int i 1; i sheet.getLastRowNum(); i) { Row row sheet.getRow(i); String name row.getCell(0).getStringCellValue(); String isbn row.getCell(1).getStringCellValue(); double price row.getCell(2).getNumericCellValue(); int count (int) row.getCell(3).getNumericCellValue(); try { addBookOrIncreaseStock(name, isbn, , BigDecimal.valueOf(price), count); successCount; } catch (Exception e) { errorMsg.append(第).append(i 1).append(行失败: ).append(e.getMessage()).append(\n); } } System.out.println(成功导入 successCount 条失败明细: errorMsg); } catch (IOException e) { e.printStackTrace(); }这段逻辑的边界处理值得说清楚CellType 可能是 STRING、NUMERIC 或 BLANK直接从 getCell 拿值容易遇到类型不匹配异常。更稳妥的写法是先判断cell.getCellType()再调用对应的 getStringCellValue 或 getNumericCellValue。教材名称里有空行时程序应该跳过而不是报错常见做法是判断字符串为 null 或 empty 后 continue。打包方面用 IDEA 的 Artifacts 功能可以生成可执行 jar但外部依赖 mysql-connector-java 需要一并打入。最省事的办法是 Maven 配置 maven-assembly-plugin 生成 fat jar把所有依赖解压合并进一个包。没有 Maven 的话也可以手动解压 mysql-connector-java.jar 到输出目录再和项目 class 文件一起打 jar。注意数据库连接地址要支持运行环境覆盖比如String url System.getProperty(db.url, jdbc:mysql://localhost:3306/book_db...)这样部署时可以用java -Ddb.url... -jar bookmanager.jar指定远程数据库地址NULL 值处理同理正式部署前在干净的 Windows 机器上拿几万行数据实测一遍导入耗时顺便检查系统在大数据量下的界面刷新是否卡顿——毕竟学校教材系统用得好不好往往取决于最后一个学期末的统计数据导出顺不顺。本文还有配套的精品资源点击获取