Java JDBC银行系统实战:事务控制与ACID落地

发布时间:2026/10/8 8:28:10
Java JDBC银行系统实战:事务控制与ACID落地 简介这是一份面向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人学习下载适合课程实训、期末综合练习或JavaMySQL基础整合实践。读者可直接导入Eclipse运行快速掌握JDBC连接、CRUD操作、用户权限分层设计及控制台交互逻辑同时获得结构清晰的工程目录范例与可调试的完整代码链路。1. 这不是“JavaMySQL”简单拼凑银行管理系统期末项目本质是用JDBC把业务逻辑钉进事务边界里你手上的这个“Java大数据期末银行管理系统Eclipse项目/MySQL数据库”表面看是教务系统里最常见的一类课设——但真正拉开差距的从来不是能不能跑起来而是能不能在转账、开户、日志写入这三件事上让MySQL的ACID不被Java线程和Eclipse默认配置悄悄吃掉。我带过7届Java课设每年都有学生在“余额扣减后查不到流水”“并发两笔转账导致总金额多出100块”这类问题上卡死三天最后交个“功能基本可用”的版本草草了事。这不是代码写得不够多而是从第一行Connection conn DriverManager.getConnection(...)开始就缺了一层对事务隔离级别、连接池生命周期、Eclipse构建路径下资源加载顺序的预判。它适合两类人一类是刚学完JDBC但还没碰过真实业务约束的学生另一类是想用最小成本验证自己能否把“银行”二字背后隐含的强一致性要求落地到本地EclipseMySQL环境的准开发者。别急着建表、别急着写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 ProjectProject name 填BankSystem不要用中文、空格、特殊字符Use project folder as root directory ✅JRE选已安装的JDK 17Eclipse 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 Workbenchmysql -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中偶发NullPointerExceptionEclipse内部ClassLoader冲突。在src/下新建包util创建类DBUtil.javapackage util; import java.sql.*; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/bank_system?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue; 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则骨架打通。注意serverTimezoneAsia/Shanghai是必须参数否则插入时间字段报错allowPublicKeyRetrievaltrue解决MySQL 8.0默认RSA密钥交换问题。3. 核心业务落地开户、转账、查询三件事全在JDBC事务里闭环3.1 开户操作INSERT必须绑定客户与账户且需生成唯一账号银行开户不是简单插两条记录——它要求客户信息先入库获取自增ID账户号需按规则生成如BANK2024000001不能用UUID不符合银行业务习惯两者必须在同一个事务内完成否则出现“有客户无账户”脏数据。在src/下建包service创建AccountService.javapackage 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 转账操作双账户更新流水记录三步原子性缺一不可转账是检验事务能力的试金石。必须同时完成转出账户余额扣减转入账户余额增加插入双向流水记录typeTRANSFER。三者缺一就是资金缺口。继续在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.javapackage 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.javapackage service; import model.AccountVO; import util.DBUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class QueryService { public ListAccountVO 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 ?; ListAccountVO 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通配符但整个字符串通过?传入杜绝拼接SQLqueryAccountByNumber用精确匹配避免转账时输错半个字符导致转错人。4. EclipseMySQL组合避坑指南那些让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.Driver8.0.x必须用com.mysql.cj.jdbc.Driver。解决右键项目 → Build Path → Configure Build Path → Libraries → 确认mysql-connector-java-8.0.33.jar在列表中切换到Order and Export标签页勾选该jar左侧复选框检查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而非账户IDMySQL表引擎不是InnoDBMyISAM不支持事务。解决在transfer()开头强制加conn.setAutoCommit(false)打印SQL执行前的参数System.out.printf(UPDATE accounts SET balancebalance-%.2f WHERE id%d%n, amount, fromAccountId);执行SHOW CREATE TABLE accounts;确认ENGINEInnoDB否则ALTER TABLE accounts ENGINEInnoDB;。4.3 现象中文姓名存入MySQL后变成??或乱码原因MySQL服务器默认字符集是latin1非utf8mb4JDBC URL中未指定characterEncodingutf8mb4Eclipse工作空间编码不是UTF-8影响.java文件读取。解决MySQL中执行SET NAMES utf8mb4;并修改my.iniWindows或my.cnfLinux[client] default-character-set utf8mb4 [mysqld] character-set-server utf8mb4 collation-server utf8mb4_unicode_ciJDBC URL追加?useUnicodetruecharacterEncodingutf8mb4EclipseWindow → 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。解决右键项目 → Properties → Java Build Path → Libraries → 移除错误JREAdd Library → JRE System Library → Execution environment → JavaSE-17删除项目根目录下.classpath、.project右键项目 → Configure → Convert to Maven Project取消勾选再Rebuild终极方案File → Import → Existing Projects into Workspace重新导入。4.5 现象ResultSet.next()永远返回false查不到数据原因PreparedStatement参数未设置ps.setString(1, value)漏写SQL中表名或字段名大小写错误Linux下MySQL默认区分大小写ResultSet游标已在循环中被提前消费如调用过一次next()后未重置。解决在executeQuery()前打印参数System.out.println(Query param: name);执行SELECT * FROM customers LIMIT 1;确认表结构确保while (rs.next())是唯一遍历方式勿在循环内再调rs.next()。5. 期末答辩高分技巧用三个真实场景证明你懂“银行”而不仅是“Java”5.1 场景一演示“并发转账”——用两个线程同时转同一笔钱证明事务隔离有效期末答辩老师最爱问“如果两个人同时给同一个账户转账会怎样” 这不是考你Thread.sleep()而是看你是否理解READ COMMITTED隔离级别下的幻读与不可重复读。实操步骤写一个ConcurrentTransferDemo.java启动两个线程都向账号BANK2024000001转100元在transfer()方法开头加日志System.out.println(Thread.currentThread().getName() 开始转账...);观察输出两个线程日志交错但最终余额只增加200非300且transactions表有两条记录。话术重点“我设置了conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED)MySQL默认级别。当线程A执行UPDATE accounts SET balancebalance100时会对该行加行锁线程B必须等待A提交后才能更新——这就是MVCC机制在起作用。如果去掉事务余额会凭空多出100。”5.2 场景二故意制造“转账失败”展示异常处理与回滚证据老师会说“你代码里写了try-catch但怎么证明真的回滚了”实操步骤修改transfer()中updateToSQL把WHERE id ?改成WHERE id ? AND 10强制更新0行运行转账观察控制台输出❌ 转账失败且数据库中转出账户余额未扣减手动查SELECT balance FROM accounts WHERE id ?确认余额不变。话术重点“我在catch块里显式调用conn.rollback()并捕获了SQLException的getSQLState()值如23000主键冲突、45000自定义异常。这里故意让SQL失效证明事务不是摆设——只要commit()没执行所有DML操作都不落地。”5.3 场景三导出交易流水为CSV体现“大数据”中的“数据”二字标题里有“大数据”但不必上Hadoop。用Java原生IO导出万级流水就是最实在的“数据处理”。实操代码ExportService.javapackage 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()调用盯住三分钟——那里藏着比任何框架都硬核的工程敬畏。希望帮到你。本文还有配套的精品资源点击获取