ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

银行管理系统Java课程设计:JDBC连接池与事务转账实战解析

银行管理系统Java课程设计:JDBC连接池与事务转账实战解析 简介银行管理系统JavaMySQL8是一套面向Java学习者与课程设计场景的完整参考项目旨在演示银行核心业务的后端实现。系统覆盖查询余额、转账、存款、取款、修改密码、开户、自动生成密码及账号管理等常见功能采用Java处理业务逻辑MySQL8存储数据并通过DBCP连接池优化数据库访问性能。资源包为zip压缩包共38个文件包含13个Java源码、13个class编译文件、4个jar依赖库含MySQL8驱动与DBCP组件、4个xml配置和2个properties文件整体大小约2.59MB目录结构清晰便于导入IDE后直接运行与调试。目前已有2005人学习下载适合作为课程设计、期末项目或JavaWeb入门实战的参考。通过这套资源读者可以学习JDBC与MySQL8的整合方法理解DBCP连接池的配置与使用并借鉴转账等场景中的事务处理与异常回滚思路快速搭建一个可运行的银行管理系统雏形。1. 银行管理系统javamysql8不是玩具项目是能跑通全流程的课程设计做Java课程设计或者毕设的人十有八九避不开银行管理系统。这个名字听起来像教学 demo但它实际上是检验你对 Java 后端基本功最好的试金石连接池、事务、JDBC、SQL 正确性一个都不少。我拆的这个项目是基于 Java MySQL8 的银行管理系统核心功能覆盖了查询余额、转账、存款、取款、修改密码、开户、自动生成密码以及账户管理。后端用 Java 写业务逻辑MySQL8 负责持久化DBCP 连接池管数据库连接。它不是那种花哨的前后端分离项目没有 Spring Boot没有 MyBatis就是最朴素的 JDBC 连接池 Servlet 的写法。恰恰因为朴素代码里每个环节都能看清楚——连接从哪来、事务怎么提交、SQL 为什么会错。对刚学完 Java SE 和 MySQL、准备做课设的人来说这份资源能让你少走很多弯路对已经工作的人拆一遍能捡起不少被框架掩盖的底层细节。2. 工程结构与 DBCP 连接池先把依赖和配置跑起来拿到压缩包以后第一件事不是急着打开 IDE而是先看一眼目录结构搞清楚每个文件是干什么的。这个项目的路径不算复杂但如果你以前只写过单文件 Java 程序第一次见到 lib、src、out 这种目录可能会有点懵。2.1 解压之后的目录里都有什么解压 atm.zip 之后核心内容大概是下面这几块atm/ ├── src/ # Java 源码目录 ├── lib/ # 第三方依赖 jar 包 │ ├── commons-dbcp-1.4.jar │ ├── commons-pool-1.6.jar │ ├── commons-collections-3.2.1.jar │ └── mysql-connector-java-8.0.11.jar ├── dbcp.properties # 数据库连接池配置文件 ├── out/production/ # 编译后的 class 文件输出目录 └── .idea/ # IntelliJ IDEA 工程配置src 下面就是 Java 源码按功能模块分包。lib 里的四个 jar 是关键依赖mysql-connector-java-8.0.11.jar 是 MySQL 8 的 JDBC 驱动负责让 Java 和 MySQL 之间能通信commons-dbcp-1.4.jar 是连接池的实现commons-pool 和 commons-collections 是它的底层依赖。我一般拿到这种项目会先打开 dbcp.properties 看一眼因为这门课设九成以上的报错都出在这。配置内容大致长这样driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/bank?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 usernameroot passwordyour_password initialSize5 maxActive20 maxIdle10 minIdle2 maxWait3000driverClassName 用的是com.mysql.cj.jdbc.Driver这是 MySQL 8 专用驱动类。如果你以前用的是 MySQL 5.x 时代的com.mysql.jdbc.Driver到 8.0 直接会报 ClassNotFoundException。URL 里面的useSSLfalse是让本地开发环境不要走加密连接serverTimezoneAsia/Shanghai解决时区问题characterEncodingutf8治中文乱码。这几个参数是项目里最容易翻车的地方。2.2 连接池参数怎么理解怎么调DBCP 连接池的核心思路是程序启动时预先创建一批数据库连接放在池子里用的时候借用完归还而不是每次都建立新连接。建立一次 MySQL 连接要经历 TCP 握手、认证、权限检查开销很高银行管理系统这种频繁操作数据库的场景没有连接池根本扛不住。配置文件里那几个数字是重点。initialSize5表示启动时预创建 5 个连接maxActive20是池子里同时最多借出 20 个连接超过就得排队maxIdle10是池中最多保留 10 个空闲连接maxWait3000表示当 20 个连接全部被占用时新请求最多等 3 秒超时就抛异常。实际开发中我会把 maxActive 和 maxWait 调成关联的如果并发上来了、频繁报获取连接超时先把 maxActive 调大再观察连接是不是真的被用完了而不是无脑调大否则内存会先撑不住。读取这个配置文件并创建数据源的代码常见做法是写一个工具类例如import org.apache.commons.dbcp.BasicDataSource; import java.io.InputStream; import java.sql.Connection; import java.sql.SQLException; import java.util.Properties; public class DBUtil { private static BasicDataSource dataSource; static { try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(dbcp.properties)) { Properties props new Properties(); props.load(in); dataSource new BasicDataSource(); dataSource.setDriverClassName(props.getProperty(driverClassName)); dataSource.setUrl(props.getProperty(url)); dataSource.setUsername(props.getProperty(username)); dataSource.setPassword(props.getProperty(password)); dataSource.setInitialSize(Integer.parseInt(props.getProperty(initialSize))); dataSource.setMaxActive(Integer.parseInt(props.getProperty(maxActive))); dataSource.setMaxIdle(Integer.parseInt(props.getProperty(maxIdle))); dataSource.setMinIdle(Integer.parseInt(props.getProperty(minIdle))); dataSource.setMaxWait(Long.parseLong(props.getProperty(maxWait))); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }这段代码里BasicDataSource是 DBCP 的实现类setDriverClassName告诉它用哪个驱动setUrl告诉它连哪个库。注意我用的是类加载器读取dbcp.properties这样配置文件放在 classpath 下就能被找到不用写绝对路径。getConnection()方法每一次调用都是从池子里借连接不是新建连接。提示如果你发现连接时区报错、或者中文乱码问题基本都在serverTimezone和characterEncoding这两个参数上改动后必须重启应用才生效。3. 核心业务逻辑从开户到转账的实现路径连接池只是地基真正体现 Java 能力的是业务代码。这个银行管理系统的功能不算多但每一条都对应真实的银行操作逻辑尤其开户和转账这两块代码写得规范不规范一眼就能看出来。3.1 开户流程账号生成、密码加密、数据落库开户是系统的入口其他功能都建立在账户存在的基础上。开户的逻辑大致是接收用户填写的姓名、身份证号、初始存款系统生成一个唯一的银行账号自动生成一个初始密码然后把数据写入数据库。这里面有两个细节值得注意。第一个是账号生成。如果用数据库自增 id那账户号从 1 开始很容易被猜出来真实银行系统不可能这么干。常见做法是用时间戳加随机数组合或者用专门发号器。在这类课设里我一般会建议用System.currentTimeMillis()加随机后缀的方式生成 16 位或 19 位账号public static String generateAccountNo() { String timestamp String.valueOf(System.currentTimeMillis()); int random (int) ((Math.random() * 9 1) * 100000); return timestamp random; }System.currentTimeMillis()返回当前毫秒时间戳后面拼一个 6 位随机数组合起来基本不会重复。当然这只是课程设计级别的方案真实银行会用更严格的发号算法但思路是一致的账号可预测性越低越安全。第二个是密码存储。自动生成密码一般用随机数产生 6 位数字比如String.valueOf((int) ((Math.random() * 9 1) * 100000))可以保证六位数。但这里有个很容易被忽略的点——密码不能明文存数据库。课程设计里可以不做加密但如果你想在答辩的时候多说两句建议用MessageDigest做一次 MD5 或者 SHA-256 哈希再存public static String md5(String input) { try { MessageDigest md MessageDigest.getInstance(MD5); byte[] digest md.digest(input.getBytes()); StringBuilder sb new StringBuilder(); for (byte b : digest) { sb.append(String.format(%02x, b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(加密失败, e); } }这段代码把密码字符串转成 MD5 摘要然后逐字节转成十六进制字符串。存库的时候存的是摘要而不是原文修改密码时也是先算摘要再比对。这样即使数据库泄露密码也不会直接暴露。开户的 SQL 操作对应的是 INSERT 语句。数据落库之前需要先确认身份证号没有重复开户否则一个身份证多个账户会破坏业务规则。3.2 存款、取款、修改密码一行 UPDATE 背后的核对逻辑存款相对简单输入账号和金额执行UPDATE account SET balance balance ? WHERE account_no ?。取款则是SET balance balance - ?但取款前必须查余额判断是否足够避免出现负数。修改密码则需要先验证旧密码是否正确再执行 UPDATE且新密码不能和旧密码相同这是基本安全规则。这里有个很容易犯的错余额字段用double或者float。银行金额必须用DECIMALJava 代码里对应BigDecimal。浮点数在二进制表示中无法精确表达小数例如 0.1 加 0.2 的结果并不等于 0.3。如果余额用 double 存储时间长了会出现分毫级误差这在银行系统里是不可接受的。数据库建表时余额字段应该写成这样CREATE TABLE account ( account_no VARCHAR(19) PRIMARY KEY, real_name VARCHAR(50) NOT NULL, id_card VARCHAR(18) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, balance DECIMAL(18, 2) NOT NULL DEFAULT 0.00, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );DECIMAL(18, 2)表示最多 18 位数字其中 2 位是小数位整数部分最多 16 位完全够用。主键用VARCHAR(19)对应生成的 19 位账号id_card加UNIQUE约束保证一个身份证只能开一个户。3.3 转账最考验事务意识的功能转账是银行管理系统里最核心、也是面试最容易问的功能。它的业务规则很简单A 账户扣款B 账户加款两个操作必须同时成功或同时失败。代码层面伪代码如下public boolean transfer(String fromAccount, String toAccount, BigDecimal amount) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); BigDecimal balance queryBalance(conn, fromAccount); if (balance.compareTo(amount) 0) { conn.rollback(); return false; } updateBalance(conn, fromAccount, balance.subtract(amount)); updateBalance(conn, toAccount, queryBalance(conn, toAccount).add(amount)); conn.commit(); return true; } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }这里有几个关键点。setAutoCommit(false)关闭自动提交让两条 UPDATE 变成一个逻辑上的原子操作。如果第一条 UPDATE 成功、第二条失败代码会执行rollback()把第一条操作也撤销掉。finally块里把连接归还给连接池之前必须把setAutoCommit(true)改回来否则连接池里的连接会一直保持事务状态下一次被借出时自动提交是关闭的后续操作不 commit 就永远不生效——这是连接池项目里极其隐蔽的坑。余额比较用compareTo(amount)而不是减法因为BigDecimal的subtract如果结果为负你还要再比对符号直接 compareTo 更符合金额比较的场景。数据库层面的 UPDATE 语句常见写法是UPDATE account SET balance balance - ? WHERE account_no ? AND balance ?这样把余额校验放到 SQL 层面可以多一层保护。4. 事务与数据一致性MySQL8 下转账怎么做到不丢钱上一节代码展示了事务的基本写法但如果你只是抄着写完答辩时被追问两句就会露馅。这一节把事务和数据一致性掰开讲清楚。4.1 为什么要手动控制事务MySQL 默认自动提交也就是每一条 UPDATE 或 INSERT 执行完就永久生效。但是转账是一个逻辑操作涉及两条 SQL自动提交意味着第一条成功、第二条失败时前者已经落库无法撤回。所以必须把两条 SQL 包在同一个事务里全部成功才 commit任何一步出错就 rollback。MySQL8 的 InnoDB 存储引擎支持事务MyISAM 不支持。如果你的表还是 MyISAM 引擎上述转账代码完全无效。建表时如果不指定引擎MySQL8 默认是 InnoDB但有些旧课程设计代码里可能会写ENGINEMyISAM一定要注意。4.2 连接池和 ThreadLocal 的配合问题刚才的转账代码里getConnection()和queryBalance(conn, ...)是同一个连接。但如果你在一个方法里多次调用DBUtil.getConnection()每次拿到的可能是池里不同的连接事务就失效了——每条 SQL 在不同的连接上执行各自自动提交rollback 只能回滚其中某一个连接上未提交的操作。正确做法是保证同一个线程内所有数据库操作使用同一个连接。常见解决方案是 ThreadLocalpublic class DBUtil { private static final ThreadLocalConnection threadLocal new ThreadLocal(); public static Connection getConnection() throws SQLException { Connection conn threadLocal.get(); if (conn null || conn.isClosed()) { conn dataSource.getConnection(); threadLocal.set(conn); } return conn; } public static void closeConnection() throws SQLException { Connection conn threadLocal.get(); if (conn ! null) { conn.close(); threadLocal.remove(); } } }ThreadLocal 的原理是每个线程维护一份独立的变量副本线程 A 拿到的连接线程 B 拿不到。这样转账过程中无论中间调用了多少次getConnection()只要在同一线程拿到的都是同一个连接。ThreadLocal用完之后必须remove()否则在线程池环境下线程复用会导致连接泄漏。4.3 隔离级别与并发扣款事务的 ACID 特性里隔离性由隔离级别控制。MySQL8 默认隔离级别是REPEATABLE READ意思是同一个事务内多次读取同一数据结果保持一致。这对银行系统很重要假设两个事务同时读取余额 1000 元各自扣款 100 元如果隔离级别是READ UNCOMMITTED两个事务都能读到 1000最终余额变成 900 而不是 800钱就凭空少了一部分。REPEATABLE READ通过快照读解决了这个问题但在某些极端场景下也有陷阱。如果你只靠 Java 代码里的if (balance.compareTo(amount) 0)做校验两个线程同时进入转账方法都读到余额 1000都判断通过然后各自执行扣款最终余额可能变成负数。应对方式是 SQL 层加条件UPDATE account SET balance balance - ? WHERE account_no ? AND balance ?这条语句里balance ?放在 WHERE 条件中数据库会在行锁的粒度上做判断第二个事务要等第一个事务提交后才能更新如果余额不足影响行数为 0代码里通过int rows statement.executeUpdate()判断返回值即可得知是否扣款成功。行级锁是 InnoDB 的特性这也是为什么表引擎必须是 InnoDB 的原因之一。数据一致性是面试官最爱问的点。如果被问到「怎么保证银行转账不丢钱」你要能说清楚三层代码层做余额校验SQL 层用条件更新兜底事务层保证原子性。三层缺一不可。5. 常见问题排查从 ClassNotFoundException 到中文乱码的六条血泪记录这部分是我拆这个项目时实际踩过的坑也是群里同学问得最多的几个问题。每条都按「现象 → 原因 → 解决」写遇到问题直接对号入座。5.1 驱动类找不到ClassNotFoundException现象程序启动或第一次查询时报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。原因项目用的是 MySQL 8 的驱动驱动类路径已经改成com.mysql.cj.jdbc.Driver但代码或配置文件里还在用 MySQL 5.x 的老路径。也有可能是 jar 包没放进 lib 目录或者 IDEA 里没把 lib 加入依赖。解决检查 dbcp.properties 里的 driverClassName确保是com.mysql.cj.jdbc.Driver然后在 IDEA 里 File - Project Structure - Libraries 确认四个 jar 已经添加。忘记把 lib 加进依赖是最常见的低级错误但项目新建时很容易漏掉。5.2 连接时报时区错误Server returns invalid timezone现象java.sql.SQLException: The server time zone value Öйú±ê׼ʱ¼ä is unrecognized or represents more than one time zone。原因MySQL 8 的 JDBC 驱动对时区检测更严格而 MySQL 服务端的时区信息不完整。数据库里存的默认时区字段可能显示为乱码。解决在连接 URL 末尾加serverTimezoneAsia/Shanghai。注意不要写成GMT8因为某些驱动版本解析会出问题直接写时区库里的名字最稳妥。5.3 中文乱码查询结果全是问号现象控制台输出的账户姓名是???或者数据库里存的本来就是乱码。原因连接 URL 没有指定characterEncodingJava 程序和 MySQL 之间使用了不同字符集。也可能是建表时表字符集不是 utf8mb4。解决URL 里加上characterEncodingutf8同时建库语句写CREATE DATABASE bank DEFAULT CHARACTER SET utf8mb4。MySQL8 的 utf8mb4 才是完整支持中文的字符集utf8 在 MySQL 里是 utf8mb3 的别名有些生僻字会丢。5.4 余额计算越算越不对浮点误差现象存款、取款几次之后余额变成999.9999999998这种数字。原因表结构里 balance 字段用的是DOUBLE或FLOAT或者 Java 代码里用double做金额加减。二进制浮点无法精确表示十进制小数累加多次后误差累积。解决表结构改成DECIMAL(18, 2)Java 代码里用BigDecimal。BigDecimal构造时注意用new BigDecimal(10.50)而不是new BigDecimal(10.50)后者仍然会从 double 转过来精度照样丢。如果是从查询结果ResultSet里取值用getBigDecimal()。5.5 连接池耗尽获取连接超时现象运行一段时间后报Cannot get a connection, pool exhausted或者Timeout waiting for idle object。原因程序里获取了连接但没有close()或者close()只执行在正常路径异常路径中连接没归还。连接池的连接总数是有限的借出去不还池子自然就空了。解决Connection的关闭放在finally块里确保无论正常还是异常都能归还。常见写法是try (Connection conn DBUtil.getConnection()) { ... }这种 try-with-resources 语法它等价于自动执行conn.close()。另外检查代码里有没有一次请求内多次调用getConnection()而没有对应的 close用 ThreadLocal 方案时要确认线程结束时remove()了。5.6 转账后一方余额没变现象转账方法执行成功、没有报错但转出账户扣款了转入账户没加钱。原因事务虽然提交了但两条 UPDATE 不在同一个连接上。比如转账方法里每次都调用DBUtil.getConnection()拿到的连接可能是池里的任意一个第一条 UPDATE 在连接 A 上提交第二条 UPDATE 在连接 B 上失败回滚A 上的操作已经生效看起来就是只扣不加。解决用第 4 节提到的 ThreadLocal 方案保证一次请求内拿到同一个连接。或者把两个 UPDATE 放在同一个Connection实例上执行不要在方法内部到处getConnection()。这个问题的隐蔽性在于它不会报错只有核对余额时才能发现属于「逻辑对但数据错」的经典案例。6. 把课程设计改成面试能聊的项目验证事务回滚与代码审查课设有个特点是「能跑就行」但答辩或者面试时对方问一句「你怎么验证事务回滚是生效的」很多人就答不上来。这里给你一个可以直接动手验证的思路。先在转账方法里人为制造一个失败点比如转入账户故意用一个不存在的账号。执行转账后查一下两个账户的余额如果转出账户余额没变说明回滚生效了如果转出账户钱少了说明事务根本没包住。具体做法是在转账方法中加一行int x 1 / 0;模拟运行时异常或者在转入账号不存在时主动抛一个RuntimeException然后确认回滚动作执行了。这个实验做完你对事务的理解会比背十遍概念都深。然后检查一下项目里有没有把close()写在finally里、转账有没有做 SQL 层余额校验、密码是否明文存储。这三个点分别对应资源释放、并发安全、数据安全是面试官最常揪住问的地方。把代码改成符合这三个习惯的版本这个项目就能从「课程设计」升级成「有工程意识的作品」。如果还有余力可以加一张交易流水表转账时记录转出方、转入方、金额、时间这样转账和流水记录就处于同一个事务里。面试时聊到这个切入点就是「事务的原子性不止覆盖状态更新还包括审计日志」这会让你的项目讲述比同水平的人高一个档次。我自己的习惯是每次改完这类项目都强制走一遍「启动 → 开户 → 存款 → 转账 → 取款 → 查流水」全链路再手动在转账时制造一次异常确认回滚无遗漏才算收工。否则等项目答辩前一天才发现事务没生效那种感觉比踩坑本身难受得多。希望帮到你祝答辩顺利。本文还有配套的精品资源点击获取
返回列表