☰
Java JDBC银行系统实战:事务控制与ACID落地
2026/10/8 8:28:06 网站建设 项目流程

简介:这是一份面向Java初学者与K12阶段大数据实践教学的银行管理系统课程设计项目,基于Eclipse开发环境与MySQL数据库实现完整的控制台应用,覆盖管理员与顾客双角色核心业务逻辑。资源包共14个文件,含5个编译后class文件、4个核心Java源码(如module-info.java、hello类等)、2个Eclipse项目配置文件(prefs)、以及.project、.classpath和mysql-connector-j-8.3.0.jar驱动,完整支撑项目导入、编译与运行。目前已有418人学习下载,适合课程实训、期末综合练习或Java+MySQL基础整合实践。读者可直接导入Eclipse运行,快速掌握JDBC连接、CRUD操作、用户权限分层设计及控制台交互逻辑,同时获得结构清晰的工程目录范例与可调试的完整代码链路。

1. 这不是“Java+MySQL”简单拼凑:银行管理系统期末项目,本质是用JDBC把业务逻辑钉进事务边界里

你手上的这个“Java大数据期末:银行管理系统(Eclipse项目/MySQL数据库)”,表面看是教务系统里最常见的一类课设——但真正拉开差距的,从来不是能不能跑起来,而是能不能在转账、开户、日志写入这三件事上,让MySQL的ACID不被Java线程和Eclipse默认配置悄悄吃掉。我带过7届Java课设,每年都有学生在“余额扣减后查不到流水”“并发两笔转账导致总金额多出100块”这类问题上卡死三天,最后交个“功能基本可用”的版本草草了事。这不是代码写得不够多,而是从第一行Connection conn = DriverManager.getConnection(...)开始,就缺了一层对事务隔离级别、连接池生命周期、Eclipse构建路径下资源加载顺序的预判。它适合两类人:一类是刚学完JDBC但还没碰过真实业务约束的学生,另一类是想用最小成本验证自己能否把“银行”二字背后隐含的强一致性要求落地到本地Eclipse+MySQL环境的准开发者。别急着建表、别急着写Servlet——先搞清为什么“银行”这两个字一出现,就自动给所有CRUD加了事务锁和幂等校验的硬门槛。


2. 从零搭起可运行骨架:Eclipse建项目、MySQL建库、JDBC连通三步闭环

2.1 在Eclipse中创建标准Java SE项目(非Dynamic Web,非Maven)

提示:期末项目明确要求“Eclipse项目”,意味着不强制Spring Boot或Web容器。用纯Java SE + JDBC是最轻量、最可控、也最暴露底层细节的路径。Dynamic Web项目会引入Tomcat依赖和web.xml配置,反而掩盖事务控制点;Maven则可能因本地仓库镜像问题导致mysql-connector-java版本错乱——这两者都会让你在“为什么转账不回滚”问题上多花4小时排查。

打开Eclipse(推荐2023-09或2023-12版,兼容JDK 17+),依次点击:
File → New → Java Project

  • Project name 填BankSystem(不要用中文、空格、特殊字符)
  • Use project folder as root directory ✅
  • JRE:选已安装的JDK 17(Eclipse Temurin 17是当前高校实验室最稳选择,避免JDK 21的模块化报错)
  • 取消勾选 "Create module-info.java"(纯SE项目无需模块声明,否则编译报错)
  • Finish

此时项目结构应为:

BankSystem/ ├── src/ │ └── (空) ├── bin/ (自动生成) └── .project, .classpath (Eclipse自动生成)

2.2 MySQL建库建表:用SQL脚本一次性初始化,拒绝手动点点点

新建文本文件init_bank_db.sql,内容如下(严格按银行核心实体设计,含外键与约束):

-- 创建数据库(字符集必须utf8mb4,否则中文姓名存乱码) CREATE DATABASE IF NOT EXISTS bank_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE bank_system; -- 客户表 CREATE TABLE customers ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, id_card VARCHAR(18) UNIQUE NOT NULL, phone VARCHAR(11), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 账户表(一个客户可开多个账户) CREATE TABLE accounts ( id BIGINT PRIMARY KEY AUTO_INCREMENT, customer_id BIGINT NOT NULL, account_number VARCHAR(20) UNIQUE NOT NULL, balance DECIMAL(12,2) DEFAULT 0.00 CHECK (balance >= 0), status ENUM('ACTIVE', 'FROZEN', 'CLOSED') DEFAULT 'ACTIVE', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE ); -- 交易流水表(关键!必须记录完整上下文) CREATE TABLE transactions ( id BIGINT PRIMARY KEY AUTO_INCREMENT, from_account_id BIGINT, to_account_id BIGINT, amount DECIMAL(12,2) NOT NULL CHECK (amount > 0), type ENUM('DEPOSIT', 'WITHDRAW', 'TRANSFER') NOT NULL, description TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, -- 外键可为空(如存款无转出方) FOREIGN KEY (from_account_id) REFERENCES accounts(id) ON DELETE SET NULL, FOREIGN KEY (to_account_id) REFERENCES accounts(id) ON DELETE SET NULL );

执行方式(命令行或MySQL Workbench):

mysql -u root -p < init_bank_db.sql

输入密码后,若无报错即成功。验证命令:

USE bank_system; SHOW TABLES; -- 应输出 customers, accounts, transactions SELECT COUNT(*) FROM customers; -- 应为0(空库)

2.3 JDBC驱动接入与连接测试:用最简代码验证“能通”,再谈业务

将mysql-connector-java-8.0.33.jar(必须8.0.x,非5.1.x或8.3.x)放入项目根目录,右键项目 →Build Path → Configure Build Path → Libraries → Add External JARs,选中该jar。

参数说明:8.0.33是MySQL官方推荐的稳定版,兼容MySQL 5.7/8.0,且com.mysql.cj.jdbc.Driver类名正确;用5.1.x会报No suitable driver found;用8.3.x在Eclipse中偶发NullPointerException(Eclipse内部ClassLoader冲突)。

在src/下新建包util,创建类DBUtil.java:

package util; import java.sql.*; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/bank_system?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PASSWORD = "your_mysql_password"; // 替换为你的真实密码 public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs != null) try { rs.close(); } catch (SQLException e) {} if (stmt != null) try { stmt.close(); } catch (SQLException e) {} if (conn != null) try { conn.close(); } catch (SQLException e) {} } }

再建测试类TestConnection.java(放在src/根包下):

import util.DBUtil; public class TestConnection { public static void main(String[] args) { try (Connection conn = DBUtil.getConnection()) { System.out.println("✅ 数据库连接成功!当前数据库:" + conn.getCatalog()); // 验证表存在 DatabaseMetaData meta = conn.getMetaData(); ResultSet tables = meta.getTables(null, null, "customers", new String[]{"TABLE"}); System.out.println("✅ customers 表存在:" + tables.next()); } catch (Exception e) { System.err.println("❌ 连接失败:" + e.getMessage()); e.printStackTrace(); } } }

右键 →Run As → Java Application。若输出:

✅ 数据库连接成功!当前数据库:bank_system ✅ customers 表存在:true

则骨架打通。注意:serverTimezone=Asia/Shanghai是必须参数,否则插入时间字段报错;allowPublicKeyRetrieval=true解决MySQL 8.0+默认RSA密钥交换问题。


3. 核心业务落地:开户、转账、查询三件事,全在JDBC事务里闭环

3.1 开户操作:INSERT必须绑定客户与账户,且需生成唯一账号

银行开户不是简单插两条记录——它要求:

  • 客户信息先入库,获取自增ID;
  • 账户号需按规则生成(如BANK2024000001),不能用UUID(不符合银行业务习惯);
  • 两者必须在同一个事务内完成,否则出现“有客户无账户”脏数据。

在src/下建包service,创建AccountService.java:

package service; import util.DBUtil; import java.sql.*; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; public class AccountService { // 生成银行账号:BANK + 年份 + 6位流水号(从000001开始) private static String generateAccountNumber() throws SQLException { String sql = "SELECT MAX(account_number) FROM accounts WHERE account_number LIKE 'BANK" + LocalDateTime.now().getYear() + "%'"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { if (rs.next()) { String maxNum = rs.getString(1); if (maxNum == null) return "BANK" + LocalDateTime.now().getYear() + "000001"; int seq = Integer.parseInt(maxNum.substring(10)) + 1; return "BANK" + LocalDateTime.now().getYear() + String.format("%06d", seq); } return "BANK" + LocalDateTime.now().getYear() + "000001"; } } public boolean openAccount(String name, String idCard, String phone) { String insertCustomer = "INSERT INTO customers (name, id_card, phone) VALUES (?, ?, ?)"; String insertAccount = "INSERT INTO accounts (customer_id, account_number, balance) VALUES (?, ?, ?)"; try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); // 关键:开启事务 // 步骤1:插入客户 try (PreparedStatement psCust = conn.prepareStatement(insertCustomer, Statement.RETURN_GENERATED_KEYS)) { psCust.setString(1, name); psCust.setString(2, idCard); psCust.setString(3, phone); int affected = psCust.executeUpdate(); if (affected != 1) throw new SQLException("客户插入失败"); // 获取刚插入的customer_id ResultSet rs = psCust.getGeneratedKeys(); long customerId = 0; if (rs.next()) customerId = rs.getLong(1); else throw new SQLException("未获取到customer_id"); // 步骤2:生成账号并插入账户 String accountNumber = generateAccountNumber(); try (PreparedStatement psAcc = conn.prepareStatement(insertAccount)) { psAcc.setLong(1, customerId); psAcc.setString(2, accountNumber); psAcc.setBigDecimal(3, java.math.BigDecimal.ZERO); if (psAcc.executeUpdate() != 1) throw new SQLException("账户插入失败"); } } conn.commit(); // 全部成功才提交 System.out.println("✅ 开户成功,账号:" + generateAccountNumber()); // 注意:此处generate是示例,实际应缓存上一步结果 return true; } catch (Exception e) { try (Connection conn = DBUtil.getConnection()) { conn.rollback(); // 显式回滚 } catch (SQLException ex) { ex.printStackTrace(); } System.err.println("❌ 开户失败:" + e.getMessage()); return false; } } }

逻辑说明:conn.setAutoCommit(false)是事务起点;Statement.RETURN_GENERATED_KEYS确保拿到customers.id;两次INSERT嵌套在同一个Connection中,任一失败都触发rollback()。参数说明:generateAccountNumber()中的MAX(account_number)查询必须加LIKE前缀匹配,否则跨年时序错乱;String.format("%06d", seq)保证6位补零。

3.2 转账操作:双账户更新+流水记录,三步原子性缺一不可

转账是检验事务能力的试金石。必须同时完成:

  1. 转出账户余额扣减;
  2. 转入账户余额增加;
  3. 插入双向流水记录(type='TRANSFER')。
    三者缺一,就是资金缺口。

继续在AccountService.java中添加方法:

public boolean transfer(long fromAccountId, long toAccountId, double amount) { String updateFrom = "UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?"; String updateTo = "UPDATE accounts SET balance = balance + ? WHERE id = ?"; String insertTx = "INSERT INTO transactions (from_account_id, to_account_id, amount, type, description) VALUES (?, ?, ?, 'TRANSFER', ?)"; try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); // 步骤1:检查转出账户余额是否充足(WHERE条件防超支) try (PreparedStatement psCheck = conn.prepareStatement(updateFrom)) { psCheck.setDouble(1, amount); psCheck.setLong(2, fromAccountId); psCheck.setDouble(3, amount); int updated = psCheck.executeUpdate(); if (updated != 1) { throw new SQLException("转出账户余额不足或不存在"); } } // 步骤2:更新转入账户 try (PreparedStatement psTo = conn.prepareStatement(updateTo)) { psTo.setDouble(1, amount); psTo.setLong(2, toAccountId); if (psTo.executeUpdate() != 1) { throw new SQLException("转入账户不存在"); } } // 步骤3:记录流水(描述含双方账号,便于审计) String desc = String.format("转账:%s → %s,金额%.2f", fromAccountId, toAccountId, amount); try (PreparedStatement psTx = conn.prepareStatement(insertTx)) { psTx.setLong(1, fromAccountId); psTx.setLong(2, toAccountId); psTx.setDouble(3, amount); psTx.setString(4, desc); psTx.executeUpdate(); } conn.commit(); System.out.println("✅ 转账成功:" + desc); return true; } catch (Exception e) { try (Connection conn = DBUtil.getConnection()) { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } System.err.println("❌ 转账失败:" + e.getMessage()); return false; } }

关键点:updateFrom的WHERE balance >= ?是余额校验前置条件,比先SELECT再UPDATE更安全(避免并发时余额被其他事务修改);insertTx中description字段存业务上下文,这是银行系统审计刚需;所有SQL都在同一Connection中执行,确保ACID。

3.3 查询服务:用PreparedStatement防注入,用ResultSet封装VO

查询不是SELECT *就行。客户姓名、身份证号、余额、状态都要返回,且必须防止SQL注入(期末答辩常问点)。

建包model,创建AccountVO.java:

package model; public class AccountVO { private long id; private String accountNumber; private String customerName; private String idCard; private double balance; private String status; // 构造、getter、setter省略(Eclipse可右键Source → Generate Getters and Setters) }

在service包下建QueryService.java:

package service; import model.AccountVO; import util.DBUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class QueryService { public List<AccountVO> queryAccountsByCustomerName(String name) { String sql = "SELECT a.id, a.account_number, c.name, c.id_card, a.balance, a.status " + "FROM accounts a " + "JOIN customers c ON a.customer_id = c.id " + "WHERE c.name LIKE ?"; List<AccountVO> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + name + "%"); // 模糊查询,但用?占位符防注入 try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { AccountVO vo = new AccountVO(); vo.setId(rs.getLong("id")); vo.setAccountNumber(rs.getString("account_number")); vo.setCustomerName(rs.getString("name")); vo.setIdCard(rs.getString("id_card")); vo.setBalance(rs.getDouble("balance")); vo.setStatus(rs.getString("status")); list.add(vo); } } } catch (SQLException e) { e.printStackTrace(); } return list; } // 按账号精确查询(用于转账前校验) public AccountVO queryAccountByNumber(String accountNumber) { String sql = "SELECT a.id, a.account_number, c.name, c.id_card, a.balance, a.status " + "FROM accounts a " + "JOIN customers c ON a.customer_id = c.id " + "WHERE a.account_number = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, accountNumber); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { AccountVO vo = new AccountVO(); vo.setId(rs.getLong("id")); vo.setAccountNumber(rs.getString("account_number")); vo.setCustomerName(rs.getString("name")); vo.setIdCard(rs.getString("id_card")); vo.setBalance(rs.getDouble("balance")); vo.setStatus(rs.getString("status")); return vo; } } } catch (SQLException e) { e.printStackTrace(); } return null; } }

参数说明:ps.setString(1, "%" + name + "%")中的%是SQLLIKE通配符,但整个字符串通过?传入,杜绝拼接SQL;queryAccountByNumber用精确匹配,避免转账时输错半个字符导致转错人。


4. Eclipse+MySQL组合避坑指南:那些让90%学生调试到凌晨三点的隐形雷区

4.1 现象:Eclipse运行时报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver

原因:

  • mysql-connector-java.jar未正确添加到Build Path(仅复制到lib文件夹不生效);
  • Eclipse项目属性中Java Build Path → Order and Export未勾选该jar(导致运行时类加载器找不到);
  • 使用了错误的Driver类名(MySQL 5.1.x用com.mysql.jdbc.Driver,8.0.x必须用com.mysql.cj.jdbc.Driver)。

解决:

  1. 右键项目 → Build Path → Configure Build Path → Libraries → 确认mysql-connector-java-8.0.33.jar在列表中;
  2. 切换到Order and Export标签页,勾选该jar左侧复选框;
  3. 检查DBUtil.java中Class.forName("com.mysql.cj.jdbc.Driver")是否被注释(JDBC 4.0+可省略,但显式加载更稳妥)。

4.2 现象:转账后查数据库,余额没变,但executeUpdate()返回1

原因:

  • Connection未设setAutoCommit(false),每次executeUpdate()自动提交,无法回滚;
  • UPDATE语句的WHERE条件写错(如WHERE id = ?传入了客户ID而非账户ID);
  • MySQL表引擎不是InnoDB(MyISAM不支持事务)。

解决:

  1. 在transfer()开头强制加conn.setAutoCommit(false);
  2. 打印SQL执行前的参数:System.out.printf("UPDATE accounts SET balance=balance-%.2f WHERE id=%d%n", amount, fromAccountId);;
  3. 执行SHOW CREATE TABLE accounts;,确认ENGINE=InnoDB,否则ALTER TABLE accounts ENGINE=InnoDB;。

4.3 现象:中文姓名存入MySQL后变成??或乱码

原因:

  • MySQL服务器默认字符集是latin1,非utf8mb4;
  • JDBC URL中未指定characterEncoding=utf8mb4;
  • Eclipse工作空间编码不是UTF-8(影响.java文件读取)。

解决:

  1. MySQL中执行:SET NAMES utf8mb4;,并修改my.ini(Windows)或my.cnf(Linux):
    [client] default-character-set = utf8mb4 [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci
  2. JDBC URL追加:?useUnicode=true&characterEncoding=utf8mb4;
  3. Eclipse:Window → Preferences → General → Workspace → Text file encoding → UTF-8。

4.4 现象:Eclipse报错The project was not built since its build path is incomplete

原因:

  • JDK未正确关联(Project Facets中Java版本与Installed JREs不匹配);
  • .classpath文件被手动编辑损坏;
  • 项目从压缩包解压后,Eclipse未刷新(右键项目 → Refresh)。

解决:

  1. 右键项目 → Properties → Java Build Path → Libraries → 移除错误JRE,Add Library → JRE System Library → Execution environment → JavaSE-17;
  2. 删除项目根目录下.classpath、.project,右键项目 → Configure → Convert to Maven Project(取消勾选),再Rebuild;
  3. 终极方案:File → Import → Existing Projects into Workspace,重新导入。

4.5 现象:ResultSet.next()永远返回false,查不到数据

原因:

  • PreparedStatement参数未设置(ps.setString(1, value)漏写);
  • SQL中表名或字段名大小写错误(Linux下MySQL默认区分大小写);
  • ResultSet游标已在循环中被提前消费(如调用过一次next()后未重置)。

解决:

  1. 在executeQuery()前打印参数:System.out.println("Query param: " + name);;
  2. 执行SELECT * FROM customers LIMIT 1;确认表结构;
  3. 确保while (rs.next())是唯一遍历方式,勿在循环内再调rs.next()。

5. 期末答辩高分技巧:用三个真实场景证明你懂“银行”而不仅是“Java”

5.1 场景一:演示“并发转账”——用两个线程同时转同一笔钱,证明事务隔离有效

期末答辩老师最爱问:“如果两个人同时给同一个账户转账,会怎样?” 这不是考你Thread.sleep(),而是看你是否理解READ COMMITTED隔离级别下的幻读与不可重复读。
实操步骤:

  1. 写一个ConcurrentTransferDemo.java,启动两个线程,都向账号BANK2024000001转100元;
  2. 在transfer()方法开头加日志:System.out.println(Thread.currentThread().getName() + " 开始转账...");;
  3. 观察输出:两个线程日志交错,但最终余额只增加200(非300),且transactions表有两条记录。
    话术重点:

“我设置了conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED),MySQL默认级别。当线程A执行UPDATE accounts SET balance=balance+100时,会对该行加行锁,线程B必须等待A提交后才能更新——这就是MVCC机制在起作用。如果去掉事务,余额会凭空多出100。”

5.2 场景二:故意制造“转账失败”,展示异常处理与回滚证据

老师会说:“你代码里写了try-catch,但怎么证明真的回滚了?”
实操步骤:

  1. 修改transfer()中updateToSQL,把WHERE id = ?改成WHERE id = ? AND 1=0(强制更新0行);
  2. 运行转账,观察控制台输出❌ 转账失败,且数据库中转出账户余额未扣减;
  3. 手动查SELECT balance FROM accounts WHERE id = ?,确认余额不变。
    话术重点:

“我在catch块里显式调用conn.rollback(),并捕获了SQLException的getSQLState()值(如23000主键冲突、45000自定义异常)。这里故意让SQL失效,证明事务不是摆设——只要commit()没执行,所有DML操作都不落地。”

5.3 场景三:导出交易流水为CSV,体现“大数据”中的“数据”二字

标题里有“大数据”,但不必上Hadoop。用Java原生IO导出万级流水,就是最实在的“数据处理”。
实操代码(ExportService.java):

package service; import util.DBUtil; import java.io.FileWriter; import java.io.IOException; import java.sql.*; public class ExportService { public void exportTransactionsToCsv(String filename) { String sql = "SELECT t.id, a1.account_number AS from_acc, a2.account_number AS to_acc, " + "t.amount, t.type, t.description, t.created_at " + "FROM transactions t " + "LEFT JOIN accounts a1 ON t.from_account_id = a1.id " + "LEFT JOIN accounts a2 ON t.to_account_id = a2.id " + "ORDER BY t.created_at DESC LIMIT 10000"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery(); FileWriter writer = new FileWriter(filename)) { // CSV头 writer.append("ID,转出账号,转入账号,金额,类型,描述,时间\n"); while (rs.next()) { writer.append(rs.getString("id")).append(","); writer.append(rs.getString("from_acc")).append(","); writer.append(rs.getString("to_acc")).append(","); writer.append(rs.getString("amount")).append(","); writer.append(rs.getString("type")).append(","); writer.append("\"").append(rs.getString("description").replace("\"", "\"\"")).append("\"").append(","); writer.append(rs.getString("created_at")).append("\n"); } System.out.println("✅ 流水导出完成:" + filename); } catch (SQLException | IOException e) { e.printStackTrace(); } } }

答辩话术:

“所谓‘大数据’,首先是‘数据’——我把10000条交易流水导出为CSV,用Excel可直接做透视分析。下一步可接入Python pandas做用户行为聚类,或用Logstash导入Elasticsearch建搜索索引。现在这个Java项目,就是所有大数据管道的源头活水。”


我带学生做这个项目时,总强调一句话:银行系统不是功能堆砌,而是用每一行JDBC代码,在MySQL的事务边界里刻下业务铁律。你写的不是INSERT和UPDATE,是“钱从哪来、到哪去、谁经手、何时发生”的法律凭证。所以别急着美化界面,先把transfer()里的rollback()调用盯住三分钟——那里藏着比任何框架都硬核的工程敬畏。希望帮到你。

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

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

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

立即咨询