简介:本资源是一套基于Java+MyBatis+Swing开发的班费管理系统完整源码工程,面向计算机相关专业在校学生、课程设计者及数据库初学者,解决班级经费登记、查询、统计与可视化管理等实际教学场景需求,适合作为数据库原理、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 default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/banfei?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments> <mappers> <mapper resource="mappers/FeeRecordMapper.xml"/> <mapper resource="mappers/StudentInfoMapper.xml"/> </mappers> </configuration>注意:
url中的&是 XML 实体转义,实际运行时会被解析为&;serverTimezone=Asia/Shanghai解决时区导致的时间错乱;characterEncoding=utf8避免中文备注乱码。若连接失败,优先检查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必须有对应 XML
FeeRecordMapper.java定义方法签名,FeeRecordMapper.xml实现 SQL 细节,二者通过全限定名严格绑定:
// FeeRecordMapper.java public interface FeeRecordMapper { int insert(FeeRecord record); // 对应 XML 中的 <insert id="insert"> List<FeeRecord> selectByStudentId(String studentId); // 对应 <select id="selectByStudentId"> BigDecimal sumAmountByCategory(String category); // 对应 <select id="sumAmountByCategory"> }<!-- FeeRecordMapper.xml --> <mapper namespace="com.example.banfei.mapper.FeeRecordMapper"> <insert id="insert" parameterType="com.example.banfei.model.FeeRecord" useGeneratedKeys="true" keyProperty="id"> INSERT INTO fee_record (student_id, amount, category, remark) VALUES (#{studentId}, #{amount}, #{category}, #{remark}) </insert> <select id="selectByStudentId" resultType="com.example.banfei.model.FeeRecord"> SELECT * FROM fee_record WHERE student_id = #{studentId} ORDER BY create_time DESC </select> </mapper>关键参数说明:
useGeneratedKeys="true"+keyProperty="id"让 MyBatis 在INSERT后自动将数据库生成的AUTO_INCREMENT主键值回填到record.id字段;#{}是预编译占位符,彻底规避 SQL 注入;resultType指定结果集映射目标类,要求FeeRecord的字段名(如studentId)与数据库列名(student_id)遵循驼峰转下划线规则,或通过<resultMap>显式映射。
2.4 Swing 界面如何调用 MyBatis:MainFrame.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或abc,Swing 文本框无法阻止,必须由后端拦截。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 的List<FeeRecord>对接
Swing 的JTable不直接接受List,需通过DefaultTableModel中转。FeeRecordMapper.selectByStudentId()返回List<FeeRecord>,需转换为二维数组:
private void loadRecordsForStudent(String studentId) { SqlSession session = MyBatisUtil.getSqlSessionFactory().openSession(); try { FeeRecordMapper mapper = session.getMapper(FeeRecordMapper.class); List<FeeRecord> 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.Date,JTable默认显示为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 id="getTotalBalance" resultType="java.math.BigDecimal"> SELECT COALESCE(SUM( CASE WHEN category = '收入' THEN amount ELSE -amount END ), 0) AS balance FROM fee_record </select>优点:数据库计算快,网络传输量小(只传一个BigDecimal);缺点:无法缓存中间结果。
方式二:Java 内存计算
List<FeeRecord> 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 status="WARN"> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </Console> </Appenders> <Loggers> <!-- 打印 MyBatis 执行的 SQL --> <Logger name="org.apache.ibatis" level="debug" additivity="false"> <AppenderRef ref="Console"/> </Logger> <!-- 打印具体 Mapper 的 SQL --> <Logger name="com.example.banfei.mapper" level="trace" additivity="false"> <AppenderRef ref="Console"/> </Logger> <Root level="error"> <AppenderRef ref="Console"/> </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 -> { List<FeeRecord> list = mapper.selectByStudentId(id); // 网络延迟时界面假死 updateTable(list); // 此时才更新表格 });正确做法是用SwingWorker将耗时操作移出 EDT:
// ✅ 正确:异步查询,EDT 更新 UI searchButton.addActionListener(e -> { String id = studentIdField.getText().trim(); new SwingWorker<List<FeeRecord>, Void>() { @Override protected List<FeeRecord> doInBackground() throws Exception { SqlSession session = MyBatisUtil.getSqlSessionFactory().openSession(); try { return session.getMapper(FeeRecordMapper.class).selectByStudentId(id); } finally { session.close(); } } @Override protected void done() { try { List<FeeRecord> records = get(); // 获取 doInBackground 返回值 updateTable(records); // 在 EDT 中安全更新 UI } catch (Exception ex) { JOptionPane.showMessageDialog(null, "查询失败:" + ex.getMessage()); } } }.execute(); // 启动异步任务 });为什么必须用
SwingWorker?SwingWorker自动处理线程切换:doInBackground()在后台线程执行,done()回到 EDT 执行 UI 更新。若用Thread手动启动,updateTable()必须用SwingUtilities.invokeLater()包裹,否则抛出java.lang.IllegalStateException: java.awt.Component cannot be null。
4.3 数据库连接池配置:mybatis-config.xml中POOLED的三个关键参数调优
<dataSource type="POOLED">并非开箱即用,需根据班费系统的实际负载调整。默认配置可能在高并发测试时暴露问题:
| 参数名 | 默认值 | 课程设计建议值 | 说明 |
|---|---|---|---|
poolMaximumActiveConnections | 10 | 5 | 班费系统极少并发,5 个活跃连接足够,避免 MySQL 连接数超限 |
poolMaximumIdleConnections | 5 | 3 | 空闲连接数,减少资源占用 |
poolMaximumWait | 20000ms | 5000ms | 连接池获取连接超时时间,5 秒足够诊断网络或配置问题 |
修改方式(在<dataSource>内添加):
<property name="poolMaximumActiveConnections" value="5"/> <property name="poolMaximumIdleConnections" value="3"/> <property name="poolMaximumWait" value="5000"/>验证方法:故意将
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 层。重构第一步是创建FeeService:
public 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声明为@Service,MyBatis 的@Mapper注解自动注入,UI 层(Swing 或 Web Controller)完全复用。
5.2 MyBatis 的@SelectProvider动态 SQL:应对“按多条件模糊查询”的灵活扩展
课设要求“按学号、姓名、日期范围查询”,硬编码 SQL 会爆炸式增长。MyBatis 的@SelectProvider可动态拼接:
// FeeRecordMapper.java public interface FeeRecordMapper { @SelectProvider(type = FeeRecordSqlProvider.class, method = "buildSelect") List<FeeRecord> 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(Map<String, Object> params) { StringBuilder sql = new StringBuilder("SELECT * FROM fee_record WHERE 1=1"); 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.sql,Flyway 会自动按版本号顺序执行。在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一行命令。课程设计虽单人开发,但此习惯能无缝衔接到企业级项目。
本文还有配套的精品资源,点击获取