Java Swing银行管理系统实战:JDBC与MySQL事务控制全解析
2026/9/16 3:57:08 网站建设 项目流程

简介:这是一套面向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 ) ENGINE=InnoDB; 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) ) ENGINE=InnoDB; 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 driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/bank_system?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 username=root password=你的数据库密码 initialSize=5 maxActive=20 maxWait=5000 validationQuery=SELECT 1 testWhileIdle=true

使用连接池后,应用启动时创建固定数量的物理连接放在池里,每次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 '1'='1这样的输入直接绕过校验。前者在预编译阶段就把参数当成数据处理,恶意输入只会被当成普通字符串匹配。

验证码逻辑是可选的,加了会更贴近真实系统。常见实现是在登录面板放一个JLabel,点击或刷新时生成四位随机字符绘制到BufferedImage上,存储时同时保留 String 形式的验证码用于比对,登录时先校验验证码再查数据库,这样可以把暴力破解的成本提高一个量级。密码哈希存储使用加盐的策略更稳妥,java 标准库的MessageDigest可以完成 SHA-256 运算,加盐的话则在用户输入密码后拼接一段随机字符串再做哈希。

3.2 主界面布局:CardLayout 切换页面而不重复创建窗体

登录成功后进入主界面,主界面应该是JFrame加上菜单栏或者左侧导航,中间内容区放一个CardLayout容器。左侧按钮对应“余额查询”“存款”“取款”“转账”“交易明细”“开户销户”等模块,每个模块对应一个JPanel子类。切换面板时只需要CardLayout.show(container, cardName),不需要重新创建窗口。

使用 CardLayout 的好处至少有三个方面。一是所有面板在登录后一次性构建,后续切换不再有初始化延迟,也不会因为重复读取数据库产生额外开销;二是主窗体内只有一个WindowListener来统一处理关闭事件,窗口管理简单清晰;三是状态共享方便,当前登录用户的 Account 对象可以放在主窗体的成员变量里,通过构造方法传入各面板,避免使用static全局变量造成的状态污染。

余额显示的数字格式要特别注意。BigDecimaltoString()可能输出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 等 A,MySQL 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-03',6 月 3 日当天所有时间大于00:00:00的数据都会被漏掉。正确写法是把结束日期加一天,用create_time >= ? AND create_time < DATE_ADD(?, INTERVAL 1 DAY)。这类细节在真实生产环境里经常导致数据对不上,修复起来却不难,但写出来的效果能体现对日期边界是否敏感。

4. 并发、密码加密与优雅退出:Java 银行管理系统上线前的关键加固

4.1 存款取款的并发安全:JVM 锁与数据库锁协作

很多人把synchronized直接写在按钮监听器里,认为这样账户就安全了,这其实只锁住了当前窗口。多个 JFrame 实例或多次操作同时执行时,每个按钮持有自己的监听器对象,锁的不是同一个对象,等于没锁。正确思路是让“更新余额”这个数据库操作用悲观锁或乐观锁来保证,界面层只负责防止用户重复点击。

如果选择在 Java 侧做控制,需要按账户维度加锁,可以用ConcurrentHashMap<String, 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 把主类设置为 LoginFrame,Build 之后会在 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 全家桶版本的实现,会发现事务、连接池、分层的核心概念完全一致,差异只在框架帮你掩藏了底层细节。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询