ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java Swing银行管理系统实战:JDBC与MySQL事务控制全解析

Java Swing银行管理系统实战:JDBC与MySQL事务控制全解析 简介这是一套面向Java学习者的银行管理系统教学项目采用IDEA开发以Swing构建图形用户界面并用MySQL完成数据持久化。系统分为管理员端和顾客端管理员可登录、添加或删除顾客、统计存储金额顾客可登录、存款、取款、转账、修改密码、查看交易记录并安全退出。资源包为rar格式总共28个文件大小40KB除核心Java源码外还包含编译后的class文件、Maven工程配置、数据库配置、HTML说明页面及帮助文档便于导入开发环境直接运行。目前已有424人学习下载。通过完整阅读该项目可加深对面向对象设计、数据库连接操作及银行核心业务流程的理解借助内置帮助说明与工程配置文件能快速搭建运行环境、梳理代码脉络便于二次扩展适合课程设计、毕业设计或入门金融系统开发的实战参考。1. 把 Swing 银行系统做成能答辩、能上线的最小闭环很多人第一次接触 Java 银行管理系统时容易把它当成 Web 项目来做其实这个题目真正的技术内核是一次典型的 Java SE JDBC 关系型数据库的完整落地。Swing 承担用户界面MySQL 存账户流水两者之间靠 JDBC 完成数据交换。整个过程不涉及 Tomcat、不涉及 HTTP 协议却能把 Java 语言的面向对象特性、集合框架、异常体系、多线程和事务控制串成一条线。这套系统在毕业设计和初级 Java 岗位面试里出现频率极高但大多数实现只做到“能跑”的程度SQL 拼接、事务不完整、主键用自增、窗口之间靠 new 一个 JFrame 跳转。能把这些细节做对的版本反而更容易在同题竞争里拉开差距。本文按照我会实际做这套系统的顺序来写从 IDEA 工程骨架开始到 MySQL 表设计再到 Swing 界面与 JDBC 的边界划分最后收在几个最重要的进阶处理上。2. 技术选型与工程骨架IDEA 里搭 JavaSwing MySQL 的可靠起点2.1 为什么这套选型是合理的以及它和 Web 版的本质区别Java 银行管理系统最常见的两种形态是纯 Swing 客户端和 Spring Boot Thymeleaf 的 Web 版。既然标题锁定了 JavaSwing就要按传统 CS 架构来设计。Swing 是 JFC 的一部分基于 AWT 的事件分发线程模型自带一套轻量级组件不需要额外引入 UI 框架。MySQL 负责持久化通过 JDBC 驱动交互。这套方案相对 Web 版有几个实际好处。第一部署形态简单双击 jar 就能运行不需要装 Web 容器第二Swing 的组件模型对 Java 基础概念的呈现更直观每个按钮的点击监听都是一个匿名内部类或 Lambda 表达式每个表格都是一个 JTable TableModel这些恰恰是 Java 面试八股文里常考的东西第三事务控制可以写得很直白因为所有数据库操作都在同一个 JVM 进程里发起比起 Web 版更容易让初学者顺着代码路径理解 connection、statement 和结果集的生命周期。要注意的边界是Swing 不是为高并发设计的。银行的核心账务系统在真实生产环境里根本不会用 Swing 客户端直连数据库这只是一个教学场景。但正因如此它反而适合把数据库连接池、事务隔离级别、SQL 防注入这些偏工程的问题放在小体量代码里讲透。2.2 IDEA 工程结构按包分层而不是按类堆文件打开 IntelliJ IDEA新建一个普通 Java 项目不需要 Maven 骨架直接 Empty Project 然后加 Module 即可。社区版足够完成这套系统不需要激活码也不需要破解版安装教程里的那些额外步骤。项目结构我一般会分成以下五个包com.bank ├── ui # 所有 Swing 界面类如 LoginFrame、MainFrame、AccountPanel ├── dao # 数据访问层只负责 SQL 和 JDBC不写业务判断 ├── service # 业务逻辑层比如转账、开户、销户 ├── entity # 实体类对应数据库表结构 └── util # 工具类如 DBUtil 连接管理、数据校验、导出工具在 ui 包里再按面板划分而不是按窗体划分这样才能解决后续窗口复用的问题。很多入门实现把“存款窗口”“取款窗口”都写成独立 JFrame结果系统一开十几个窗口。更可靠的做法是主窗体只有一个内部用 JPanel 配合 CardLayout 切换页面类似单页应用的感觉。这个决策会在后面减少大量状态同步问题。2.3 MySQL 表结构银行系统如何设计表以及为什么不用自增主键数据库侧不要急着写代码先定表和连接配置。银行系统的核心是“账户 流水”双表结构账户表保存当前余额流水表记录每一笔变更取款、存款、转账都通过流水驱动余额更新。CREATE DATABASE bank_system CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE bank_system; CREATE TABLE account ( account_id VARCHAR(32) PRIMARY KEY COMMENT 账号业务自定义不用自增, customer_name VARCHAR(64) NOT NULL, password_hash VARCHAR(255) NOT NULL COMMENT 使用SHA-256加盐后存储, balance DECIMAL(14,2) NOT NULL DEFAULT 0.00, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-正常 0-冻结 2-销户, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE account_flow ( flow_id BIGINT AUTO_INCREMENT PRIMARY KEY, account_id VARCHAR(32) NOT NULL, flow_type TINYINT NOT NULL COMMENT 1-存款 2-取款 3-转入 4-转出, amount DECIMAL(14,2) NOT NULL, balance_after DECIMAL(14,2) NOT NULL, remark VARCHAR(255) DEFAULT , create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_account_time (account_id, create_time) ) ENGINEInnoDB; INSERT INTO account (account_id, customer_name, password_hash, balance) VALUES (10001, 测试用户, 8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918, 5000.00);这里故意不用自增做主键而是用业务账号因为银行场景里账号本身有业务含义比如开户行号加客户序号。生产环境会用专门的发号器来避免重复教学环境用一个时间戳加随机数就能解决。流水表用自增主键是合理的因为它只做追加update 和 delete 基本不发生。password_hash字段不能存明文原因是银行系统涉及资金安全哪怕只是教学项目也要养成不落明文密码的习惯。上面插入语句中写的是admin123经过 SHA-256 后的哈希值这里只是预置测试数据真实开户需要通过代码写入哈希结果。2.4 连接池配置Druid 在普通 Java 项目里的最小接入方式网上大多数教程在 JDBC 里直接DriverManager.getConnection()这在单机测试里没问题但一旦界面操作频繁每次新建连接的开销会拖慢响应而且连接数不可控。常见做法是引入阿里巴巴的 Druid 连接池它是纯粹的 Java 库在 IDEA 里通过 Project Structure 添加 jar 或者在 Maven 项目里引入依赖都可以。# src/druid.properties driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/bank_system?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 usernameroot password你的数据库密码 initialSize5 maxActive20 maxWait5000 validationQuerySELECT 1 testWhileIdletrue使用连接池后应用启动时创建固定数量的物理连接放在池里每次getConnection()从池中取一个空闲连接用完调用close()其实是归还给池而不是真正断开。这个机制让数据库连接的创建成本被摊薄同时通过maxWait控制获取连接的最大等待时间避免界面线程无限期阻塞。 写一个DBUtil类来封装数据源界面层和 DAO 层只依赖这个工具类拿连接不自己加载驱动。整个系统只有一个静态数据源实例所有线程共享避免了频繁创建连接导致的 MySQLToo many connections报错。3. 从空窗体到一个能转账的银行系统Swing 界面、JDBC 编码与事务边界3.1 登录窗体的正确写法验证码、回车键与用户反馈登录是进入系统的第一道门也是展示代码质量的小窗口。一个合格的登录窗体不应该只是“账号密码按钮”三个组件的堆砌至少要包含密码框的回车提交、错误提示的友好展示以及防止重复提交的按钮禁用逻辑。用 Swing 的JPasswordField而不是JTextField来接收密码因为前者自带字符掩盖效果。在这一步DAO 层查询用户时使用PreparedStatement这是防御 SQL 注入的基本要求。public Account login(String accountId, String password) throws Exception { String sql SELECT account_id, customer_name, password_hash, balance, status FROM account WHERE account_id ? AND status 1; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, accountId); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { String hash SHA256Util.encode(password); if (hash.equals(rs.getString(password_hash))) { Account acc new Account(); acc.setAccountId(rs.getString(account_id)); acc.setCustomerName(rs.getString(customer_name)); acc.setBalance(rs.getBigDecimal(balance)); return acc; } } return null; } } }这段代码里最核心的地方在于 SQL 中使用了?占位符由PreparedStatement负责参数转义而不是用字符串拼接 WHERE account_id accountId 。后者在经典八股文里是必考题攻击者可以构造 OR 11这样的输入直接绕过校验。前者在预编译阶段就把参数当成数据处理恶意输入只会被当成普通字符串匹配。验证码逻辑是可选的加了会更贴近真实系统。常见实现是在登录面板放一个JLabel点击或刷新时生成四位随机字符绘制到BufferedImage上存储时同时保留 String 形式的验证码用于比对登录时先校验验证码再查数据库这样可以把暴力破解的成本提高一个量级。密码哈希存储使用加盐的策略更稳妥java 标准库的MessageDigest可以完成 SHA-256 运算加盐的话则在用户输入密码后拼接一段随机字符串再做哈希。3.2 主界面布局CardLayout 切换页面而不重复创建窗体登录成功后进入主界面主界面应该是JFrame加上菜单栏或者左侧导航中间内容区放一个CardLayout容器。左侧按钮对应“余额查询”“存款”“取款”“转账”“交易明细”“开户销户”等模块每个模块对应一个JPanel子类。切换面板时只需要CardLayout.show(container, cardName)不需要重新创建窗口。使用 CardLayout 的好处至少有三个方面。一是所有面板在登录后一次性构建后续切换不再有初始化延迟也不会因为重复读取数据库产生额外开销二是主窗体内只有一个WindowListener来统一处理关闭事件窗口管理简单清晰三是状态共享方便当前登录用户的 Account 对象可以放在主窗体的成员变量里通过构造方法传入各面板避免使用static全局变量造成的状态污染。余额显示的数字格式要特别注意。BigDecimal的toString()可能输出5000.00或科学计数法界面显示时应该统一用DecimalFormat(#,##0.00)格式化否则用户看到1.23456789E7会直接懵掉。金额计算也一律使用BigDecimal禁止用double因为二进制浮点数无法精确保有十进制小数在做 0.1 0.2 这类运算时会产生误差银行系统对金额误差是零容忍的。3.3 转账功能把事务边界画在 Service 层转账是银行系统里最具含金量的功能也是面试里最喜欢追问的模块。单纯在 DAO 层写两条 update 语句只完成了一半原子性才是转账的灵魂。一次转账涉及转出方扣款和转入方入账两个操作任何一个失败都必须回滚否则系统会出现账实不符。用 Spring 的话一个Transactional注解就结束了但 JavaSwing 项目里没有 Spring事务控制要靠手动管理。public boolean transfer(String fromAccount, String toAccount, BigDecimal amount, String remark) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); AccountDao dao new AccountDao(); Account from dao.findForUpdate(conn, fromAccount); Account to dao.findForUpdate(conn, toAccount); if (from null || to null) { throw new RuntimeException(转账账户不存在); } if (from.getStatus() ! 1 || to.getStatus() ! 1) { throw new RuntimeException(账户状态异常); } if (from.getBalance().compareTo(amount) 0) { throw new RuntimeException(余额不足当前余额 from.getBalance()); } dao.updateBalance(conn, fromAccount, from.getBalance().subtract(amount)); dao.updateBalance(conn, toAccount, to.getBalance().add(amount)); dao.insertFlow(conn, fromAccount, 4, amount, from.getBalance().subtract(amount), remark); dao.insertFlow(conn, toAccount, 3, amount, to.getBalance().add(amount), 来自 fromAccount); conn.commit(); return true; } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (Exception ex) { ex.printStackTrace(); } } throw new RuntimeException(转账失败 e.getMessage(), e); } finally { if (conn ! null) { try { conn.setAutoCommit(true); } catch (Exception e) { } try { conn.close(); } catch (Exception e) { } } } }这段代码需要解释的细节比较多。conn.setAutoCommit(false)是事务起点告诉 JDBC 不要每条语句执行完就自动提交而是等待手动 commit。findForUpdate内部执行的是SELECT ... FOR UPDATE通过给账户行加锁防止两个线程同时读到同一个余额如果不加锁并发转账时可能发生丢失更新这是数据库隔离级别和锁机制的实际应用。事务有两种回滚路径SQL 执行抛异常时走catch块里的rollback()业务校验不通过时同样抛出运行时异常触发回滚。finally块里把连接归还之前要恢复setAutoCommit(true)否则连接下次从池里取出来时还带着关闭事务的状态会导致后续查询无法正常工作。这里还有一个细节值得注意转账时要先锁两个账户顺序上要避免死锁。如果转出和转入同时反向操作一个锁 A 等 B另一个锁 B 等 AMySQL InnoDB 会检测到死锁并让其中一个事务回滚。常见缓解策略是先把两个 account_id 排序保证加锁顺序一致或者在重试一次。教学项目里可以提一下这个思路不一定要完整实现。3.4 交易明细查询JTable 渲染与时间区间过滤交易明细是典型的查询场景用户选择起止日期点击查询按钮结果展示在JTable里。Swing 里 JTable 不直接接收ResultSet需要把数据转成Vector或使用DefaultTableModel。推荐后者因为它的setValueAt方法支持单元格更新后续做分页刷新会比较方便。时间字段的处理有个高频坑。MySQL 的DATETIME类型通过 JDBC 读取时默认得到java.sql.Timestamp直接toString()得到的是2025-06-01 14:23:11.0末尾多了小数秒。界面展示之前要么在 SQL 里用DATE_FORMAT(create_time, %Y-%m-%d %H:%i:%s)格式化要么在 Java 侧用DateTimeFormatter转换。比较稳妥的方式是 DAO 层查出原始 Timestamp视图层负责格式化SQL 保持数据职责纯净。 区间查询的时间边界也容易踩坑。用户选了“2025-06-01 到 2025-06-03”如果直接create_time BETWEEN 2025-06-01 AND 2025-06-036 月 3 日当天所有时间大于00:00:00的数据都会被漏掉。正确写法是把结束日期加一天用create_time ? AND create_time DATE_ADD(?, INTERVAL 1 DAY)。这类细节在真实生产环境里经常导致数据对不上修复起来却不难但写出来的效果能体现对日期边界是否敏感。4. 并发、密码加密与优雅退出Java 银行管理系统上线前的关键加固4.1 存款取款的并发安全JVM 锁与数据库锁协作很多人把synchronized直接写在按钮监听器里认为这样账户就安全了这其实只锁住了当前窗口。多个 JFrame 实例或多次操作同时执行时每个按钮持有自己的监听器对象锁的不是同一个对象等于没锁。正确思路是让“更新余额”这个数据库操作用悲观锁或乐观锁来保证界面层只负责防止用户重复点击。如果选择在 Java 侧做控制需要按账户维度加锁可以用ConcurrentHashMapString, ReentrantLock维护每个账号的锁对象。转账发生时先获取转出账户的锁再执行上面的 DAO 事务流程。这个方案在单机范围内是有效的但它并不能替代数据库层面的行锁——最可靠的方式还是SELECT ... FOR UPDATE。这个知识点可以在面试里展开先说界面层防重再说 JVM 内锁最后落到数据库行锁层层递进才能体现对整个调用链路的完整理解。4.2 密码存储的工程级做法加盐哈希而不只是 SHA-256上文的建表 SQL 里写了 sha-256 哈希但严格来说直接对密码做哈希仍然存在彩虹表攻击的风险。工程上没有几十行代码成本的做法是加盐注册时生成一段 16 位随机字符串作为盐密码哈希值等于SHA-256(盐 密码)最终在数据库里同时存密码哈希和盐登录时用传入的密码拼接库里的盐重新计算比对。使用加盐方案时数据库需要增加一列salt VARCHAR(32)代码侧的逻辑也会相应调整。如果时间允许也可以直接引入 PBKDF2 这种相对标准的算法javax.crypto.SecretKeyFactory原生支持不依赖第三方库只是参数多一些。重要的是养成“不落明文”和“不直接用裸哈希”两个习惯。4.3 在 IDEA 中直接打包可运行的 jar解决双击无法启动的问题Swing 程序做完了最后的收尾是打包。在 IDEA 里通过 File → Project Structure → Artifacts → JAR → From modules with dependencies 把主类设置为 LoginFrameBuild 之后会在 out/artifacts 下生成 jar 包。真正双击运行时常见的问题是缺 MySQL 驱动。IDEA 编译时引用的外部 jar 不会自动打进产物需要在 Artifacts 的 Output Layout 里把 mysql-connector-j 的 jar 文件拖进输出目录。另一个容易遇到的问题是双击 jar 没反应。可以先用命令行跑一遍能看到真正的报错信息而不是被系统吞掉java -jar bank-system.jar看到控制台输出再排查。MySQL 连接不上时先确认 MySQL 服务是否启动再确认 Druid 配置的密码是否和本地一致最后看防火墙有没有拦截 3306 端口。按照这个顺序排查能解决绝大多数启动失败问题。4.4 快速自检清单一个可用于验收的功能完整度检查功能点验收标准常见失分点登录校验数据库校验、错误提示、查询条件使用占位符写死账号或不查库直接通过开户新账号写入库并完成密码哈希幂等性缺失重复点击导致重复开户转账原子性转出失败时转入不生效余额回滚两条 update 之间抛异常导致半完成交易明细可查指定时间段时间边界正确结束日期当天数据为空数据库连接连接池统一管理用完归还直接使用DriverManager且不关连接界面层级单一主窗体 CardLayout每次操作新建 JFrame 累积这套系统做到以上程度在毕业设计答辩和简历项目里都可以站得住。真正的银行系统远比这复杂但是理解这个最小闭环之后再去看 Spring 全家桶版本的实现会发现事务、连接池、分层的核心概念完全一致差异只在框架帮你掩藏了底层细节。本文还有配套的精品资源点击获取
返回列表