Java Swing+MySQL构建高校教材管理系统:从JDBC到事务的完整实战
发布时间:2026/10/2 2:41:08来源:尧图网络
简介面向高校教务场景的JavaSwingMySQL高校教材管理系统覆盖管理员、教师、学生三类角色实现出版社与教材类型维护、教材订购、入库、领用等完整业务流程并内置教材书号以ISBN开头后跟10位数字的校验规则适合Java课程设计或毕业设计参考。资源共357个文件压缩包约2.57MB包含56个java源码、11个xml配置文件、SQL脚本及运行所需jar包等源码可直接导入Idea或Eclipse运行便于结合界面代码与数据库脚本理解Swing事件驱动和MySQL数据持久化的协作方式。已有537人学习下载适合需要快速搭建教材管理系统原型或学习桌面应用分层开发的初学者。通过查看不同角色主类及各个业务管理面板源码可快速掌握增删改查、角色权限区分和日期/格式校验等常见实现思路。1. 高校教材管理系统为什么还在用Swing课程设计的第一道分水岭每年Java课程设计清单里都会出现“高校教材管理系统”我帮身边人改过不少这类项目后得出一个反直觉的结论真正让人翻车的往往不是Swing窗口画不好而是Java、Swing、MySQL三者两两之间的衔接。这个系统要管的是教材信息维护、库存余量、学生领用申请这些日常事务用桌面端程序完全够用适合Java基础语法刚过关、想拿一个完整项目扛过课程设计或者面试作品环节的人。我一般会建议把它当成一个数据库应用系统来做界面只是壳数据链路才是命。动手之前先把MySQL装好把JDBC驱动放进classpath剩下的事下面几章逐步拆。2. 系统结构设计把Swing界面和MySQL数据层拆开再合拢2.1 选型逻辑SwingMySQL这个组合为什么还没过时Swing在Web前端面前显得老旧但课程设计和桌面工具场景里它依然是主力。它由JDK标准库自带不引入额外前端框架跨平台行为一致打包成jar就能在装了JDK的机器上跑。Java面试一般不会让你现场画窗口但会追着问JDBC、事务和数据库连接池这个项目恰好能在Java基础之上把这两块练扎实。对一个目标是“交得出来、讲得清楚”的课设来说Swing的试错成本比JavaFX低很多。如果自己练手可以把JavaFX拿来对比JavaFX界面更现代但它的控件生命周期、并发模型和Swing完全不同学习曲线陡一截。Swing和JDBC的搭配被验证了很多年遇到的报错基本都有现成答案。MySQL作为存储体积小、教程多和Java配套的连接驱动资料丰富随便一搜就是mysql安装配置教程级别的说明。这个组合能跑通后面想换Spring Boot加MyBatis只是换皮。我一般不建议一上来就套MyBatis或者Spring Boot。题目叫“高校教材管理系统”GUI是Java Swing直接拿JDBC写DAO反而能让你看清SQL和事务边界连接在哪开、事务在哪提交、ResultSet怎么映射成对象。等这套链路跑顺了再谈框架优化。2.2 模块划分五个核心业务模块先画清楚拿到题目先别急着拖控件把业务模块拆出来。常见做法是按角色加业务流拆成五块登录与权限、教材档案、库存管理、领用审批、统计查询。每一块对应一到两张数据表和一个独立的功能面板后面写代码时最怕“想到哪写到哪”。模块核心数据表关键动作登录与权限sys_user登录、按角色显示可用功能教材档案textbook新增、编辑、删除、按关键字过滤库存管理textbook、stock_log入库、出库、查余量领用审批borrow_order学生提交申请、管理员审批统计查询borrow_order、textbook按学期汇总、按教材汇总这张表列清楚以后你会发现界面数量和DAO方法都是可数的。统计查询别做太复杂先保证按学期、按教材两个维度的汇总SQL能跑通。领用审批是整个系统最容易出问题的地方因为它同时改borrow_order的状态和textbook的库存必须用事务包裹第4章会给完整代码。2.3 数据流向从按钮点击到ResultSet的完整链路把一次“按书名查教材”完整走一遍用户在JTextField输入书名点击按钮触发ActionListenerActionListener调Service层方法Service不直接写SQL转给DAO层DAO用PreparedStatement绑定参数执行SELECT拿到ResultSet后逐行映射成Textbook对象最后回到UI层把List丢给TableModelJTable重绘。这条链路每个环节都有明确的代码落点排错也就成了定位到某一层的问题。工程结构我按四五个包划分对课设体量刚好com.example.tbms ├── ui # JFrame / JPanel / 对话框 ├── service # 登录、领用审批等业务逻辑 ├── dao # 所有JDBC操作集中在这里 ├── entity # User、Textbook、Order 实体类 └── util # DBUtil 连接工具类这个分层的意义在排错时体现最明显界面报错去ui包找SQL报错去dao包找业务规则错比如库存不够还能审批通过去service包找。新手最常见的写法是把conn和Statement直接写进ActionListener多个窗口都要查同一张表时代码重复到没法维护。dao和service都要拿数据库连接所以连接工具要单独放。HikariCP是比较省心的连接池实现配置如下public class DBUtil { private static final HikariDataSource DATA_SOURCE; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://127.0.0.1:3306/textbook_ms ?useUnicodetruecharacterEncodingutf8mb4 serverTimezoneAsia/ShanghaiuseSSLfalse); config.setUsername(root); config.setPassword(你的密码); config.setMaximumPoolSize(20); config.setMinimumIdle(3); config.setConnectionTimeout(30000); DATA_SOURCE new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return DATA_SOURCE.getConnection(); } }连接串里四个参数别省utf8mb4保证中文不乱码serverTimezone是MySQL 8必需否则时区报错useSSLfalse去掉SSL握手告警127.0.0.1比localhost更保险避免Linux下走unix socket时报2002。连接池参数里maximumPoolSize不要盲目调大Swing桌面端并发量很低20就够用minimumIdle保持3避免频繁建连。理解了“谁用完了就close归还连接”这个约定连接池就理解了八成。3. MySQL侧先行建表与JDBC连接池的落地配置3.1 建表SQL五张表把业务闭环撑起来我习惯在写Java代码之前先把SQL脚本在MySQL里跑一遍。这样后面所有DAO方法都只是“拼接SQL加映射结果”不用边写边改表结构。建库时字符集直接定utf8mb4后续的中文乱码问题能减少一半。CREATE DATABASE textbook_ms DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE textbook_ms; CREATE TABLE sys_user ( user_id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role ENUM(ADMIN,TEACHER,STUDENT) NOT NULL DEFAULT STUDENT, real_name VARCHAR(30), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE textbook ( book_id INT AUTO_INCREMENT PRIMARY KEY, isbn VARCHAR(20) NOT NULL UNIQUE, book_name VARCHAR(100) NOT NULL, author VARCHAR(50), publisher VARCHAR(80), price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock_quantity INT NOT NULL DEFAULT 0, total_quantity INT NOT NULL DEFAULT 0, semester VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE borrow_order ( order_id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, book_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, status ENUM(PENDING,APPROVED,REJECTED,RETURNED) NOT NULL DEFAULT PENDING, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, audit_time DATETIME, CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES sys_user(user_id), CONSTRAINT fk_order_book FOREIGN KEY (book_id) REFERENCES textbook(book_id) ) ENGINEInnoDB;三个设计要点。第一价格用DECIMAL(10,2)不要用floatJava里对应BigDecimal浮点算钱会丢精度。第二stock_quantity默认值设成0新教材录入后不会出现nullJTable取值不用判空查询统计时也不会被空值干扰。第三borrow_order用status枚举走状态流主表数据不物理删除只更新状态这是保留审批追溯记录的常用做法。库存流水如果需要单独留痕再加一张stock_log表记录每笔入库出库的变化量和操作人CREATE TABLE stock_log ( log_id INT AUTO_INCREMENT PRIMARY KEY, book_id INT NOT NULL, change_quantity INT NOT NULL, change_type ENUM(IN,OUT) NOT NULL, operator_id INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_stock_book FOREIGN KEY (book_id) REFERENCES textbook(book_id) ) ENGINEInnoDB;外键在课设阶段建议保留答辩时能讲约束完整性生产环境有人故意去掉外键换性能但这个项目规模不需要。所有表都用InnoDBMyISAM不支持外键和事务领用审批的事务需求会被直接砍掉。3.2 JDBC连接池不要在点击按钮时才建连接很多课程设计初版用的是DriverManager.getConnection写在一个工具类里每次查询都新建连接。演示时没问题一旦连续点击审批、导出这类操作窗口就会明显卡顿因为桌面程序反复建连还要承担TCP握手和MySQL认证的开销。连接池的差别在于让连接保持存活用完归还而不是断开。上一章的DBUtil用的HikariCP是Java生态里应用很广的池子。Maven工程引入两个依赖即可dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId version4.0.3/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency如果是Eclipse或普通Java工程把两个jar放进lib目录再Build Path。Connector/J 8.x的驱动类名是com.mysql.cj.jdbc.Driver老教程里写的com.mysql.jdbc.Driver只在5.x生效这是最常见的翻车点之一。连接池和驱动版本定了之后代码层面几乎不用改还是按DBUtil.getConnection拿连接。3.3 MySQL安装后必调的三件事密码、认证插件、时区Windows上装MySQL 8.0的流程很成熟但装完别急着写代码。第一确认root密码第二处理认证插件第三处理时区。认证插件是最容易被忽略的MySQL 8默认caching_sha2_password老版本JDBC驱动根本不认。创建一个专用账号并指定mysql_native_password比改全局配置更安全mysql -u root -p CREATE DATABASE textbook_ms DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER tbmslocalhost IDENTIFIED WITH mysql_native_password BY Tbms123456; GRANT ALL PRIVILEGES ON textbook_ms.* TO tbmslocalhost; FLUSH PRIVILEGES;用独立tbms账号而不是root跑项目是容易被忽略的安全习惯。IDENTIFIED WITH mysql_native_password解决的是“Authentication plugin caching_sha2_password cannot be loaded”这个报错如果你的开发机没装MySQL直接去mysql下载官网拿最新安装包安装时记住选Server Only避免装一堆用不到的功能。第三件是时区。MySQL 8经常报“The server time zone value … is unrecognized”。两个办法同时做连接URL带serverTimezoneAsia/ShanghaiMySQL侧执行SET GLOBAL time_zone 08:00。只改一边也能跑但换个环境又容易复发。提示排查连接问题时先用命令行客户端连一次再怀疑代码。命令行连不上程序一定连不上。4. Swing界面与事件联调登录、列表、事务一条链路写下来4.1 登录窗口JFrame搭壳事件里只做调度登录窗口是功能入口结构就是两个输入框加一个按钮。这里有个原则ActionListener里不写SQL只负责取输入、调Service、按结果切换窗口。这样测试登录逻辑时不用启动界面写单元测试也方便。public class LoginFrame extends JFrame { private JTextField usernameField; private JPasswordField passwordField; public LoginFrame() { super(高校教材管理系统); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setSize(420, 300); setLocationRelativeTo(null); buildUi(); } private void buildUi() { JPanel panel new JPanel(new GridBagLayout()); GridBagConstraints gbc new GridBagConstraints(); gbc.insets new Insets(10, 10, 10, 10); gbc.gridx 0; gbc.gridy 0; panel.add(new JLabel(用户名), gbc); gbc.gridx 1; usernameField new JTextField(16); panel.add(usernameField, gbc); gbc.gridx 0; gbc.gridy 1; panel.add(new JLabel(密码), gbc); gbc.gridx 1; passwordField new JPasswordField(16); panel.add(passwordField, gbc); gbc.gridx 0; gbc.gridy 2; gbc.gridwidth 2; JButton loginBtn new JButton(登录); panel.add(loginBtn, gbc); loginBtn.addActionListener(e - doLogin()); setContentPane(panel); } private void doLogin() { String username usernameField.getText().trim(); String password new String(passwordField.getPassword()); UserService userService new UserService(); User user userService.login(username, password); if (user null) { JOptionPane.showMessageDialog(this, 用户名或密码错误, 登录失败, JOptionPane.ERROR_MESSAGE); return; } new MainFrame(user).setVisible(true); dispose(); } }getPassword()返回char[]而不是String转成String后建议立刻清空临时数组这是Swing密码框的通用习惯面试如果问“为什么JPasswordField不提供getText”也能接上。登录失败用JOptionPane弹窗比在窗口底部放红色Label更直观。登录成功跳转MainFrame后要dispose()掉登录窗口否则两个窗口叠一起关了一个另一个还开着。UserService.login里的核心还是PreparedStatement。密码不要用字符串拼接这是Java基础面试反复出现的SQL注入考点public User login(String username, String password) { String sql SELECT user_id, username, role, real_name FROM sys_user WHERE username ? AND password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, md5(password)); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { return new User(rs.getInt(user_id), rs.getString(username), rs.getString(role), rs.getString(real_name)); } } } catch (SQLException e) { throw new RuntimeException(登录查询失败, e); } return null; }md5()这里只是示意。课设这么写能跑但答辩被追问安全性时要说清楚生产环境要用BCrypt这类加盐哈希MD5在暴力破解面前不够看。异常整体抛给UI层统一弹窗每一层都打印堆栈会让控制台刷屏也没人看。4.2 主界面JTabbedPane把五个模块装进去主窗口用BorderLayout顶部放当前用户信息和退出按钮中间放JTabbedPaneTab页和2.2的模块一一对应教材档案、库存管理、领用审批、统计查询。每个Tab对应一个JPanel里面再放独立的表格和按钮组。做多Tab窗口最忌讳把全部代码塞进一个类。我一般给每个Tab做一个独立JPanel类主窗口只负责new出来往JTabbedPane里add。TextbookPanel内部持有JTable和DefaultTableModel刷新数据的方法单独暴露出来这样切换Tab不会丢状态新增一个Tab也不用动主窗口代码。4.3 JTable展示教材列表模型和视图分离JTable最核心的概念是TableModel。直接用DefaultTableModel能应付大部分课设场景把查询结果的行列塞进去就能显示。常见写法是DefaultTableModel model new DefaultTableModel(); model.setColumnIdentifiers(new String[]{编号, 书名, 作者, 库存}); JTable table new JTable(model); ListTextbook list textbookDao.listByKeyword(keyword); model.setRowCount(0); // 先清空旧行 for (Textbook t : list) { model.addRow(new Object[]{t.getBookId(), t.getBookName(), t.getAuthor(), t.getStockQuantity()}); }setRowCount(0)再addRow是刷新表格的常用做法原理是让TableModel触发TableModelEventJTable监听后重绘。如果只是改了List不改Model界面永远不会变。注意列顺序要和setColumnIdentifiers一致否则数据会错行错列查半小时都找不到原因。想在表格上方加搜索框不要自己遍历行。JTable自带TableRowSorter支持按列排序和过滤。给表格设置RowSorter再监听搜索框文档变化更新过滤规则性能和写法的成熟度都更高。4.4 领用审批事务边界放在Service层领用审批是系统里唯一涉及两张表的事务操作先改borrow_order的状态为APPROVED再扣减textbook的库存。两步必须在一个事务里否则会出现“订单批了但库存没扣”或者相反的情况。public boolean approveOrder(int orderId, int bookId, int quantity) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); try (PreparedStatement ps conn.prepareStatement( UPDATE borrow_order SET statusAPPROVED, audit_timeNOW() WHERE order_id?)) { ps.setInt(1, orderId); ps.executeUpdate(); } try (PreparedStatement ps conn.prepareStatement( UPDATE textbook SET stock_quantity stock_quantity - ? WHERE book_id? AND stock_quantity ?)) { ps.setInt(1, quantity); ps.setInt(2, bookId); ps.setInt(3, quantity); if (ps.executeUpdate() 0) { conn.rollback(); return false; } } conn.commit(); return true; } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { e.addSuppressed(ex); } return false; } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { // 关闭异常忽略 } } } }逻辑是先关自动提交依次执行两个UPDATE任一步失败就rollbackfinally里把连接还回池子。第二个UPDATE带上stock_quantity ?条件把“库存是否足够”的判断下沉到SQL层比先查一次再更新更保险也避免并发下超额出库。finally里setAutoCommit(true)必不可少否则连接归还后自动提交状态是脏的下一个借到这个连接的方法会莫名其妙出问题。事务的位置在Service层不在UI层。按钮事件里直接拿返回值弹窗就行别把Connection和commit写到ActionListener里那个位置既不好复用也不好测试。注意Swing事件回调里不要做数据库事务和长查询界面会卡死。相关排查在第5章。5. SwingMySQL联调避坑五处反复翻车的现场还原5.1 中文乱码表格里全是问号现象JTable里查出来的书名显示成“???”或者程序写入数据库后变成乱码。原因三层里至少有一层字符集没对齐。最常见是建库时没指定字符集MySQL默认用了latin1其次是JDBC连接URL没带characterEncoding参数还有一种是SQL脚本文件本身被编辑器存成了非UTF-8编码Windows记事本默认ANSI导入后中文全乱。解决建库语句里明确写DEFAULT CHARACTER SET utf8mb4连接URL带上useUnicodetruecharacterEncodingutf8mb4SQL脚本保存时选“另存为UTF-8”用IDE的编码转换功能批量修正。写完代码先用命令行客户端查一遍中文判断是存储乱码还是显示乱码方向搞反会浪费时间。5.2 连不上MySQLerror 2002被误判成驱动问题现象程序启动报获取连接失败控制台里有“error 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock”但账号密码确认没写错。原因URL里写了localhostMySQL客户端默认走unix socketLinux或命名管道Windows而不是TCP端口另一个常见原因是MySQL服务根本没启动。解决先把jdbcUrl里的localhost换成127.0.0.1强制走TCP的3306端口。再用命令行验证MySQL本身mysql -h 127.0.0.1 -P 3306 -u tbms -p systemctl status mysqld # Linux 检查服务状态如果命令行也连不上去检查MySQL服务状态Windows看服务管理器里的MySQL服务Linux用systemctl看mysqld。我排查过不少案例最后发现只是服务没启动和驱动、代码完全无关。顺序上先命令行、再驱动类名、最后看代码能把排查时间缩短一半。5.3 Swing界面卡死点击按钮后整个窗口转圈现象点“查询”按钮后窗口拖不动、按钮没反馈过几秒才恢复数据库越大卡得越久。原因数据库查询在事件分发线程里同步执行。Swing是单线程模型所有UI刷新和事件响应都靠事件分发线程你在这个线程里做耗时操作整个窗口的重绘和点击都被堵死。这是Swing并发模型最经典的坑Java面试八股里也常问。解决用SwingWorker把查询挪到后台线程查完再回EDT刷新表格。课设里最简写法new SwingWorkerListTextbook, Void() { Override protected ListTextbook doInBackground() { return textbookDao.listByKeyword(keyword); } Override protected void done() { try { model.setRowCount(0); for (Textbook t : get()) { model.addRow(new Object[]{t.getBookId(), t.getBookName(), t.getStockQuantity()}); } } catch (Exception e) { JOptionPane.showMessageDialog(table, 查询失败 e.getMessage(), 错误, JOptionPane.ERROR_MESSAGE); } } }.execute();doInBackground在后台线程跑查询done回到EDT更新表格模型、视图、线程边界一次理清。不要为了绕过这个问题去调用Thread.sleep模拟那不是解决方案。5.4 JTable刷新了不显示改的是List而不是Model现象保存成功后界面没有新数据打开数据库确认数据已经写进去了。原因表格显示的内容由TableModel提供你只更新了内存里的List对象没有让TableModel触发事件。setRowCount(0)没调用时JTable收不到任何变化自然不会重绘。解决统一封装refreshTable(List list)方法里面固定先setRowCount(0)再循环addRow。查询、新增、修改、删除之后的刷新一律走这个方法。数据量大时考虑自定义TableModel重写getValueAt但课设里DefaultTableModel足够。这个坑的本质是理解和记住“界面只认Model不认List”。5.5 mysql-connector版本和Java版本错配现象Class.forName(com.mysql.jdbc.Driver)报ClassNotFoundException或者连接时提示“caching_sha2_password cannot be loaded”。原因Connector/J 5.x和8.x驱动类名不同8.x的Driver类在com.mysql.cj.jdbc.Driver同时MySQL 8默认认证插件caching_sha2_password需要高版本驱动支持老驱动不认。解决MySQL 8配8.0.33版驱动驱动类名写com.mysql.cj.jdbc.Driver别按老教程抄。如果项目环境只能用5.1.49就按3.3创建一个mysql_native_password账号。另外注意别把多个版本的mysql-connector-java jar同时放进lib目录jar冲突比缺jar更难查报错往往指向莫名其妙的AbstractMethodError。6. 答辩前最后一步用Navicat和操作日志验证系统闭环6.1 先用Navicat把SQL脚本和查询跑通写Java代码之前把建表SQL和业务SQL全部在Navicat或MySQL Workbench里过一遍。Navicat查询工具能直接看到执行结果和影响行数比在Java代码里打System.out.println调试快得多。把常用查询收集成一个SQL文件按登录、教材列表、领用审批、统计报表分好段。Java侧报错时把错误SQL复制进Navicat执行多数问题立刻水落石出是SQL写错还是字段名敲错一目了然。6.2 加一个操作日志表答辩时有话讲想给系统加分可以在三张表之外加一张operation_log记录谁在什么时间做了什么操作比如登录、审批、入库。DAO层每次写操作成功后插一条日志代码量不大但答辩能讲“审计设计”排障时也能按时间线还原操作。统计报表的汇总SQL可以先在Navicat里跑通再搬进Java的Service层必要时把复杂统计封装成存储过程Java侧只留一行调用这也是一种值得在答辩里提的优化。收尾说一个多年养成的习惯凡是带数据库的课设先把初始化SQL脚本存成init.sql并配好注释再碰Swing界面。顺序反了后面八成时间都在改表结构和改SQL之间来回折腾。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网