ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Swing+JDBC实现Java银行管理系统:三层架构与事务处理

Swing+JDBC实现Java银行管理系统:三层架构与事务处理

简介:这份Java银行管理系统实验资源面向Java初学者与软件工程课程学习者,覆盖账户管理、存款、取款、转账、利息计算及用户查询等核心业务场景,通过完整项目实践帮助掌握面向对象编程、模块化设计与数据持久化等关键技能。压缩包共7个文件,包含1个java源文件、3个已编译的class文件以及classpath、project、prefs等工程配置文件,整体仅8KB,属于轻量级教学项目,便于快速导入IDE阅读与运行调试。源码中的账户类、存取款及转账方法实现可直观展示余额校验与线程安全等设计要点,编译产物也能帮助对照验证运行结果。整体适合作为课程设计或实验报告的参考范例,已有763人学习下载,对于正在完成Java课程作业或想理解银行系统基础架构的读者颇具参考价值。

1. 项目定位与技术选型:一个能跑通完整柜台业务的Swing+JDBC实现

第一次拆这个项目时我有点意外:一个标注着“Java银行管理系统”的源码包,没有Spring,没有Maven,类加起来不超过二十个,三张数据库表就把开户、存款、取款、转账、流水查询跑通了。它用 Java SE 8 + Swing 做界面,JDBC 直连 MySQL,属于典型的课程设计/毕业设计体量,但业务闭环完整到可以直接拿去做答辩演示。如果你正在找一份能下载下来、导入 IDE 就能跑的 Java 项目,想看懂“界面怎么跟数据库打交道”,或者需要一个课设项目做二次开发底子,这套代码是很合适的参考。它不追求生产级高并发,胜在每一层都写得规矩,新手能顺着调用链看懂,熟手也能快速找到改功能的入口。

2. 系统架构与数据库设计:三层结构下十几个类的职责划分

2.1 分层设计:entity、dao、service、ui 各管一段

项目没有引入任何框架,包结构就是一套经典的三层架构。我在本地打开源码后,第一件事就是把包目录摊开看,结构非常清爽:

src/ ├── ui/ # Swing 界面层:登录窗体、主窗体、开户对话框 ├── service/ # 业务逻辑层:登录验证、存款、取款、转账 ├── dao/ # 数据访问层:基于 JDBC 的增删改查 ├── entity/ # 实体类:Customer、Account、TransactionRecord ├── util/ # 工具类:DBUtil 数据库连接、SessionHolder 会话保持 ├── resources/ │ └── db.properties # 数据库连接配置文件 └── Launcher.java # 程序入口,启动登录窗体

这种分层最直观的好处是:UI 里不会出现任何 SQL 字符串,DAO 里不会出现任何 JOptionPane。比如开户这个动作,用户在窗体上填完姓名、身份证号、初始金额,点击“开户”按钮后,UI 层只做三件事:把界面字段封装成 Customer 对象、调用 CustomerService 的 openAccount 方法、根据返回值弹提示。具体的“密码生成”“卡号生成”“插入两条数据后再回滚”全部由 service 和 dao 两层配合完成。

我通常建议拿到这种项目的开发者先画一条调用链再开始读代码:UI 事件监听 → Service 业务方法 → DAO 数据操作 → 数据库表。顺着这条路走,代码量不大但层次感很强,后续加功能时你很清楚该把新代码放在哪一层。

2.2 数据库设计与三张表的关联关系

以前有人拿过结构混乱的课设代码,界面层直接拼 SQL,改一个字段要全局搜索。这个项目在数据库设计上收敛得很好,一共三张表,关系清楚。建表脚本在项目根目录的bank.sql里,导入之后就是完整的初始数据。

表名主要字段作用
customercustomer_id, name, id_card, phone, create_time客户基本信息
accountaccount_id, account_no, password, balance, customer_id, status, open_date账户凭证,与客户一对一
transaction_recordrecord_id, account_no, trans_type, trans_amount, balance_after, trans_time, remark交易流水账,只追加不修改

表之间的关联用的是逻辑外键:account 表通过 customer_id 关联 customer 表;transaction_record 表通过 account_no 关联 account 表。没有建物理外键约束,这是课设项目里非常常见的设计取舍——删数据、批量导入时不用受约束掣肘,业务层已经保证了引用不会悬空。

其中trans_type字段的值我专门看了一眼,是字符串枚举:deposit(存款)、withdraw(取款)、transfer_out(转出)、transfer_in(转入)。这说明转账操作一次要写两条流水,一条记扣款一条记入账,业务层需要事务保证这个操作的原子性。balance_after字段记录每次交易发生后的余额快照,这样查流水时能直接看到交易后余额,不需要再回算。

3. 核心业务模块实现:登录、取款、转账的完整调用链

3.1 登录链路:从JFrame回显到DAO参数化查询

登录是整个系统的入口,也最能直观体现“界面层 → 业务层 → 数据层”的写法。UI 层代码在ui/LoginFrame.java里,我摘了事件监听的核心片段:

public class LoginFrame extends JFrame { private JTextField accountField; private JPasswordField passwordField; public LoginFrame() { // ... 界面初始化代码省略 ... JButton loginBtn = new JButton("登录"); loginBtn.addActionListener(e -> handleLogin()); } private void handleLogin() { String accountNo = accountField.getText().trim(); String password = new String(passwordField.getPassword()); if (accountNo.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(this, "卡号和密码不能为空"); return; } Account account = new AccountService().login(accountNo, password); if (account == null) { JOptionPane.showMessageDialog(this, "卡号或密码错误"); return; } SessionHolder.setCurrentAccount(account); // 登录成功后保持会话 new MainFrame().setVisible(true); dispose(); // 关闭登录窗体 } }

这段代码的逻辑很直接:把输入框内容取出来做非空校验,然后把验证责任完全交给 service 层。login返回 null 就提示失败,返回 Account 对象就放会话、跳主界面。不把 SQL 写在 UI 层,这就是个干净的习惯。

AccountService.login内部调 DAO,DAO 用 PreparedStatement 做参数化查询:

public Account findByAccountNoAndPassword(String accountNo, String password) { String sql = "SELECT * FROM account WHERE account_no = ? AND password = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, accountNo); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { Account account = new Account(); account.setAccountId(rs.getInt("account_id")); account.setAccountNo(rs.getString("account_no")); account.setBalance(rs.getBigDecimal("balance")); account.setStatus(rs.getString("status")); account.setCustomerId(rs.getInt("customer_id")); return account; } } } catch (SQLException e) { e.printStackTrace(); } return null; }

这里有几个细节值得注意:密码字段在示例项目里直接用明文存储,生产系统显然要加密,但课设里为了演示方便这么做很常见,您拿到源码后如果要加强,可以在 service 层加一层 MD5 或 BCrypt 处理;查询用的是 PreparedStatement 而不是字符串拼接,这是参数化查询,能防 SQL 注入;try-with-resources让 Connection、PreparedStatement、ResultSet 全部自动关闭,不写 finally 也不泄漏连接。

3.2 取款与存款:Service层的数值校验和余额更新

取款接口是理解整个业务校验逻辑的钥匙。真实银行系统的核心规则无非两条:金额必须为正、余额不能为负。我在AccountService.withdraw里看到的实现是这样:

public boolean withdraw(String accountNo, BigDecimal amount) { if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalArgumentException("取款金额必须大于0"); } Account account = accountDao.findByAccountNo(accountNo); if (account == null) { throw new IllegalArgumentException("账户不存在"); } if (account.getStatus() == null || !"normal".equals(account.getStatus())) { throw new IllegalArgumentException("账户已冻结,无法取款"); } if (account.getBalance().compareTo(amount) < 0) { throw new IllegalArgumentException("余额不足,当前余额:" + account.getBalance()); } String sql = "UPDATE account SET balance = balance - ? WHERE account_no = ? AND balance >= ?"; int rows = accountDao.updateBalance(accountNo, amount.negate()); if (rows == 0) { throw new RuntimeException("取款失败,请稍后重试"); } TransactionRecord record = new TransactionRecord(accountNo, "withdraw", amount, account.getBalance().subtract(amount), "柜台取款"); transactionDao.insert(record); return true; }

业务校验全部前置:金额不合法直接抛异常,不让脏数据走到 SQL 层。UPDATE account SET balance = balance - ? WHERE account_no = ? AND balance >= ?这行是关键,它在数据库层面做了一道余额保护——即使并发情况下两个请求同时扣款,也只有一个能通过balance >= ?条件,另一个更新行数为 0,触发异常回滚。这是典型的乐观锁思路,比“先查再改”更可靠。

3.3 转账事务:Connection手动提交保证数据不丢

转账是整个项目里事务最典型的地方。它必须同时完成:扣款账户减余额、收款账户加余额、写入两条流水。任何一个步骤失败,前面所有操作都要撤销。TransferService.transfer的写法很值得背下来:

public boolean transfer(String fromAccountNo, String toAccountNo, BigDecimal amount) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 int rows = accountDao.updateBalance(conn, fromAccountNo, amount.negate()); if (rows == 0) { throw new RuntimeException("转出账户余额不足或账户不存在"); } rows = accountDao.updateBalance(conn, toAccountNo, amount); if (rows == 0) { throw new RuntimeException("转入账户不存在"); } transactionDao.insert(conn, new TransactionRecord(fromAccountNo, "transfer_out", amount, balanceAfterQuery(conn, fromAccountNo), "转账支出")); transactionDao.insert(conn, new TransactionRecord(toAccountNo, "transfer_in", amount, balanceAfterQuery(conn, toAccountNo), "转账收入")); conn.commit(); // 全部成功才提交 return true; } catch (Exception e) { if (conn != null) { try { conn.rollback(); // 任何一个环节失败,整体回滚 } catch (SQLException ex) { ex.printStackTrace(); } } throw new RuntimeException("转账失败:" + e.getMessage(), e); } finally { DBUtil.close(conn); // 连接只在这里关闭,不在业务方法里到处关 } }

这个实现里最容易忽略的细节是“连接从外面传入而不是内部新建”。accountDao.updateBalance和transactionDao.insert都接收 Connection 参数,这保证它们用的是同一个数据库连接、处于同一个事务边界内;如果每个 DAO 方法内部都自己 getConnection,那事务就完全失效了——各自独立提交,前半成功后半失败时无法回滚。这也是我在审阅这类项目时重点看的地方。

整体看下来,登录、存取款、转账这三块代码把一条完整的调用链串起来了:UI 只收集参数和展示结果,Service 管规则校验和事务边界,DAO 只执行 SQL 并返回结果。

4. 本地运行与常见问题排查:从导入到跑通的避坑记录

4.1 运行环境与启动步骤

这个项目的运行环境要求并不高。我本地用的是 JDK 8 + MySQL 5.7 + IDEA,MySQL 8.0 也兼容,但要做两处配置调整,具体在下面的排查部分会说到。您需要准备的东西:JDK 8 或 11,MySQL 5.7 以上,一个能管理 jar 依赖的 IDE(IDEA 或 Eclipse 都行),以及项目里自带的 lib 目录下那个mysql-connector-java驱动包。

跑通流程按照下面这几步做:

  1. 打开 MySQL 命令行或 Navicat,执行源码包里的bank.sql,会自动创建bank数据库和三张核心表。
  2. 用 IDEA 打开项目(选择“Open”指向源码根目录即可,无需 Maven 导入)。
  3. 编译项目,把lib/mysql-connector-java-xxx.jar添加到项目结构里的 Libraries,这个不添加的话启动时直接报 ClassNotFoundException。
  4. 修改src/resources/db.properties里的数据库连接地址和账号密码,默认配置是jdbc:mysql://localhost:3306/bank,账号root,密码为空。
  5. 运行Launcher.java,弹出登录窗体后用初始卡号登录。初始数据在bank.sql里有两三条账户记录,密码是123456。

4.2 高频报错排查:时区、驱动、乱码与事务失效

我当年第一次跑这类项目的时候,卡在启动步骤之外的各种杂病上,下面这四条是我在这个项目里真正遇到过、也最常被后来人问的。

现象一:启动后报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized。原因是 MySQL 8.0 驱动的时区校验比 5.x 严格,数据库所在系统时区被驱动识别不了。解决方法是打开db.properties,在连接 URL 后面追加serverTimezone=Asia/Shanghai,或者改连接串为jdbc:mysql://localhost:3306/bank?serverTimezone=Asia/Shanghai。

现象二:运行时提示ClassNotFoundException: com.mysql.jdbc.Driver。原因是驱动 jar 没有真正进入 IDE 的构建路径,只是放在了文件夹里。解决方法是手动在 IDEA 的 Project Structure → Modules → Dependencies 里点加号,把lib下的 jar 添加为 Library。每换一台电脑、重新导入项目都要检查这一步,它最容易漏。

现象三:控制台输出中文乱码,界面按钮标题乱成一团。原因是 IDE 默认文件编码不是 UTF-8,导致 Java 文件里的中文被按 GBK 读取。解决方法是把 IDEA 的 Global Encoding、Project Encoding、Properties Files 编码全部切到 UTF-8,然后重新打开项目。你还可以顺手检查db.properties里连接 URL 是否包含characterEncoding=utf-8,保证数据库读出中文不乱。

现象四:转账操作不报错,但余额也没变。这个最阴间,因为代码逻辑看着是对的。原因通常是修改某个 DAO 方法时把 Connection 换成了自己内部获取的连接,导致 Service 层setAutoCommit(false)管不住这个新连接,操作直接自动提交了。解决方法是检查updateBalance和insert的签名,必须接收外部传入的 Connection,不要让它们自己getConnection()。如果你看源码发现这个方法签名的确要求传 Connection 而你有这段代码,那八成是你在重构时改漏了。

5. 二次开发改造:给系统增加“修改密码”的完整落地过程

5.1 改造路径:UI事件、Service方法、DAO更新

拿到这个项目后,最值得动手做的第一个改造是加“修改密码”。原因很朴素:现有登录逻辑直接比对明文密码,用户自己改密码这个动作没实现,面试官或导师大概率会问“如果用户想改密码怎么办”。改造路径完全顺着三层结构走,不需要动表结构,只加一个字段验证步骤。

先加 DAO 方法,核心就是一个 UPDATE:

public int updatePassword(Connection conn, String accountNo, String newPassword) { String sql = "UPDATE account SET password = ? WHERE account_no = ?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, newPassword); ps.setString(2, accountNo); return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("修改密码失败", e); } }

注意这里依然选择接收 Connection 参数。如果未来要把“改密码”和“写一条流水”合并成一个事务,这个方法不需要再改动签名,Service 层直接复用现有事务骨架即可。之后再在AccountService里加一个changePassword方法,内部校验新密码长度、确认两次输入一致,然后调用上面的 DAO 方法。

UI 层则是做一个PasswordDialog,靠三个字段收集数据:“旧密码”“新密码”“确认新密码”。点击确认后调 Service,成功后顺手把SessionHolder里的账户对象密码也更新掉,并提示用户重新登录。这个过程大概 30 分钟能做完,改完你就完整走了一遍“界面→业务→数据”三个层次的增改路径。

5.2 验证方式与常见误用

功能加完后不要直接点一下“能弹窗”就算过了。我的验证清单是这样的:先用旧密码登录成功,再改密码,然后退出登录,用新密码登录成功,流程通过;接着故意在新旧密码输入框中制造不一致,确认系统拦截并给出提示;再故意输入错误的旧密码,确认改密失败而且数据库里的密码没有被改动。最后一个用例才是最容易翻车的地方——如果你在 UI 层先把旧密码校验过了,再在 Service 层漏掉校验,就会出现数据库密码被静默修改的情况。

我还想提醒一个容易踩的坑:页面里用getPassword()拿到的是char[]而不是直接转 String,这个项目本来也是这种写法;但有人在写改密功能时图省事,改成new String(passwordField.getPassword()),这在大作业里没人管,但如果以后要往生产方向走,记得用完就置空 chars 数组。这是第一次做完之后复盘时发现的,从那以后我每次接这类课设项目都会强制走一遍“功能跑通 → 异常用例验证 → 检查资源释放和密码生命周期”这三步,脏代码改起来很痛苦,但提前验证能省下大量事后补洞的时间。希望帮你顺利把项目跑起来,也少踩几个我当年踩过的坑。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进