MyBatis+Swing班费管理系统:轻量级Java桌面应用实战
发布时间:2026/9/16 6:19:03来源:尧图网络
简介本资源是一套基于JavaMyBatisSwing开发的班费管理系统完整源码工程面向计算机相关专业在校学生、课程设计者及数据库初学者解决班级经费登记、查询、统计与可视化管理等实际教学场景需求适合作为数据库原理、Java程序设计或软件工程类课程大作业、毕业设计参考项目。压缩包共88个文件含36个Java业务与界面逻辑类、20个MyBatis映射XML配置、14个JFormDesigner生成的UI布局文件.jfd、12张系统运行效果截图含登录、主界面、收支录入、报表查询等关键页面以及cfms.sql数据库脚本、README说明文档和Maven依赖配置整体仅156KB轻量易部署。已有423人学习下载代码经实机测试全部可运行评审得分94.5分附带清晰目录结构与模块划分如ui、dao、service、model分层便于理解Swing桌面应用与MyBatis数据持久化集成方案亦可作为二次开发基础快速拓展功能。1. 这不是又一个“学生交作业式 Swing 界面”——它用 MyBatis 把班费流水管出了生产级数据契约意识你打开这个.zip文件第一眼看到的可能是一个带“添加支出”“查询余额”按钮的 Java Swing 窗口、几张截图里整齐排列的表格、还有banfei.sql文件里建好的三张表。但真正值得细看的是——它没用JDBC DriverManager.getConnection()硬编码连接没用Statement.executeUpdate()拼接 SQL 字符串而是把每笔班费录入、每条明细查询、每次余额汇总都通过 MyBatis 的Mapper接口和 XML 映射文件落地。这意味着SQL 与 Java 逻辑解耦、参数绑定防注入、结果集自动映射成FeeRecord对象、甚至SelectKey处理了主键回填。对数据库课程设计而言这已超出“能跑就行”的及格线对 Java 初学者而言这是第一次真实触摸 ORM 层如何把“人话需求”翻译成“数据库契约”。适合正在做课设但不想被老师问住“为什么不用 PreparedStatement”“MyBatis 和 JDBC 本质区别在哪”的同学也适合想用最小技术栈零 Spring、零 Web 容器验证数据流闭环的轻量级桌面应用实践者。2. 从banfei.sql建库到 MyBatis 加载 Mapper四步打通数据链路2.1 先让数据库说话banfei.sql的三张表设计意图与约束逻辑banfei.sql并非简单堆砌CREATE TABLE其字段命名和约束隐含业务语义。例如fee_record表中CREATE TABLE fee_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id VARCHAR(16) NOT NULL COMMENT 学号唯一标识学生, amount DECIMAL(10,2) NOT NULL CHECK (amount 0) COMMENT 金额必须为正数, category ENUM(收入,支出) NOT NULL COMMENT 收支类型强制二选一, remark VARCHAR(100) DEFAULT COMMENT 备注允许为空但不建议, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 记录时间自动填充 );提示ENUM类型在 MySQL 中虽简洁但实际开发中更推荐用TINYINT 字典表替代避免后期新增类型如“退费”需 DDL 变更。此处用ENUM是课程设计常见折中理解其限制比盲目替换更重要。该脚本还包含student_info存姓名、班级和fee_summary按班级/月份聚合余额三者通过student_id关联。执行时需确保 MySQL 版本 ≥ 5.7支持CURRENT_TIMESTAMP默认值若用 MariaDB 或较新 MySQL 8.0可直接source banfei.sql导入若用低版本需手动将DEFAULT CURRENT_TIMESTAMP改为DEFAULT NOW()。2.2 MyBatis 核心配置mybatis-config.xml如何精准定位数据库与映射文件mybatis-config.xml是整个 ORM 的中枢其关键段落如下configuration environments defaultdevelopment environment iddevelopment transactionManager typeJDBC/ dataSource typePOOLED property namedriver valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/banfei?useSSLfalseamp;serverTimezoneAsia/Shanghaiamp;characterEncodingutf8/ property nameusername valueroot/ property namepassword value123456/ /dataSource /environment /environments mappers mapper resourcemappers/FeeRecordMapper.xml/ mapper resourcemappers/StudentInfoMapper.xml/ /mappers /configuration注意url中的amp;是 XML 实体转义实际运行时会被解析为serverTimezoneAsia/Shanghai解决时区导致的时间错乱characterEncodingutf8避免中文备注乱码。若连接失败优先检查com.mysql.cj.jdbc.Driver是否在lib/目录下MySQL 8.0 必须用此驱动旧版com.mysql.jdbc.Driver已废弃。mappers节点指明了 XML 映射文件路径这些文件必须放在src/main/resources/mappers/下Maven 结构或src/mappers/传统结构否则SqlSessionFactoryBuilder会抛出org.apache.ibatis.binding.BindingException: Type interface xxx.FeeRecordMapper is not known to the MapperRegistry。2.3 Mapper 接口与 XML 的双向绑定为什么FeeRecordMapper.java必须有对应 XMLFeeRecordMapper.java定义方法签名FeeRecordMapper.xml实现 SQL 细节二者通过全限定名严格绑定// FeeRecordMapper.java public interface FeeRecordMapper { int insert(FeeRecord record); // 对应 XML 中的 insert idinsert ListFeeRecord selectByStudentId(String studentId); // 对应 select idselectByStudentId BigDecimal sumAmountByCategory(String category); // 对应 select idsumAmountByCategory }!-- FeeRecordMapper.xml -- mapper namespacecom.example.banfei.mapper.FeeRecordMapper insert idinsert parameterTypecom.example.banfei.model.FeeRecord useGeneratedKeystrue keyPropertyid INSERT INTO fee_record (student_id, amount, category, remark) VALUES (#{studentId}, #{amount}, #{category}, #{remark}) /insert select idselectByStudentId resultTypecom.example.banfei.model.FeeRecord SELECT * FROM fee_record WHERE student_id #{studentId} ORDER BY create_time DESC /select /mapper关键参数说明useGeneratedKeystruekeyPropertyid让 MyBatis 在INSERT后自动将数据库生成的AUTO_INCREMENT主键值回填到record.id字段#{}是预编译占位符彻底规避 SQL 注入resultType指定结果集映射目标类要求FeeRecord的字段名如studentId与数据库列名student_id遵循驼峰转下划线规则或通过resultMap显式映射。2.4 Swing 界面如何调用 MyBatisMainFrame.java中的SqlSession生命周期管理Swing 是单线程 GUI 框架但 MyBatis 的SqlSession不是线程安全的。常见错误是全局单例SqlSession导致并发操作崩溃。正确做法是在每个业务操作如点击“添加”按钮内创建并关闭// 在按钮事件监听器中 addButton.addActionListener(e - { SqlSession session null; try { session MyBatisUtil.getSqlSessionFactory().openSession(); // 获取新会话 FeeRecordMapper mapper session.getMapper(FeeRecordMapper.class); FeeRecord record new FeeRecord(); record.setStudentId(studentIdField.getText().trim()); record.setAmount(new BigDecimal(amountField.getText().trim())); record.setCategory(categoryComboBox.getSelectedItem().toString()); record.setRemark(remarkArea.getText().trim()); mapper.insert(record); // 执行插入 session.commit(); // 必须显式提交 JOptionPane.showMessageDialog(null, 添加成功); } catch (Exception ex) { if (session ! null) session.rollback(); // 异常时回滚 JOptionPane.showMessageDialog(null, 添加失败 ex.getMessage()); } finally { if (session ! null) session.close(); // 必须关闭释放资源 } });提示MyBatisUtil是封装SqlSessionFactory构建的工具类通常用静态块初始化一次。session.close()不可省略否则连接池耗尽后所有操作将阻塞超时。3. Swing 界面与 MyBatis 数据交互的实战细节从录入校验到余额实时刷新3.1 输入校验不能只靠前端MyBatis 层的SelectKey与数据库约束协同防御班费系统最易出错的是金额输入。用户可能输入-50或abcSwing 文本框无法阻止必须由后端拦截。FeeRecordMapper.xml中的insert语句已依赖数据库CHECK (amount 0)但 MyBatis 可提前拦截// 在插入前校验 String amountStr amountField.getText().trim(); if (!amountStr.matches(^\\d(\\.\\d{1,2})?$)) { JOptionPane.showMessageDialog(null, 金额格式错误请输入数字最多两位小数); return; } BigDecimal amount new BigDecimal(amountStr); if (amount.compareTo(BigDecimal.ZERO) 0) { JOptionPane.showMessageDialog(null, 金额必须大于零); return; }为什么双重校验前端校验提升用户体验数据库约束保证数据一致性。MyBatis 的#{amount}会将BigDecimal安全转换为 SQL 参数而CHECK约束是最后一道防线。若跳过前端校验直接提交MyBatis 会捕获SQLException并抛出org.apache.ibatis.exceptions.PersistenceException此时session.rollback()确保事务不污染数据。3.2 查询结果动态渲染JTable的DefaultTableModel如何与 MyBatis 的ListFeeRecord对接Swing 的JTable不直接接受List需通过DefaultTableModel中转。FeeRecordMapper.selectByStudentId()返回ListFeeRecord需转换为二维数组private void loadRecordsForStudent(String studentId) { SqlSession session MyBatisUtil.getSqlSessionFactory().openSession(); try { FeeRecordMapper mapper session.getMapper(FeeRecordMapper.class); ListFeeRecord records mapper.selectByStudentId(studentId); // 清空现有模型 DefaultTableModel model (DefaultTableModel) table.getModel(); model.setRowCount(0); // 清空行 // 逐行添加 for (FeeRecord r : records) { Object[] row { r.getId(), r.getStudentId(), r.getAmount(), r.getCategory(), r.getRemark(), r.getCreateTime() }; model.addRow(row); } } finally { session.close(); } }注意model.setRowCount(0)比循环model.removeRow(0)更高效r.getCreateTime()返回java.util.DateJTable默认显示为yyyy-MM-dd HH:mm:ss格式无需额外格式化。若需自定义日期列显示可重写JTable的getColumnClass()方法返回Date.class再设置JTable.setDefaultRenderer(Date.class, new DateCellRenderer())。3.3 余额汇总的两种实现SQL 聚合 vs Java 内存计算何时选哪一种系统需显示“当前总余额”即SUM(CASE WHEN category收入 THEN amount ELSE -amount END)。有两种方式方式一SQL 聚合推荐select idgetTotalBalance resultTypejava.math.BigDecimal SELECT COALESCE(SUM( CASE WHEN category 收入 THEN amount ELSE -amount END ), 0) AS balance FROM fee_record /select优点数据库计算快网络传输量小只传一个BigDecimal缺点无法缓存中间结果。方式二Java 内存计算ListFeeRecord allRecords mapper.selectAll(); // 查询全部记录 BigDecimal balance BigDecimal.ZERO; for (FeeRecord r : allRecords) { if (收入.equals(r.getCategory())) { balance balance.add(r.getAmount()); } else { balance balance.subtract(r.getAmount()); } }优点便于后续扩展如按日统计趋势缺点全表扫描压力大数据量大时卡顿。决策依据班费数据量通常 1000 条两种方式均可。但课程设计应体现“数据库擅长聚合Java 擅长逻辑”的分层思想故优先用 SQL 聚合。若需展示“本月收入/支出柱状图”再考虑内存计算。4. MyBatis 日志调试与 Swing 线程安全排查“界面卡死”和“查不到数据”的真实原因4.1 开启 MyBatis 日志用log4j2.xml看清每一条 SQL 的执行真相Swing 界面无响应或查询为空90% 源于 SQL 未执行或执行失败。MyBatis 默认不打印 SQL需配置日志框架。在src/main/resources/log4j2.xml中添加Configuration statusWARN Appenders Console nameConsole targetSYSTEM_OUT PatternLayout pattern%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n/ /Console /Appenders Loggers !-- 打印 MyBatis 执行的 SQL -- Logger nameorg.apache.ibatis leveldebug additivityfalse AppenderRef refConsole/ /Logger !-- 打印具体 Mapper 的 SQL -- Logger namecom.example.banfei.mapper leveltrace additivityfalse AppenderRef refConsole/ /Logger Root levelerror AppenderRef refConsole/ /Root /Loggers /Configuration启用后控制台将输出14:22:31.123 [AWT-EventQueue-0] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection 14:22:31.125 [AWT-EventQueue-0] DEBUG com.example.banfei.mapper.FeeRecordMapper - Preparing: SELECT * FROM fee_record WHERE student_id ? ORDER BY create_time DESC 14:22:31.126 [AWT-EventQueue-0] DEBUG com.example.banfei.mapper.FeeRecordMapper - Parameters: 2023001(String) 14:22:31.128 [AWT-EventQueue-0] DEBUG com.example.banfei.mapper.FeeRecordMapper - Total: 3关键信息解读Preparing行显示实际执行的 SQL已替换#{}占位符Parameters行显示传入参数值Total: 3表示返回 3 条记录。若Total为 0检查student_id是否存在若无Preparing行说明 Mapper 方法未被调用需回溯 Swing 事件监听器是否注册成功。4.2 Swing 的 EDT 线程陷阱为什么数据库操作不能在ActionListener中直接耗时执行Swing 的所有 UI 更新必须在 Event Dispatch Thread (EDT) 执行但 MyBatis 的session.selectList()是阻塞 I/O 操作。若在ActionListener中直接调用界面将完全冻结直到查询完成// ❌ 错误示范阻塞 EDT searchButton.addActionListener(e - { ListFeeRecord list mapper.selectByStudentId(id); // 网络延迟时界面假死 updateTable(list); // 此时才更新表格 });正确做法是用SwingWorker将耗时操作移出 EDT// ✅ 正确异步查询EDT 更新 UI searchButton.addActionListener(e - { String id studentIdField.getText().trim(); new SwingWorkerListFeeRecord, Void() { Override protected ListFeeRecord doInBackground() throws Exception { SqlSession session MyBatisUtil.getSqlSessionFactory().openSession(); try { return session.getMapper(FeeRecordMapper.class).selectByStudentId(id); } finally { session.close(); } } Override protected void done() { try { ListFeeRecord records get(); // 获取 doInBackground 返回值 updateTable(records); // 在 EDT 中安全更新 UI } catch (Exception ex) { JOptionPane.showMessageDialog(null, 查询失败 ex.getMessage()); } } }.execute(); // 启动异步任务 });为什么必须用SwingWorkerSwingWorker自动处理线程切换doInBackground()在后台线程执行done()回到 EDT 执行 UI 更新。若用Thread手动启动updateTable()必须用SwingUtilities.invokeLater()包裹否则抛出java.lang.IllegalStateException: java.awt.Component cannot be null。4.3 数据库连接池配置mybatis-config.xml中POOLED的三个关键参数调优dataSource typePOOLED并非开箱即用需根据班费系统的实际负载调整。默认配置可能在高并发测试时暴露问题参数名默认值课程设计建议值说明poolMaximumActiveConnections105班费系统极少并发5 个活跃连接足够避免 MySQL 连接数超限poolMaximumIdleConnections53空闲连接数减少资源占用poolMaximumWait20000ms5000ms连接池获取连接超时时间5 秒足够诊断网络或配置问题修改方式在dataSource内添加property namepoolMaximumActiveConnections value5/ property namepoolMaximumIdleConnections value3/ property namepoolMaximumWait value5000/验证方法故意将username设为错误值观察poolMaximumWait是否生效——若 5 秒后抛出org.apache.ibatis.exceptions.PersistenceException: Cause: java.sql.SQLException: Connections could not be acquired from the underlying database!则配置生效。5. 从课设到可演进架构剥离 Swing 与 MyBatis 的耦合为未来接入 Web 层铺路5.1 提取 Service 层用FeeService.java隔离业务逻辑与界面交互当前代码中Swing 事件监听器直接调用FeeRecordMapper导致业务逻辑如“添加记录需同步更新 summary 表”散落在 UI 层。重构第一步是创建FeeServicepublic class FeeService { private final FeeRecordMapper recordMapper; private final FeeSummaryMapper summaryMapper; public FeeService(SqlSessionFactory factory) { this.recordMapper factory.openSession().getMapper(FeeRecordMapper.class); this.summaryMapper factory.openSession().getMapper(FeeSummaryMapper.class); } public boolean addRecord(FeeRecord record) { SqlSession session null; try { session MyBatisUtil.getSqlSessionFactory().openSession(); recordMapper session.getMapper(FeeRecordMapper.class); summaryMapper session.getMapper(FeeSummaryMapper.class); recordMapper.insert(record); // 同步更新汇总表伪代码实际需按班级/月份更新 summaryMapper.updateBalance(record.getStudentId(), record.getAmount(), record.getCategory()); session.commit(); return true; } catch (Exception e) { if (session ! null) session.rollback(); return false; } finally { if (session ! null) session.close(); } } }好处Swing 界面只需调用feeService.addRecord(record)不再关心 Mapper 和事务未来若改用 Spring Boot只需将FeeService声明为ServiceMyBatis 的Mapper注解自动注入UI 层Swing 或 Web Controller完全复用。5.2 MyBatis 的SelectProvider动态 SQL应对“按多条件模糊查询”的灵活扩展课设要求“按学号、姓名、日期范围查询”硬编码 SQL 会爆炸式增长。MyBatis 的SelectProvider可动态拼接// FeeRecordMapper.java public interface FeeRecordMapper { SelectProvider(type FeeRecordSqlProvider.class, method buildSelect) ListFeeRecord selectByConditions(Param(studentId) String studentId, Param(name) String name, Param(startDate) String startDate, Param(endDate) String endDate); } // FeeRecordSqlProvider.java public class FeeRecordSqlProvider { public String buildSelect(MapString, Object params) { StringBuilder sql new StringBuilder(SELECT * FROM fee_record WHERE 11); if (params.get(studentId) ! null !.equals(params.get(studentId))) { sql.append( AND student_id LIKE CONCAT(%, #{studentId}, %)); } if (params.get(name) ! null !.equals(params.get(name))) { sql.append( AND student_id IN (SELECT student_id FROM student_info WHERE name LIKE CONCAT(%, #{name}, %))); } if (params.get(startDate) ! null !.equals(params.get(startDate))) { sql.append( AND create_time #{startDate}); } if (params.get(endDate) ! null !.equals(params.get(endDate))) { sql.append( AND create_time #{endDate}); } sql.append( ORDER BY create_time DESC); return sql.toString(); } }优势SQL 拼接逻辑集中易于单元测试LIKE CONCAT(%, #{xxx}, %)防止 SQL 注入Param注解确保#{studentId}正确绑定。Swing 界面只需收集条件字段调用mapper.selectByConditions(...)即可无需修改 XML。5.3 数据库迁移准备用Flyway管理banfei.sql的版本演进当前banfei.sql是初始版本但实际开发中表结构会迭代如新增“审核状态”字段。手动生成新 SQL 并手动执行极易出错。引入 Flyway 后目录结构变为src/main/resources/db/migration/ ├── V1__init.sql # 对应原始 banfei.sql ├── V2__add_audit_status.sql # 新增字段 └── V3__index_on_student_id.sql # 添加索引V1__init.sql内容即原banfei.sqlFlyway 会自动按版本号顺序执行。在mybatis-config.xml初始化前加入 Flyway 启动// 在 MyBatisUtil 静态块中 Flyway flyway Flyway.configure() .dataSource(jdbc:mysql://localhost:3306/banfei, root, 123456) .locations(classpath:db/migration) .load(); flyway.migrate(); // 执行迁移价值团队协作时新人拉取代码后mvn clean compile即自动建库建表生产环境升级只需flyway migrate一行命令。课程设计虽单人开发但此习惯能无缝衔接到企业级项目。本文还有配套的精品资源点击获取
网站建设高端定制企业官网