简介:这份Java银行管理系统是一套面向Java初学者与课程实验学习者的教学型项目源码,围绕账户管理、存取款、转账、利息计算与账户查询等核心业务展开,帮助读者在真实场景中理解面向对象编程的封装、抽象与模块化设计。压缩包共7个文件,以class编译文件、java源码、classpath与project工程配置、prefs偏好设置等为主,整体约8KB,属于轻量级Eclipse工程,导入后即可查看类结构与业务方法实现。目前已有763人学习下载,适合作为Java实验课作业参考或课程设计起步模板。读者可从中借鉴Account类的属性与方法设计、transferTo转账中的并发控制思路、基于synchronized的线程安全处理,以及余额查询与交易记录的数据组织方式,并进一步扩展贷款、信用卡、客户信息等子系统,是巩固Java基础与软件工程实践的实用素材。
1. 从一套 Java 银行管理系统源码说起:它到底能跑通哪些真实业务
如果你正在搜「Java 银行管理系统」或者「Java 银行管理」相关的课程设计、毕业设计参考,大概率你手里已经有一堆压缩包了,但真正能跑起来、代码结构能看、业务逻辑不糊弄的没几个。我这次拆的这套 Java 银行管理系统源码,核心是用 Java SE 加 Swing 做界面、JDBC 直连数据库做持久化,覆盖了开户、存款、取款、转账、查询流水、修改密码这几条最常被要求的主线业务。它不是那种只打印几行控制台输出的玩具 Demo,而是带完整表结构、带事务控制、带异常提示的桌面端应用,适合两类人:一类是刚学完 JDBC 和 Swing、需要拿一个完整项目练手的在校生;另一类是带课程设计、想快速看清「银行类业务系统到底怎么组织代码」的开发者。下面我按「先跑起来、再看结构、再改参数、最后避坑」的顺序,把这份资源拆开讲透。
2. 环境搭建与数据库初始化:把空库跑成能登录的系统
2.1 先确认 JDK 和数据库版本,别急着双击运行
这套系统对环境的依赖不算重,但版本对不上会直接卡在第一步。我一般会先确认三件事:JDK 是不是 8 或 11(源码里用了LocalDate和try-with-resources,JDK 7 以下直接编译不过),MySQL 是不是 5.7 或 8.0(驱动包mysql-connector-java的版本要和数据库大版本匹配),以及 IDE 用的是 IntelliJ IDEA 还是 Eclipse(导入方式不同,但源码本身不依赖特定 IDE)。
常见做法是新建一个 Java 项目,把src目录整个拷进去,然后在项目结构里把lib下的mysql-connector-java-8.0.xx.jar加到模块依赖。如果你用的是 Maven,也可以直接换成pom.xml依赖,坐标是mysql:mysql-connector-java:8.0.33。这一步不做,后面所有ClassNotFoundException: com.mysql.cj.jdbc.Driver的报错都是白折腾。
提示:驱动类名在 MySQL 8 里是
com.mysql.cj.jdbc.Driver,5.x 是com.mysql.jdbc.Driver,源码里如果写死了一个,另一个版本就会连不上,这是最高频的翻车点。
2.2 建库建表:三张核心表撑起全部业务
这套系统的数据库设计不复杂,但字段类型和约束值得看一眼。核心表就三张:account(账户主表)、transaction_record(交易流水表)、user(登录用户表)。我一般会先执行建库语句,再逐条执行建表语句,避免外键依赖顺序出错。
-- 创建数据库,字符集用 utf8mb4,防止中文姓名乱码 CREATE DATABASE bank_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE bank_system; -- 账户表:卡号做主键,余额用 DECIMAL 保证精度 CREATE TABLE account ( card_no VARCHAR(20) PRIMARY KEY, user_name VARCHAR(50) NOT NULL, id_card VARCHAR(18) NOT NULL UNIQUE, balance DECIMAL(15,2) NOT NULL DEFAULT 0.00, password VARCHAR(64) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 交易流水表:记录每一笔资金变动,type 区分业务类型 CREATE TABLE transaction_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, card_no VARCHAR(20) NOT NULL, type VARCHAR(20) NOT NULL, amount DECIMAL(15,2) NOT NULL, balance_after DECIMAL(15,2) NOT NULL, trade_time DATETIME DEFAULT CURRENT_TIMESTAMP, remark VARCHAR(200), INDEX idx_card_no (card_no) ); -- 登录用户表:和账户表通过 card_no 关联 CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY, card_no VARCHAR(20) NOT NULL UNIQUE, login_pwd VARCHAR(64) NOT NULL, last_login DATETIME );这段 SQL 里有三个参数值得注意。DECIMAL(15,2)是金额字段的标准写法,用FLOAT或DOUBLE会在多次存取后出现0.30000000000000004这种精度问题,对账时就是灾难。utf8mb4而不是utf8,是因为utf8在 MySQL 里只支持三字节,存不下某些生僻字和 emoji,虽然银行系统用不到 emoji,但姓名里的生僻字会直接报错。transaction_record上的idx_card_no索引,是为了后面查流水时不用全表扫描,数据量上万条以后差别很明显。
2.3 改配置文件:数据库连接串是第一个要动的地方
源码里通常有一个db.properties或者直接在DBUtil.java里写死了连接信息。我一般会把连接串抽到配置文件,方便换环境。关键参数就四个:URL、用户名、密码、驱动类。
# db.properties jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/bank_system?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4 jdbc.username=root jdbc.password=你的数据库密码serverTimezone=Asia/Shanghai这个参数在 MySQL 8 里必须加,不加会报The server time zone value '...' is unrecognized,因为 MySQL 8 默认时区是 UTC,而 Java 的LocalDateTime按本地时区解析,两边对不上。useSSL=false是关掉 SSL 警告,本地开发无所谓,生产环境要另说。characterEncoding=utf8mb4保证 Java 端和数据库端编码一致,中文姓名和备注才不会变成问号。
改完配置后,先跑一个main方法测试连接,能打印出Connection success再往下走。这一步过了,后面 80% 的环境问题就排除了。
3. 核心业务代码拆解:开户、存取款、转账是怎么串起来的
3.1 开户逻辑:一次插入两张表,事务不能省
开户这个功能看起来简单,实际上要同时往account和user两张表插数据。如果account插成功、user插失败,就会出现「有账户但登不上」的脏数据。所以开户方法里必须手动控制事务,把autoCommit关掉,两条INSERT都成功再提交。
public boolean openAccount(Account acc, String loginPwd) { Connection conn = null; PreparedStatement ps1 = null, ps2 = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 // 第一条:插入账户主表 String sql1 = "INSERT INTO account(card_no,user_name,id_card,balance,password) VALUES(?,?,?,?,?)"; ps1 = conn.prepareStatement(sql1); ps1.setString(1, acc.getCardNo()); ps1.setString(2, acc.getUserName()); ps1.setString(3, acc.getIdCard()); ps1.setBigDecimal(4, acc.getBalance()); ps1.setString(5, acc.getPassword()); ps1.executeUpdate(); // 第二条:插入登录用户表 String sql2 = "INSERT INTO user(card_no,login_pwd) VALUES(?,?)"; ps2 = conn.prepareStatement(sql2); ps2.setString(1, acc.getCardNo()); ps2.setString(2, loginPwd); ps2.executeUpdate(); conn.commit(); // 两条都成功才提交 return true; } catch (SQLException e) { try { if (conn != null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { DBUtil.close(conn, ps1, ps2); } }这里的关键参数是conn.setAutoCommit(false),它把 JDBC 默认的「每条 SQL 自动提交」改成手动模式。commit()之前任何一条失败,rollback()都会把两条插入一起撤销。我见过不少课程设计版本为了省事,两条 SQL 分开执行、不关自动提交,结果测试时偶尔出现「账户有了但登录不了」,查半天查不出来,这就是事务没上的血泪教训。
3.2 存款与取款:余额更新必须带条件,防止并发覆盖
存款和取款本质上都是「先查余额、再算新余额、再更新」。但直接UPDATE account SET balance = ? WHERE card_no = ?在单机演示时没问题,一旦有多线程或者多个客户端同时操作,就会出现「A 取 100,B 也取 100,两人都看到余额 500,最后余额变成 400 而不是 300」的覆盖问题。
常见做法是在UPDATE的WHERE里带上原余额条件,用受影响行数判断是否更新成功:
public boolean withdraw(String cardNo, BigDecimal amount) { String sql = "UPDATE account SET balance = balance - ? WHERE card_no = ? AND balance >= ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setBigDecimal(1, amount); ps.setString(2, cardNo); ps.setBigDecimal(3, amount); // 余额必须大于等于取款额 int rows = ps.executeUpdate(); if (rows == 0) { System.out.println("余额不足或卡号不存在"); return false; } // 更新成功后写流水 recordTransaction(cardNo, "WITHDRAW", amount); return true; } catch (SQLException e) { e.printStackTrace(); return false; } }balance = balance - ?这种写法让数据库自己完成减法,避免 Java 端「读出来再算再写回去」的时间窗口。AND balance >= ?保证余额不足时executeUpdate返回 0,程序就知道这笔取款没成功。rows == 0这个判断是很多新手会漏的,漏了就会出现「余额不足但流水已经记了一笔」的玄学现象。
3.3 转账:两个账户一扣一加,事务边界要包住流水
转账是这套系统里最考验事务的功能。A 卡扣钱、B 卡加钱、两条流水记录,四步操作必须在一个事务里。任何一步失败,整个转账回滚。
public boolean transfer(String fromCard, String toCard, BigDecimal amount) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 扣款方:余额充足才扣 String deductSql = "UPDATE account SET balance = balance - ? WHERE card_no = ? AND balance >= ?"; try (PreparedStatement ps = conn.prepareStatement(deductSql)) { ps.setBigDecimal(1, amount); ps.setString(2, fromCard); ps.setBigDecimal(3, amount); if (ps.executeUpdate() == 0) { conn.rollback(); return false; } } // 2. 入账方:直接加 String addSql = "UPDATE account SET balance = balance + ? WHERE card_no = ?"; try (PreparedStatement ps = conn.prepareStatement(addSql)) { ps.setBigDecimal(1, amount); ps.setString(2, toCard); if (ps.executeUpdate() == 0) { conn.rollback(); return false; } } // 3. 两条流水 recordTransaction(conn, fromCard, "TRANSFER_OUT", amount); recordTransaction(conn, toCard, "TRANSFER_IN", amount); conn.commit(); return true; } catch (SQLException e) { try { if (conn != null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { DBUtil.close(conn, null, null); } }注意recordTransaction这里传了conn进去,而不是自己再开一个连接。如果流水方法内部自己DBUtil.getConnection(),那它就不在转账事务里了,扣款回滚了流水却留下了,对账时就是黑匣子。这个细节是区分「能跑」和「能信」的分水岭。
4. 避坑与排查:这套源码最容易翻车的五个地方
4.1 中文乱码:从数据库到 Swing 界面全链路排查
现象:开户时输入中文姓名,存进数据库变成???,或者 Swing 表格里显示成方块。
原因:编码链路有三段——数据库字符集、JDBC 连接串、Java 源文件编码。任何一段不是 UTF-8 都会断。
解决:数据库建库时用utf8mb4;连接串加characterEncoding=utf8mb4;IDE 里把 File Encoding 全部设成 UTF-8;Swing 组件如果用了默认字体,换成new Font("微软雅黑", Font.PLAIN, 14)支持中文显示。四步都做,基本不会再乱。
4.2 驱动加载失败:ClassNotFoundException 的三种可能
现象:启动时报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。
原因:一是 jar 包没加到项目依赖;二是 jar 包版本和代码里的驱动类名不匹配(5.x 和 8.x 类名不同);三是 Maven 项目里 scope 写成了provided或test。
解决:先确认lib目录下的 jar 在模块依赖里;再看代码里Class.forName()写的是哪个类名,和 jar 版本对齐;Maven 项目检查pom.xml的 scope 是不是默认的compile。
4.3 余额精度丢失:用 double 存金额的后果
现象:存 0.1 元三次,查余额显示 0.30000000000000004。
原因:double和float是二进制浮点数,无法精确表示十进制小数。
解决:数据库字段用DECIMAL(15,2),Java 端用BigDecimal,并且new BigDecimal("0.1")用字符串构造,不要用new BigDecimal(0.1)。BigDecimal的add、subtract、compareTo方法也要用对,equals在比较数值时不可靠,用compareTo。
4.4 登录后界面卡死:Swing 事件线程里做了耗时操作
现象:点击登录后界面无响应几秒钟,然后才跳转。
原因:Swing 的按钮回调运行在 EDT(事件分发线程)里,如果在里面直接查数据库,数据库慢的时候整个界面就冻住。
解决:把数据库查询放到SwingWorker的doInBackground里,done方法里再更新界面。这套源码如果没做,数据量小的时候感觉不出来,一旦流水表上万条,登录就会明显卡顿。
4.5 流水查询越查越慢:没走索引的全表扫描
现象:刚开始查流水很快,数据到几万条后查询要好几秒。
原因:transaction_record表如果没建card_no索引,每次查询都是全表扫描。
解决:执行EXPLAIN SELECT * FROM transaction_record WHERE card_no = 'xxx',看type是不是ALL。是的话加索引ALTER TABLE transaction_record ADD INDEX idx_card_no (card_no);。另外查询时加LIMIT分页,别一次性SELECT *全捞出来。
5. 进阶改造与验证:把课程设计变成能写进简历的项目
5.1 用 DAO 分层替换散落的 JDBC 代码
原始源码里 SQL 和业务逻辑混在一起,改一个字段要翻好几个文件。我一般会做一层 DAO 接口,把AccountDao、TransactionDao、UserDao抽出来,业务层只调方法不碰 SQL。这样后面换数据库、加缓存、写单元测试都有地方下手。
public interface AccountDao { Account findByCardNo(String cardNo); boolean updateBalance(Connection conn, String cardNo, BigDecimal delta); boolean insert(Connection conn, Account acc); }接口里updateBalance带Connection参数,是为了让转账这种跨 DAO 的操作能共用同一个事务。实现类里只写 SQL 和参数绑定,业务层负责事务边界。这个改造做完,代码量可能多 30%,但可维护性完全不一样。
5.2 加一层密码哈希,别让数据库里躺着明文
原始版本如果password字段直接存明文,任何能看数据库的人都能拿到所有账户密码。常见做法是用MessageDigest做 SHA-256 加盐,或者引入BCrypt。最低成本是 SHA-256 加固定盐:
public static String hashPassword(String raw, String salt) throws NoSuchAlgorithmException { MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update((raw + salt).getBytes(StandardCharsets.UTF_8)); byte[] digest = md.digest(); StringBuilder sb = new StringBuilder(); for (byte b : digest) { sb.append(String.format("%02x", b)); } return sb.toString(); }盐值每个用户不同,存在user表里加一列salt。登录时用同样方式算一遍比对。这一步加上,项目档次立刻从「课程作业」变成「有安全意识」。
5.3 验证方法:用三条 SQL 确认业务正确性
改完代码别急着点界面,先用 SQL 验证数据一致性。第一条,查所有账户余额总和,转账前后应该不变:
SELECT SUM(balance) FROM account;第二条,查流水表和账户表的对账关系,每个账户的流水加减应该等于当前余额:
SELECT a.card_no, a.balance, SUM(CASE WHEN t.type IN ('DEPOSIT','TRANSFER_IN') THEN t.amount WHEN t.type IN ('WITHDRAW','TRANSFER_OUT') THEN -t.amount ELSE 0 END) AS calc_balance FROM account a LEFT JOIN transaction_record t ON a.card_no = t.card_no GROUP BY a.card_no, a.balance HAVING a.balance <> calc_balance;这条查出来如果有行,说明流水和余额对不上,事务或者流水记录逻辑有问题。第三条,查有没有余额为负的账户:
SELECT * FROM account WHERE balance < 0;正常业务下这条应该永远返回空。三条 SQL 跑完都干净,这套系统的核心资金逻辑才算真正可信。
从那以后我每次拿到这类银行管理系统源码,都强制先跑一遍对账 SQL 再点界面,界面能点通不代表数据是对的。希望帮到你。
本文还有配套的精品资源,点击获取