尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Java Swing ATM系统实战:Swing+MySQL+c3p0全链路教学基座
简介这是一套基于Java Swing开发的ATM取款机系统完整实践项目面向Java初学者与高校课程设计学生聚焦GUI编程、JDBC数据库交互及基础金融业务逻辑实现。资源包含22个文件主体为14个Java源码覆盖登录、账户管理、存取款、转账、交易记录等核心模块、4个关键依赖jar包含c3p0连接池、MySQL驱动及DBUtils工具库以及初始化数据库结构与测试数据的atm.sql脚本压缩包仅1.98MB轻量易部署。已有1355人学习下载项目结构规范采用Eclipse标准工程配置含.project、.classpath、.settings开箱即用。读者可直接导入IDE运行深入理解Swing事件驱动机制、事务控制、密码安全处理、异常捕获与用户交互设计是掌握Java桌面应用开发全流程的典型教学范例。1. Swing实现ATM取款机系统一个能跑通、能调试、能改出真实业务逻辑的Java GUI教学基座你刚在IDE里双击运行ATMSystem.java界面弹出来了——登录框、按钮、账户余额显示都对但点“取款”就报NullPointerException或者连上MySQL后注册用户成功可第二天再登录密码明明没输错却提示“验证失败”。这不是玄学是这套 Swing MySQL 的 ATM 系统最真实的入门门槛。它不是玩具 Demo而是一个带完整 SQL 初始化脚本、使用 c3p0 连接池、封装了 DAO 层与事务控制、覆盖注册/登录/存取款/转账/交易记录全链路的 Java 桌面应用工程。它不追求炫酷动效但每一步操作背后都有 JDBC 执行日志、SQL 参数绑定、事务回滚点和密码加密逻辑。适合正在学 Java GUI 编程、想把 JDBC 和数据库设计串起来练手的中级开发者——尤其当你已经写过 CRUD 控制台程序但还没真正用 Swing 搭过带状态管理、异常反馈、多线程安全的桌面系统时这个项目就是你从“会写”到“能交付”的关键跳板。它不教你 Swing 基础组件怎么拖拽而是直接让你面对为什么JPasswordField.getText()返回空为什么转账时两个账户余额更新不同步为什么atm.sql里user表的password字段是VARCHAR(64)而不是VARCHAR(32)这些问题的答案就藏在 src/cn 下那十几个.java文件和lib里四个 JAR 包的协作细节里。2. 从零启动环境准备、SQL建库与Swing主流程拆解2.1 JDK 版本与依赖包的硬性匹配关系这个项目明确打包了mysql-connector-java-5.1.48.jar、c3p0-0.9.5.5.jar、mchange-commons-java-0.2.19.jar和commons-dbutils-1.7.jar。注意它不是为 JDK 17 或 JDK 21 设计的。实测在 JDK 8u291即 1.8.0_291下完全兼容若强行用 JDK 11会在ComboPooledDataSource初始化时抛java.lang.NoClassDefFoundError: javax/naming/InitialContext——因为 JDK 11 移除了java.se.ee模块而 c3p0 0.9.5.5 仍依赖 JNDI。解决方案只有两个降级 JDK或升级 c3p0 到 0.9.5.5 之后的版本如 0.9.5.6但后者需同步替换mchange-commons-java为 0.2.20且commons-dbutils需确认 1.7 是否兼容新 c3p0。我建议直接用 JDK 8这是血泪经验曾花 3 小时排查InitialContext报错最后发现只是 JDK 版本越界。提示不要试图用 Maven 自动拉取这些 JAR——项目中已固化版本号手动复制lib目录下全部 4 个 JAR 到 Eclipse 的 Build Path顺序不能错mchange-commons-java必须在c3p0之前否则ComboPooledDataSource类加载失败。2.2 atm.sql 的三处关键结构设计与初始化陷阱atm.sql不是简单建表语句它定义了整个系统的数据契约。打开文件你会看到三张核心表user、transaction_log、account_info部分变体可能合并为一张。重点看以下字段CREATE TABLE user ( id INT(11) NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, -- 注意64位非32因用SHA-256加密 balance DECIMAL(10,2) DEFAULT 0.00, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;password长度为 64说明密码加密算法是 SHA-256输出 64 字符十六进制字符串而非 MD532 位。若你后续想换 BCrypt必须同步改字段长度为VARCHAR(100)并重写PasswordUtil.encrypt()方法。balance类型为DECIMAL(10,2)这是金融计算的底线。用FLOAT或DOUBLE会导致 0.1 0.2 ≠ 0.3取款 100.05 元时余额计算偏差会累积。account_info表如有通常含account_number字段类型为CHAR(16)或BIGINT但项目中实际用的是username作为主键关联——这意味着一个用户名对应唯一账户不支持同名多卡。这是教学简化但你要清楚真实 ATM 系统必有独立账号体系。执行atm.sql前务必确认 MySQL 字符集为utf8mb4否则中文用户名插入会乱码。命令行执行mysql -u root -p --default-character-setutf8mb4 atm_db atm.sql若报错Unknown database atm_db先手动建库CREATE DATABASE atm_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;2.3 Swing 主窗口生命周期与事件驱动链路整个 GUI 的入口是src/cn/ATMSystem.java中的main()方法它调用SwingUtilities.invokeLater()启动 EDTEvent Dispatch Thread。关键不是“怎么画按钮”而是状态如何流转启动 →LoginFrame显示继承JFrame输入用户名密码 → 点击“登录” → 触发LoginButtonActionListener.actionPerformed()该监听器调用UserService.login(username, password)→ 底层走UserDao.findByUsername()查询数据库查询成功 →LoginFrame.dispose()销毁登录窗 →new MainFrame().setVisible(true)MainFrame中每个功能按钮取款、存款等都绑定独立ActionListener点击后弹出对应JDialog如WithdrawDialog注意所有对话框JDialog都设置为setModal(true)阻塞主窗操作这是 Swing 保证 UI 线程安全的惯用手法。但如果你在WithdrawDialog的“确认取款”按钮里直接写accountService.withdraw(...)并setVisible(false)就可能因数据库事务未提交导致余额未更新——正确做法是withdraw()方法内部必须开启事务成功后才dispose()对话框。3. 核心模块实战DAO 层封装、事务控制与密码加密逻辑3.1 c3p0 连接池配置与 DataSource 初始化连接池不是摆设。项目中src/cn/config/DBConfig.java定义了ComboPooledDataSource实例public class DBConfig { private static ComboPooledDataSource dataSource; static { dataSource new ComboPooledDataSource(); try { dataSource.setDriverClass(com.mysql.jdbc.Driver); // 注意MySQL 5.x 用此驱动类名 dataSource.setJdbcUrl(jdbc:mysql://localhost:3306/atm_db?useSSLfalseserverTimezoneUTC); dataSource.setUser(root); dataSource.setPassword(123456); // 生产环境严禁硬编码 dataSource.setInitialPoolSize(3); dataSource.setMaxPoolSize(10); dataSource.setMinPoolSize(2); dataSource.setMaxStatements(50); } catch (PropertyVetoException e) { throw new RuntimeException(初始化连接池失败, e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }setDriverClass(com.mysql.jdbc.Driver)是 MySQL 5.x 的标准驱动类。若你用 MySQL 8.x必须改为com.mysql.cj.jdbc.Driver否则ClassNotFoundException。useSSLfalseserverTimezoneUTC是必需参数。MySQL 5.7 默认启用 SSL且严格校验时区缺一不可否则连接超时或时区错误。getConnection()返回的Connection对象默认 auto-committrue所以所有涉及资金变动的操作取款、转账必须显式调用conn.setAutoCommit(false)并在 try-catch-finally 中控制 commit/rollback。3.2 使用 DbUtils 封装 DAOQueryRunner 与 BeanHandler 的配合commons-dbutils让 JDBC 操作更简洁。以UserDao.java的登录查询为例public User findByUsername(String username) throws SQLException { String sql SELECT id, username, password, balance, create_time FROM user WHERE username ?; QueryRunner qr new QueryRunner(DBConfig.getDataSource()); // 注意传入的是 DataSource非 Connection return qr.query(sql, new BeanHandler(User.class), username); }QueryRunner构造时传DataSource它内部自动获取Connection并管理关闭避免手动conn.close()。BeanHandlerUser自动将 ResultSet 映射为User对象要求User类字段名与数据库列名一致或通过ColumnProcessor自定义映射。关键陷阱qr.query()是查询方法不支持事务控制。若你在withdraw()方法中先查余额再更新必须用QueryRunner(Connection conn)构造器传入同一个Connection否则两次操作不在同一事务内。3.3 密码加密与验证SHA-256 盐值的硬编码实现密码不是明文存库。src/cn/util/PasswordUtil.java提供了加密方法public class PasswordUtil { private static final String SALT ATM_SALT_2023; // 危险生产环境必须动态盐值 public static String encrypt(String rawPassword) { try { MessageDigest md MessageDigest.getInstance(SHA-256); md.update(SALT.getBytes()); byte[] digest md.digest(rawPassword.getBytes()); StringBuilder hexString new StringBuilder(); for (byte b : digest) { String hex Integer.toHexString(0xff b); if (hex.length() 1) hexString.append(0); hexString.append(hex); } return hexString.toString(); } catch (Exception e) { throw new RuntimeException(密码加密失败, e); } } }SALT是硬编码字符串这是教学项目的妥协。真实系统必须为每个用户生成唯一随机盐如SecureRandom并存入数据库salt字段。加密结果是 64 位十六进制字符串与atm.sql中password VARCHAR(64)完全匹配。登录验证时UserService.login()会调用PasswordUtil.encrypt(inputPassword)再与数据库查出的password字段比对——不是解密而是重算哈希比对。4. 避坑指南Swing 线程安全、事务失效与 SQL 注入防御边界4.1 Swing EDT 线程阻塞导致界面假死的三种典型场景现象点击“转账”按钮后界面卡住 5 秒按钮无法响应直到转账完成才恢复。原因Swing 的事件监听器ActionListener默认在 EDT 中执行而accountService.transfer()内部包含耗时的数据库操作网络 IO SQL 执行阻塞了整个 UI 线程。解决用SwingWorker异步执行。例如在TransferButtonActionListener中new SwingWorkerVoid, Void() { Override protected Void doInBackground() throws Exception { transferService.transfer(fromAccount, toAccount, amount); // 耗时操作放这里 return null; } Override protected void done() { JOptionPane.showMessageDialog(null, 转账成功); updateBalanceDisplay(); // 更新界面必须在 done() 中它在 EDT 执行 } }.execute();现象JTable显示交易记录但新增一笔后表格不刷新。原因直接修改ListTransaction数据源但未通知TableModel。解决继承AbstractTableModel在addTransaction()方法中调用fireTableRowsInserted()。现象多次快速点击“取款”按钮触发多次取款请求余额被扣减多次。原因EDT 未禁用按钮用户可重复提交。解决点击后立即withdrawButton.setEnabled(false)done()中再setEnabled(true)。4.2 事务失效的四大隐形杀手现象转账时A 账户扣款成功B 账户未到账数据库出现不一致。原因 1Connection 未复用。transfer()方法中deductBalance()和addBalance()分别调用DBConfig.getConnection()获取两个独立连接各自 commit无法保证原子性。解决transfer()方法签名改为public void transfer(Connection conn, ...)由外层统一传入同一Connection。原因 2未捕获 SQLException。try块中conn.commit()成功但finally中conn.close()抛SQLException导致 commit 结果未被确认。解决close()放在try块内或用 try-with-resources需 JDK 7。原因 3MySQL 存储引擎非 InnoDB。MyISAM 不支持事务START TRANSACTION无效。解决建表时显式指定ENGINEInnoDB或执行ALTER TABLE user ENGINEInnoDB;。原因 4自动提交未关闭。conn.setAutoCommit(false)被遗漏每条 SQL 独立提交。解决在事务开始前强制设置并在 catch 块中conn.rollback()。4.3 SQL 注入防御的实操边界预编译真的万能吗项目中所有 DAO 查询均使用?占位符如SELECT * FROM user WHERE username ?这确实防住了经典注入。但仍有两处高危点风险点 1ORDER BY 动态字段。若代码中有String sql SELECT * FROM transaction_log ORDER BY sortField;sortField来自用户输入则sortFieldamount; DROP TABLE user;--可执行任意 SQL。加固白名单校验。if (!Arrays.asList(amount, time, type).contains(sortField)) throw new IllegalArgumentException();风险点 2LIKE 模糊查询的通配符。WHERE username LIKE ?中若传入%admin%没问题但若用户输入admin%%被当作通配符可能查出意外数据。加固对?参数中的%和_进行转义username.replace(%, \\%).replace(_, \\_)并在 SQL 中加ESCAPE \\。注意mysql-connector-java-5.1.48.jar默认开启allowUrlInLocalInfilefalse已禁用LOAD DATA LOCAL INFILE攻击无需额外配置。5. 进阶验证与调试技巧用日志定位事务断点、用 Navicat 观察实时数据流5.1 在关键节点埋点打印 SQL 执行路径与参数绑定光看界面成功不够要确认每一笔钱是否真进了数据库。在DBConfig.java的getConnection()后添加日志public static Connection getConnection() throws SQLException { Connection conn dataSource.getConnection(); System.out.println([DEBUG] 获取连接: conn.hashCode()); return conn; }在QueryRunner执行前打印 SQL// 修改 commons-dbutils 源码或使用代理推荐 // 但更轻量的做法在 DAO 方法开头打印 public User findByUsername(String username) throws SQLException { System.out.printf([SQL] SELECT * FROM user WHERE username %s%n, username); // 仅调试用勿在生产环境 // ... 执行 query }更专业的方案用log4j2替换System.out并在log4j2.xml中配置 JDBC Appender将所有PreparedStatement的toString()输出到文件。关键日志格式应包含时间戳、线程名确认是否 EDT、SQL 语句、参数值?替换后的实际值。5.2 Navicat 实时监控三步确认资金操作原子性开启 Navicat 的“查询分析器”连接atm_db右键数据库 → “查询分析器”执行SELECT * FROM user WHERE username test;记下初始余额。在 Swing 界面发起一笔取款如取 50 元不要关闭 Navicat 窗口。立即切换回 Navicat按 F5 刷新观察user表中该用户balance是否实时变为原值 - 50。若未变说明事务未提交或连接池未释放若只变一次又回滚说明rollback()被触发。提示Navicat 默认开启自动提交因此你的 Swing 应用中conn.setAutoCommit(false)不会影响 Navicat 查看结果——它看到的是最终 commit 后的状态。5.3 用 JUnit 4 模拟用户全流程从注册到交易记录闭环别只靠手动点按钮。写一个ATMIntegrationTest覆盖核心链路public class ATMIntegrationTest { private UserService userService; private AccountService accountService; Before public void setUp() { userService new UserService(); accountService new AccountService(); } Test public void testRegisterLoginWithdrawFlow() throws Exception { // 1. 注册 User user new User(junit_test, 123456, new BigDecimal(1000.00)); userService.register(user); // 2. 登录 User loginResult userService.login(junit_test, 123456); assertNotNull(loginResult); // 3. 取款 accountService.withdraw(loginResult.getId(), new BigDecimal(200.00)); // 4. 验证余额 User updated userService.findById(loginResult.getId()); assertEquals(new BigDecimal(800.00), updated.getBalance()); // 5. 验证交易记录存在 ListTransaction logs new TransactionDao().findByUserId(loginResult.getId()); assertTrue(logs.stream().anyMatch(t - t.getType().equals(WITHDRAW) t.getAmount().equals(new BigDecimal(200.00)))); } }此测试强制你理解UserService和AccountService的依赖关系暴露 DAO 层未 mock 的数据库连接问题。若测试失败错误堆栈会精准定位到withdraw()中哪一行 SQL 执行异常比界面报错快 10 倍。从那以后我每次重构 DAO 方法都强制走一遍这个testRegisterLoginWithdrawFlow()哪怕只改了一个字段名。它不保证业务逻辑完美但能守住数据库层面的数据一致性底线——毕竟用户不会原谅“钱没了但系统说操作成功”。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

微信二维码登录的Spring Session与Redis序列化实践

微信二维码登录的Spring Session与Redis序列化实践

1. 微信二维码登录态管理的核心挑战 在实现微信二维码登录功能时,最关键的环节就是会话状态(Session)的管理。与传统的账号密码登录不同,二维码登录涉及两个独立的通信流程:前端轮询二维码状态和后端验证登录凭证。这种…

📅 2026/10/31 10:28:02
OpenCode开源AI编程助手:安装配置与核心功能解析

OpenCode开源AI编程助手:安装配置与核心功能解析

1. OpenCode:2026年最值得关注的开源AI编程助手 作为一名在开发一线摸爬滚打多年的程序员,我见证了从Copilot到Claude Code再到如今OpenCode的AI编程工具进化历程。OpenCode之所以能在2026年脱颖而出,关键在于它解决了三个核心痛点&#xff1…

📅 2026/10/31 10:28:02
context-mode:从上下文信号到模式切换的工程实践指南

context-mode:从上下文信号到模式切换的工程实践指南

1. 从"context-mode"这个词说起:它到底指什么第一次看到"context-mode"这个标题,很多人会下意识地把它当成某个具体框架的配置项,或者某个库里的一个枚举值。但如果你在工程一线待过几年,就会发现这个词其实是…

📅 2026/10/31 10:28:02
MORE NEWS

更多资讯

📰

PyCharm配置Python环境:解释器、虚拟环境与ARM架构避坑指南

简介:一份面向刚接触 PyCharm 与 Python 开发者的配置文档,聚焦解释器选择、虚拟环境创建、第三方库安装及环境验证等常见问题,也可作为课程实验、项目初始化或团队环境搭建时的操作参考。压缩包内仅含 1 个 docx 文件,约 15KB&am…

📰

纯前端实现PDF、Excel、PPT预览:HTML+JS方案与避坑指南

简介:面向Web前端开发者与需要为网站补充文件预览能力的工程师,这份资源围绕纯HTML与JavaScript在浏览器端实现在线预览展开,覆盖PDF、Excel、PPT、DOC、JPG、PNG等常见格式,适合作为功能原型或教学示例直接参考。压缩包共6个文件…

📰

纯前端文件预览方案:PDF、Excel、PPT 在线预览实战

简介:这份资源面向需要在Web项目中集成文件在线预览能力的开发者,提供一套基于HTML与JavaScript的轻量实现方案,覆盖PDF、Excel、PPT、DOC、JPG、PNG等常见格式,适合前端入门到中级水平、希望快速落地预览功能的读者参考。压缩包共…

📰

Android Studio SDK代理配置与排错指南

简介:面向Android开发者的SDK代理配置参考文档,聚焦官方渠道下载缓慢、组件更新不稳定等常见问题,适合需要为Android Studio配置镜像代理的初中级开发者查阅。压缩包内仅含1个docx文件,约102KB,篇幅精炼,围…

📰

PyCharm配置Python环境全链路:解释器、虚拟环境与依赖管理

简介:这份文档面向刚接触 PyCharm 的 Python 开发者,以及需要为项目搭建独立运行环境的学习者,聚焦解释器选择、虚拟环境创建与第三方库管理这几个最容易卡住的配置环节。内容从安装前的 PATH 检查讲起,涵盖新建项目时指定存储路径…

📰

终端彩色渲染“大神”,一眼,即达——一方小图

终端彩色渲染“大神”,一眼,即达。——一方小图小图: 图中内容是我对sgr体系的近五年“钻研”有幸得道——挣脱传统染缸着色文本的枷锁,走进『终端渲染状态机指令序列构造设定』体系的广袤疆域……样例tree.pypython #!/usr/bin/e…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬