资讯动态

Java校园卡系统:Swing GUI+JDBC事务+HSQLDB/MySQL迁移实战

发布时间:2026/9/16 18:50:38 来源:尧图企业网站定制
简介这是一份基于Java开发的轻量级校园卡管理系统实战项目面向Java初学者与课程设计学生聚焦食堂消费、手机充值、网费缴纳等典型校园场景助力掌握桌面应用开发全流程。资源包共30个文件含7个核心Java源码文件含GUI界面与业务逻辑、8个编译后class文件、10张功能界面截图如登录页、充值页、消费记录页以及classpath、project等Eclipse工程配置文件和说明文档整体703KB结构完整开箱即用。已有1845人学习下载。读者可直接导入Eclipse运行调试深入理解Swing界面构建、JDBC连接MySQL数据库、账户余额事务处理、密码MD5加密存储等关键技术点并通过源码与截图对照快速厘清MVC分层逻辑与事件驱动机制是巩固Java面向对象编程与数据库交互能力的优质实践素材。1. 这不是“学生交作业式”Java项目一个能真正在食堂刷卡、查余额、充话费的校园卡系统你打开 Eclipse双击SCHOOLCARD文件夹里的.projectRun As → Java Application界面弹出来——不是黑窗口是带「充值」「消费」「查询」按钮的 Swing 窗口点「查询」输入学号2023001立刻显示余额¥86.50、最近三笔消费食堂 3.5 元、网费 20 元、手机充值 50 元点「充值」选金额 100 元确认后余额变成¥186.50数据库里多了一条INSERT INTO transaction_log (...) VALUES (...)记录。这不是教学 Demo它跑在本地 HSQLDB 上但结构清晰到可直接替换为 MySQL它没用 Spring Boot却把 JDBC 连接池、事务回滚、GUI 事件线程隔离、密码 MD5 加盐这些真实生产场景的约束全写进去了。适合刚学完 Java 基础、正卡在「怎么把书上语法变成能运行的系统」阶段的开发者也适合想快速复现一个「有 GUI、有 DB、有业务逻辑」完整链路的中级工程师——它不炫技但每行代码都在回答「为什么这里要用 try-with-resources 而不是普通 try-catch」「为什么 Swing 的 actionPerformed 必须在 EventQueue.invokeLater 里调用」。2. 从 Eclipse 工程结构反推系统骨架src/bin/.classpath/.settings 是什么又为什么不能删2.1 工程目录即架构图.project和.classpath定义了 Java 编译的边界Eclipse 项目不是靠文件夹名识别的而是靠.project和.classpath两个隐藏文件。打开.project你会看到nameSCHOOLCARD/name和buildSpecbuildCommandnameorg.eclipse.jdt.core.javabuilder/name——这告诉 Eclipse这是一个 Java 项目用 JDTJava Development Tools编译器构建。.classpath更关键它定义了类路径来源classpath classpathentry kindsrc pathsrc/ classpathentry kindcon pathorg.eclipse.jdt.launching.JRE_CONTAINER/ classpathentry kindlib pathlib/hsqldb.jar/ classpathentry kindoutput pathbin/ /classpath提示classpathentry kindsrc pathsrc/表示所有 Java 源码必须放在src目录下classpathentry kindlib pathlib/hsqldb.jar/表明数据库驱动是 HSQLDB轻量级嵌入式数据库而非 MySQL 或 Oracleclasspathentry kindoutput pathbin/指定编译后的.class文件输出到bin目录——这就是为什么你双击Main.java运行时Eclipse 会自动把src/com/example/card/Main.class写进bin再从bin加载执行。如果手动删除.classpathEclipse 会报错「The project was not built since its build path is incomplete」若把lib/hsqldb.jar移走运行时必抛ClassNotFoundException: org.hsqldb.jdbc.JDBCDataSource。这不是配置冗余而是 Java 类加载机制的硬性要求JVM 只认 classpath 里声明的路径。2.2src下的真实分层com.example.card包结构暴露了 MVC 的朴素实现展开src目录结构如下src/ ├── com/ │ └── example/ │ └── card/ │ ├── model/ // 实体类CardUser.java, Transaction.java │ ├── dao/ // 数据访问对象CardUserDAO.java, TransactionDAO.java │ ├── service/ // 业务逻辑CardService.java含充值、扣款、查询 │ ├── ui/ // GUI 界面MainFrame.java, LoginDialog.java, BalancePanel.java │ └── Main.java // 程序入口new MainFrame().setVisible(true);这不是随意命名。model包里CardUser类字段与数据库card_user表完全对应// src/com/example/card/model/CardUser.java public class CardUser { private String studentId; // 对应表字段 student_id VARCHAR(12) private String name; // name VARCHAR(20) private String password; // password CHAR(32) —— 存的是 MD5(hex) 值 private double balance; // balance DECIMAL(10,2) // getter/setter 省略 }dao包中CardUserDAO的核心方法findByStudentId()直接暴露 JDBC 操作细节// src/com/example/card/dao/CardUserDAO.java public CardUser findByStudentId(String studentId) throws SQLException { String sql SELECT * FROM card_user WHERE student_id ?; try (Connection conn DataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, studentId); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { CardUser user new CardUser(); user.setStudentId(rs.getString(student_id)); user.setName(rs.getString(name)); user.setPassword(rs.getString(password)); // 注意此处未解密仅比对哈希值 user.setBalance(rs.getDouble(balance)); return user; } } } return null; }参数说明try-with-resources自动关闭Connection/PreparedStatement/ResultSet避免连接泄漏ps.setString(1, studentId)使用占位符?防止 SQL 注入rs.getDouble(balance)直接映射数据库DECIMAL字段无需类型转换。这种写法虽不如 MyBatis 简洁但让你看清 JDBC 的每一层责任连接管理、预编译、结果集遍历。2.3bin目录是运行时真相.class文件如何被 JVM 加载编译后bin目录生成与src完全镜像的包结构bin/ └── com/ └── example/ └── card/ ├── model/ │ ├── CardUser.class │ └── Transaction.class ├── dao/ │ ├── CardUserDAO.class │ └── TransactionDAO.class // ... 其他 .class 文件 └── Main.classJVM 启动时-cp bin参数让类加载器从bin开始扫描。当你在Main.java中写new CardService().recharge(2023001, 100.0)JVM 会按com.example.card.service.CardService路径在bin下查找CardService.class加载并执行其字节码。如果bin下缺失某个.class就会报NoClassDefFoundError——这和编译期的Cannot resolve symbol是不同阶段的错误前者是运行时类路径问题后者是编译期引用问题。3. GUI 与数据库协同Swing 事件如何触发 JDBC 事务以及为什么必须加锁3.1ui.MainFrame的按钮监听器从点击到数据库更新的完整链路MainFrame.java中「充值」按钮的事件处理是典型 Swing-MVC 模式// src/com/example/card/ui/MainFrame.java private void initRechargeButton() { JButton rechargeBtn new JButton(充值); rechargeBtn.addActionListener(e - { String studentId studentIdField.getText().trim(); String amountStr amountField.getText().trim(); if (studentId.isEmpty() || amountStr.isEmpty()) { JOptionPane.showMessageDialog(this, 学号和金额不能为空); return; } try { double amount Double.parseDouble(amountStr); if (amount 0) { JOptionPane.showMessageDialog(this, 金额必须大于0); return; } // 关键业务逻辑委托给 service 层 boolean success cardService.recharge(studentId, amount); if (success) { JOptionPane.showMessageDialog(this, 充值成功当前余额 cardService.getBalance(studentId)); updateBalanceDisplay(studentId); // 刷新界面显示 } else { JOptionPane.showMessageDialog(this, 充值失败请检查学号); } } catch (NumberFormatException ex) { JOptionPane.showMessageDialog(this, 请输入有效数字); } catch (Exception ex) { JOptionPane.showMessageDialog(this, 系统错误 ex.getMessage()); } }); add(rechargeBtn); }这段代码表面简单实则包含三层控制输入校验层空值检查、数字格式验证Double.parseDouble、业务规则金额 0服务调用层cardService.recharge(...)封装了数据库操作屏蔽 UI 细节反馈层JOptionPane提示结果updateBalanceDisplay(...)主动刷新界面。注意actionPerformed回调默认在 Event Dispatch ThreadEDT中执行而 JDBC 操作如Connection.createStatement()是阻塞 I/O若耗时过长会导致界面卡死。本项目未做异步处理因此实际部署时需在recharge方法内加SwingWorker或ExecutorService否则用户点击按钮后界面冻结 2 秒以上。3.2service.CardService的事务控制recharge()方法如何保证数据一致性CardService.recharge()是整个系统最核心的业务方法它必须满足 ACID// src/com/example/card/service/CardService.java public boolean recharge(String studentId, double amount) { Connection conn null; try { conn DataSource.getConnection(); // 从连接池获取连接 conn.setAutoCommit(false); // 关键关闭自动提交开启事务 // 步骤1查询用户当前余额 CardUser user cardUserDAO.findByStudentId(studentId); if (user null) return false; // 步骤2更新用户余额 double newBalance user.getBalance() amount; cardUserDAO.updateBalance(studentId, newBalance); // 步骤3记录交易日志 Transaction tx new Transaction(); tx.setStudentId(studentId); tx.setType(RECHARGE); tx.setAmount(amount); tx.setBalanceAfter(newBalance); tx.setCreateTime(new Timestamp(System.currentTimeMillis())); transactionDAO.insert(tx); conn.commit(); // 所有操作成功提交事务 return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); // 任一环节失败回滚全部操作 } catch (SQLException rollbackEx) { rollbackEx.printStackTrace(); } } e.printStackTrace(); return false; } finally { if (conn ! null) { try { conn.close(); // 归还连接到池 } catch (SQLException closeEx) { closeEx.printStackTrace(); } } } }参数说明conn.setAutoCommit(false)是事务起点conn.commit()是成功终点conn.rollback()是失败兜底。若省略rollback()当updateBalance成功但insert(tx)失败时用户余额已增加却无交易记录造成资金黑洞。本项目用 HSQLDB其默认隔离级别为TRANSACTION_READ_COMMITTED可防止脏读但未显式设置setTransactionIsolation因此并发充值时可能出现不可重复读——这是真实系统中必须通过数据库行锁或应用层synchronized解决的问题。3.3 并发安全陷阱为什么CardService不是单例而DataSource必须是CardService在MainFrame中被new CardService()创建每次操作都新建实例。这看似浪费实则是线程安全设计// src/com/example/card/service/CardService.java public class CardService { private final CardUserDAO cardUserDAO new CardUserDAO(); private final TransactionDAO transactionDAO new TransactionDAO(); // 无共享状态每个实例独立 }而DataSource位于com.example.card.util.DataSource.java是典型的单例public class DataSource { private static DataSource instance; private static HikariConfig config; private static HikariDataSource ds; private DataSource() {} public static synchronized DataSource getInstance() { if (instance null) { instance new DataSource(); } return instance; } public static Connection getConnection() throws SQLException { return ds.getConnection(); // 从连接池取连接 } }为什么这样设计CardService本身无状态但若把它做成静态工具类如public static boolean recharge(...)多个线程同时调用会共享cardUserDAO实例而 DAO 内部的Connection若未正确关闭将导致连接泄漏。DataSource必须单例因为连接池是全局资源重复初始化会创建多个池浪费内存且无法复用连接。Eclipse 项目中lib/hsqldb.jar提供嵌入式数据库但生产环境换成 MySQL 时只需修改DataSource的HikariConfig参数如jdbc:mysql://localhost:3306/schoolcard其他代码零改动。4. 从 HSQLDB 迁移到 MySQL驱动替换、SQL 语法适配与连接池配置4.1 驱动替换三步法jar 包、URL、用户名密码的硬编码位置原项目使用 HSQLDB驱动 jar 是lib/hsqldb.jar连接 URL 为jdbc:hsqldb:file:db/schoolcard。迁移到 MySQL 需四步替换 jar 包删除lib/hsqldb.jar下载mysql-connector-java-8.0.33.jar放入lib/目录修改.classpath将classpathentry kindlib pathlib/hsqldb.jar/替换为classpathentry kindlib pathlib/mysql-connector-java-8.0.33.jar/重写DataSource的连接配置// src/com/example/card/util/DataSource.java static { config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/schoolcard?useSSLfalseserverTimezoneAsia/Shanghai); config.setUsername(root); config.setPassword(your_password); // 生产环境应从配置文件读取 config.setDriverClassName(com.mysql.cj.jdbc.Driver); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(30000); ds new HikariDataSource(config); }关键差异说明HSQLDB 的jdbc:hsqldb:file:...是文件路径MySQL 的jdbc:mysql://...是网络地址useSSLfalse关闭 SSL开发环境可省略证书配置serverTimezoneAsia/Shanghai解决时区问题否则new Timestamp(...)插入时间可能偏差 8 小时com.mysql.cj.jdbc.Driver是 MySQL 8 的驱动类名旧版是com.mysql.jdbc.Driver写错会报ClassNotFoundException。4.2 SQL 语法微调HSQLDB 与 MySQL 的 DDL/DML 差异表功能HSQLDB 语法MySQL 语法是否需修改原因说明创建用户表CREATE TABLE card_user (...);CREATE TABLE card_user (...) ENGINEInnoDB;是MySQL 需指定存储引擎主键自增id INTEGER GENERATED BY DEFAULT AS IDENTITYid INT AUTO_INCREMENT PRIMARY KEY是HSQLDB 用GENERATEDMySQL 用AUTO_INCREMENT时间戳字段create_time TIMESTAMPcreate_time DATETIME DEFAULT CURRENT_TIMESTAMP是MySQLTIMESTAMP有自动更新行为DATETIME更可控密码字段长度password CHAR(32)password VARCHAR(64)是MySQLCHAR(32)存 MD5 够用但VARCHAR更省空间执行建表语句前先在 MySQL 中手动创建数据库CREATE DATABASE schoolcard CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE schoolcard; -- 然后粘贴修改后的 CREATE TABLE 语句4.3 连接池参数调优HikariCP 的maximumPoolSize如何根据并发量设定HikariCP 的maximumPoolSize不是越大越好。假设你的校园卡系统日活 5000 人峰值并发请求约 200 QPS每秒查询/交易数每个 JDBC 操作平均耗时 50ms则理论所需连接数为连接数 并发请求数 × 平均响应时间秒 200 × 0.05 10但需预留缓冲故设maximumPoolSize15minimumIdle5保持 5 个空闲连接防冷启动延迟。若设为 100大量空闲连接会占用 MySQL 的max_connections默认 151导致其他应用连接失败。Eclipse 项目中DataSource的config.setMaximumPoolSize(10)是合理起点上线前用SHOW STATUS LIKE Threads_connected;观察实际连接数再动态调整。5. Eclipse 调试实战如何定位「充值成功但余额没变」这类典型问题5.1 断点打在哪三层调试法精准捕获数据流断点当用户报告「点了充值按钮弹窗说成功但余额还是旧的」不要猜用 Eclipse 调试器逐层验证UI 层断点在MainFrame.java的rechargeBtn.addActionListener内部第一行打断点确认事件是否触发Service 层断点在CardService.recharge()方法开头打断点F6 单步进入观察studentId和amount参数值是否正确DAO 层断点在CardUserDAO.updateBalance()的PreparedStatement.executeUpdate()后打断点检查返回值int rowsAffected—— 若为0说明WHERE student_id ?没匹配到任何行可能是学号输错或数据库里不存在该用户。技巧右键断点 → Breakpoint Properties → 勾选「Suspend when value changes」对user.getBalance()的返回值设条件断点当余额变化时暂停直接看到变更前后的值。5.2 日志埋点用java.util.logging替代System.out.printlnSystem.out.println在 Eclipse 控制台可见但生产环境会被重定向或丢失。项目中util.LoggerUtil提供了标准日志// src/com/example/card/util/LoggerUtil.java public class LoggerUtil { private static final Logger LOGGER Logger.getLogger(LoggerUtil.class.getName()); static { try { FileHandler fh new FileHandler(logs/app.log, true); fh.setFormatter(new SimpleFormatter()); LOGGER.addHandler(fh); } catch (IOException e) { e.printStackTrace(); } } public static void info(String msg) { LOGGER.info(msg); } public static void error(String msg, Throwable t) { LOGGER.severe(msg); } }在CardService.recharge()关键节点添加日志LOGGER.info(开始充值学号[ studentId ]金额[ amount ]); // ... 执行逻辑 ... LOGGER.info(充值完成新余额[ newBalance ]);日志文件logs/app.log会记录时间戳、线程名、消息比控制台输出更可靠。若发现日志里有SEVERE错误但界面无提示说明catch块吞掉了异常——这是常见 bug 来源。5.3 数据库验证用 Eclipse Database Explorer 直连 MySQL 查数据Eclipse 自带 Database Explorer 视图Window → Show View → Other → Database Development → Database Explorer。配置 MySQL 连接DriverMySQL Connector/J 8.0URLjdbc:mysql://localhost:3306/schoolcardUserrootPasswordyour_password连接后展开schoolcard→Tables→card_user右键「Open Table」直接查看student_id2023001的balance字段值。若 Java 界面显示 86.50而数据库里是 186.50说明 UI 没刷新——问题在updateBalanceDisplay()方法未正确调用JLabel.setText()若数据库里也是 86.50则updateBalanceSQL 执行失败需检查 DAO 的PreparedStatement参数绑定是否正确如ps.setDouble(1, newBalance)的索引是否与 SQL 中?顺序一致。注意Eclipse Database Explorer 的「Open Table」是只读的修改数据需用 SQL 语句执行UPDATE card_user SET balance 186.50 WHERE student_id 2023001;然后刷新视图验证。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价