ARTICLE DETAIL

资讯详情

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

Java网上银行转账系统源码深度拆解:事务、并发与部署避坑

Java网上银行转账系统源码深度拆解:事务、并发与部署避坑 简介这是一份基于Java与JavaScript实现的网上银行转账系统设计源码面向Java Web初学者、金融类课程设计以及需要快速搭建金融业务原型的开发者用于学习在线转账中的账户管理、用户认证、事务处理和安全管理等核心环节。资源共46个文件压缩包仅有356KB其中19个Java源文件承载转账业务逻辑与异常处理14个JSP页面用于前端展示和表单交互7个XML配置负责数据源与组件装配2个properties文件保存数据库连接等敏感参数3个txt文档提供说明与日志1个JS脚本增强页面动态效果。目前已有279人学习下载。项目包含完整的Maven工程结构及依赖配置可帮助读者理清银行系统前后端协作方式掌握从数据库配置、防SQL注入到SSL加密通信的安全设计思路并可直接在源码基础上改进为课程设计或毕业设计项目性价比高。1. Java 网上银行转账系统源码45 个文件撑起一条完整转账链路这套基于 Java 的网上银行转账系统设计源码不是只有 main 方法的演示程序而是一份能跑起来的完整 Web 工程。45 个文件里19 个 Java 源文件管业务逻辑14 个 JSP 页面管界面7 个 XML 配置和 2 个属性文件管连接与参数外加 1 个 JavaScript 脚本做交互。从转账表单到扣款入账、事务提交整条链路都有对应代码。它的价值不是能转账而是把经典 Java Web 分层结构摆在面前JSP 和 Servlet 怎么配合、事务怎么包住两次账户更新、SQL 注入怎么防。准备 java 面试题、需要课程设计代码的人都能找到可改可跑的底子。下面从工程结构拆起再走到转账核心逻辑和部署坑点。2. 工程结构拆解从 upload.zip 到可运行的 Maven Web 应用upload.zip 解压后是标准的 Maven 目录src/main/java、src/main/resources、src/main/webapp 三个目录各司其职。先按文件类型过一遍再动手导入 IDE比直接搜代码效率高得多。2.1 45 个文件的三类归属谁管业务、谁管界面、谁管配置解压后先别急着打开 IDE在命令行里把目录结构列出来建立全局地图。按职责划分这 45 个文件能分成三组Java 源文件、Web 页面与脚本、配置文件与文档。目录/文件数量职责src/main/java19 个 .javaController、Service、DAO、实体类转账与认证核心逻辑src/main/webapp14 个 .jsp 1 个 .js登录、账户查询、转账表单、历史记录等页面src/main/resources7 个 .xml 2 个 .properties数据库连接、框架配置、环境参数根目录pom.xml、readme.txtMaven 构建描述、运行说明这样的分布是经典 Java Web 分层的结果Java 源文件里 Controller 收请求、Service 写业务、DAO 访问数据库JSP 只做展示XML 和 properties 把数据库连接串、端口这类环境相关的东西隔离出来第三方接口和日志配置也常以 XML 形式放在 resources 下。我第一次看类似工程时直接开编辑器搜转账关键字结果一半时间花在翻 XML 上。按这张表过一遍五分钟就能把地图建起来。2 个文本文件通常是 readme 之类的说明或日志1 个 JavaScript 脚本放在 webapp 下管前端交互。2.2 pom.xml 依赖选型Servlet JSP 的组合为什么仍有学习价值pom.xml 是 Maven 的项目对象模型文件声明这个工程依赖哪些库、最终打包成什么格式。网上银行教学工程最常见的组合是 Servlet JSP JDBC/MyBatis依赖一般包括 servlet-api、jsp-api、mysql-connector-java整合了 Spring 的话再加 spring-context、spring-jdbc。下面是一份典型 pom.xml 的关键片段可以对照你工程里的 pom.xml 看差异project modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdonline-bank/artifactId version1.0.0/version packagingwar/packaging dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version3.1.0/version scopeprovided/scope /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version4.3.30.RELEASE/version /dependency /dependencies /project逻辑说明packaging 声明为 war最终构建产物是能丢进 Tomcat 的 Web 归档包servlet-api 的 scope 是 provided因为 Tomcat 自带 Servlet 容器打包时不需要重复带进去这个 scope 配错是运行时类冲突的常见来源。mysql-connector-java 是 JDBC 驱动5.1.x 配 MySQL 5.x 最常见换 MySQL 8.x 就要用 8.0.x 的驱动和驱动类名。参数说明groupId、artifactId、version 三个坐标唯一标识项目版本号不是越新越好要和 JDK、Tomcat、MySQL 匹配。IDEA 导入时会按 pom.xml 自动下载依赖网络慢的话在 Maven 的 settings.xml 里配置阿里云镜像配完重新 import 会快很多。这套工程没有用 spring boot mybatis 这种全家桶反而更接近 java 面试题里常考的 Servlet 生命周期、Filter、Listener 那一套这也是它还值得跑一遍的原因。2.3 导入与启动流程三分钟把工程跑起来拿到源码后我一般的操作顺序是先在命令行解压、核对文件数再进 IDE。这一步能提前发现压缩包损坏或者文件缺失的问题unzip upload.zip -d online-bank cd online-bank find . -type f | wc -l tree src/main/java src/main/webapp逻辑说明四步依次是解压、进入目录、统计文件总数、查看源码目录层级。统计文件数是为了和简介里的 45 个文件对号对不上就说明压缩包不完整后面一切操作都白搭。参数说明unzip 的 -d 指定解压目录find 加 wc -l 只统计文件不统计目录tree 列出层级macOS 需 brew install treeWindows 用 dir /s 代替。然后打开 IDEAFile - New - Project from Existing Sources定位到 pom.xmlMaven 会自动识别并下载依赖。这里有个 java 环境变量配置的前置问题IDEA 的 Project SDK 要选 JDK 8 或更高Maven 的 JDK for importer 也不要低于工程要求的版本。配置好后我一般用 Tomcat 方式运行Run - Edit Configurations 里加一个 Tomcat ServerDeployment 选 war explodedApplication context 设为 /online-bank端口 8080。启动后访问 http://localhost:8080/online-bank/ 能看到登录页说明工程已经起来了。如果 java 启动失败先别怀疑业务代码九成是三个原因端口 8080 被占用、Tomcat 版本与 JDK 不匹配、Maven 依赖没下载完。逐个排查比反复重启有效得多。3. 转账核心逻辑认证、事务与并发扣款的落地方式src/main/java 下那 19 个 Java 源文件按职责基本能拆成三层Controller 收请求、Service 写业务、DAO 访问数据库。转账系统最重要的三条业务线是登录认证、转账事务、余额校验。3.1 用户认证与会话管理登录态怎么存、怎么防篡改网上银行的基础认证流程是表单提交用户名密码后台比对通过后把用户信息写进 Session。后续 JSP 通过 Session 里的 user 对象判断登录态没有登录直接访问转账页就重定向回登录页。WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); User user userService.login(username, password); if (user ! null) { req.getSession().setAttribute(loginUser, user); resp.sendRedirect(req.getContextPath() /account.jsp); } else { req.setAttribute(errorMsg, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } }逻辑说明登录成功后把 user 对象塞进 Session后续页面用 session.getAttribute(loginUser) 判断登录态失败时用 forward 而不是 sendRedirect这样 errorMsg 能带到登录页回显。这版代码为了教学清晰密码很可能是明文比对或者 MD5 存储生产环境必须换成 BCrypt 加盐哈希否则数据库泄露等于密码全泄露。参数说明WebServlet(/login) 是 Servlet 3.0 的注解式映射省掉了 web.xml 里配 servlet-mapping 的步骤req.getContextPath() 兼容部署在不同应用名下的情况硬编码路径在换 war 包名时会 404。Session 默认超时 30 分钟转账这类敏感操作我建议在 web.xml 把 session-timeout 调到 15 分钟并配一个 Filter 拦截未登录请求。生产环境还要在前面挂 HTTPSSSL 加密通讯属于容器和网关层的事不在这一层代码里。3.2 转账事务ACID 在 Service 层的具体写法转账的本质是账户 A 扣款、账户 B 入账两步数据库更新。这两步必须在一个事务里要么都成功要么都失败。不用事务的话扣款成功但入账失败用户资金凭空消失这是金融系统最底线的红线。public void transfer(Connection conn, String fromAccount, String toAccount, BigDecimal amount) throws SQLException { conn.setAutoCommit(false); try { int rows updateBalance(conn, fromAccount, amount.negate()); if (rows 0) { throw new BusinessException(余额不足或账户不存在); } updateBalance(conn, toAccount, amount); insertTransferLog(conn, fromAccount, toAccount, amount); conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); } }逻辑说明setAutoCommit(false) 之后所有 SQL 进入同一个事务commit 前随时可 rollbackupdateBalance 返回受影响行数为 0 说明余额不足或账号不存在直接抛异常终止事务insertTransferLog 写流水方便事后对账这是新手最容易忽略的一步finally 里恢复自动提交保证连接还回连接池时不污染下一个调用方。参数说明amount 用 BigDecimal 而不是 double。涉及金额的 java 数据类型必须是 BigDecimaldouble 的二进制浮点表示会导致 0.1 加 0.2 不等于 0.3 的精度问题。amount.negate() 取负扣款本质是给余额加负数。如果工程里用的是 Spring 的 Transactional事务边界由 AOP 切面控制原理和手写一样但注解只对 public 方法生效同类内部 this 调用会走不到切面事务静默失效。注意Transactional 失效是转账系统最隐蔽的坑。自调用、异常被 catch 吞掉、方法不是 public这三种情况都会让事务看起来没生效。3.3 并发扣款余额校验的盲区在哪转账系统最隐蔽的问题不是逻辑写错而是并发。两个请求同时读到余额 1000各自扣 800如果校验发生在读余额之后、更新之前两个请求都能通过校验最终余额变成 -600。常见做法有两种第一种是悲观锁查询账户行时加排他锁SELECT balance FROM account WHERE account_no ? FOR UPDATE逻辑说明FOR UPDATE 会锁住这条记录直到事务提交后续请求必须排队代价是并发吞吐下降但转账这种低频高价值场景完全可接受。参数说明锁的粒度是行级前提是 account_no 有索引没有索引时 InnoDB 会退化成锁整表转账一慢全站跟着慢。第二种做法是乐观锁账户表加 version 字段更新语句带上 version oldVersion受影响行数为 0 说明有并发冲突重试即可。教学工程一般不做这么深但理解这个盲区排查线上问题和准备 java 面试题都能直接用上。4. JSP 与 JavaScript14 个页面和 1 个脚本怎么撑起前端交互处理完后端逻辑再来看 webapp 目录。JSP 负责服务端渲染页面JavaScript 负责浏览器端交互两者边界在哪里、各自该干什么是这个章节要讲清楚的。4.1 14 个 JSP 页面的职责划分从登录到转账回执14 个 JSP 页面按功能可以分成登录注册、账户信息、转账流程、结果展示、历史记录和公共布局几组。下面是一个典型划分和你工程里的页面基本能对上页面组典型页面职责登录/注册login.jsp、register.jsp用户入口表单提交到 LoginServlet账户信息account.jsp、balance.jsp展示余额、账户状态转账流程transfer.jsp、confirm.jsp填写金额、二次确认结果展示result.jsp、error.jsp转账回执、失败提示历史记录history.jsp查询转账流水公共布局header.jsp、footer.jsp页面复用片段JSP 的能力是能在 HTML 里嵌 Java 代码这也是把双刃剑。工程里如果大量用 % % 脚本片段页面会越来越难维护。这份工程的学习价值之一就是让你看到能跑和好维护的差距输出用户名用 % user.getName() % 和用 EL 表达式 ${sessionScope.loginUser.name}后者明显干净。更进一步用 JSTL 的 c:forEach 遍历转账历史比在 JSP 里写 for 循环可读性好得多这也是把 JSP 当成模板引擎而不是编程环境来用。4.2 JavaScript 脚本表单校验能做防重防伪还得靠后端这 1 个 JavaScript 文件通常负责表单校验和页面动态效果。转账场景里最常见的校验是金额是正数、最多两位小数、收款账号不能为空且不能等于自己的账号function validateTransferForm() { var amount document.getElementById(amount).value; var toAccount document.getElementById(toAccount).value; var fromAccount document.getElementById(fromAccount).value; if (!amount || isNaN(amount) || parseFloat(amount) 0) { alert(请输入大于 0 的转账金额); return false; } if (!/^\d(\.\d{1,2})?$/.test(amount)) { alert(金额最多保留两位小数); return false; } if (toAccount fromAccount) { alert(不能给自己转账); return false; } return true; }逻辑说明isNaN 和正则双保险前者过滤非数字后者约束小数位格式toAccount 和 fromAccount 用页面上的两个字段比对拦截自转。返回 false 时表单标签里的 οnsubmitreturn validateTransferForm() 会阻止提交。参数说明/^\d(.\d{1,2})?$/ 里 \d 匹配整数部分.\d{1,2} 匹配 1 到 2 位小数? 表示小数部分可有可无。注意一个安全认知JavaScript 校验只是用户体验优化真正的校验后端必须重做一遍因为用户完全可以绕过页面用 curl 直接 POST 请求。提示前端校验是体验层后端校验是安全层两者不能互相替代。如果 JSP 的转账表单里看到带 csrfToken 的隐藏字段说明作者做了 CSRF 防护。没有的话这就是一个可攻击的点。我一般会补一个登录时生成 token 存 SessionJSP 表单里带上后台 Filter 校验来源请求的 token 是否一致。同时JSP 输出任何用户输入前都要做 HTML 转义防止 XSS。这两个动作加完后跨站伪造转账请求基本被挡住。5. 部署与运行避坑五个把这套系统跑起来时踩过的坑这一章是我跑类似工程时的血泪经验。每一条按现象 → 原因 → 解决排列你对着排障就行不用从头读。5.1 坑一MySQL 驱动类找不到坑二MySQL 8 连接报错坑一驱动类找不到现象Tomcat 启动时报 ClassNotFoundException: com.mysql.jdbc.Driver或者运行到连接数据库时抛出。原因mysql-connector-java 依赖没进最终产物。常见的两种一是 pom.xml 里这个依赖的 scope 被设成了 provided二是 IDEA 的 Artifacts 里没有把 Maven 依赖带进 WEB-INF/lib。解决把 scope 改成默认的 compile重新 Maven package然后用 jar tf 检查 war 包 WEB-INF/lib 下有没有 mysql-connector 的 jar。没有就说明依赖没打包进来属于 IDEA 的 Artifacts 配置问题在 Project Structure - Artifacts 里把 lib 目录加上。坑二MySQL 8 连接报错现象本地用的 MySQL 8.x连接时报 Communications link failure或者 Public Key Retrieval is not allowed。原因pom.xml 里还是 5.1.x 的老驱动驱动类名和连接参数都不匹配。MySQL 8 换了驱动类名并且默认要求 SSL 相关参数。解决mysql-connector-java 换 8.0.x驱动类写成 com.mysql.cj.jdbc.Driver连接串加上 allowPublicKeyRetrievaltrueuseSSLfalseserverTimezoneAsia/Shanghai。改完重启前先确认 jdbc.properties 里的 url、username、password 和本地环境对得上。这类问题看着像玄学本质是依赖和环境的版本没对齐。配置信息应该放在 properties 文件里别硬编码在 Java 代码中否则换环境就要改源码重新打包这是最原始的维护噩梦。5.2 坑三JSP 中文乱码坑四金额精度悄悄丢失坑三中文乱码现象页面上中文全是问号或者存入数据库的中文变成乱码。原因请求编码没有统一。JSP 页面本身是 UTF-8但 request.getParameter 拿到的参数用的还是容器默认编码比如 ISO-8859-1。解决在 Servlet 的 doPost 开头加 req.setCharacterEncoding(UTF-8)并确认 JSP 页面头部的 pageEncoding 和 contentType 都写 UTF-8。更彻底的做法是写一个 CharacterEncodingFilter拦截所有请求统一设置编码这样新增的 Servlet 也不会忘。数据库连接串里也要带 characterEncodingutf8三层全对齐才不会乱。坑四金额精度丢失现象转账金额出现 0.30000000000000004 这样的值或者对账总是差几分钱。原因代码里用 double 或 float 存金额。二进制浮点数无法精确表达十进制小数累计下来差异就出现了。解决全局搜索代码里的 double 和 float涉及金额的全部换成 BigDecimal数据库字段用 DECIMAL(18,2)。这个属于 java 基础里的经典精度问题面试常考工程里也真的会有人踩而且通常在线上对账阶段才暴露返工成本极高。5.3 坑五重复点击导致同一笔转账执行两次现象用户双击转账按钮或者请求超时后手动刷新数据库里出现两笔完全相同的转账记录。原因转账接口没有幂等处理两次请求都正常走了扣款入账流程。解决前端和后端两层堵。前端提交后立刻把按钮置灰JavaScript 里加一个 submitted 标志位后端在转账流水表里给业务单号建唯一索引插入流水时冲突就抛异常回滚。具体做法String orderNo req.getParameter(orderNo); if (transferLogService.exists(orderNo)) { throw new BusinessException(该笔转账已处理请勿重复提交); }逻辑说明orderNo 由前端生成每次提交唯一后端先查再插配合数据库唯一索引双保险。严格来说先查再插在高并发下仍有竞态窗口最终兜底的是唯一索引所以两个都要做。参数说明orderNo 建议用 UUID 去掉横线的形式长度控制在 32 位以内和表字段定义保持一致唯一索引命名规范用 uk_ 前缀方便和普通索引区分。这个思路在 spring boot mybatis 的支付类项目里同样适用属于通用方案。6. 进阶补一个幂等键再手动制造一次回滚验证事务上面的坑五提到了幂等。这一章把它落到具体实现再给你一个验证事务是否真正生效的方法。这两个动作做完教学工程在转账正确性上就和生产系统的底线对齐了。6.1 幂等键实现转账表加唯一索引Service 里先查再插表结构上加唯一索引是最终防线ALTER TABLE transfer_log ADD UNIQUE KEY uk_order_no (order_no);逻辑说明先查再插负责在正常路径下拦截重复请求唯一索引负责在并发竞态下兜底。两个机制配合单机重复提交和并发穿透都能挡住。参数说明order_no 字段长度建议 varchar(32)和生成规则保持一致唯一索引建在流水表上不影响账户表的更新性能。6.2 手动回滚演练在入账后强制抛异常验证余额复原验证事务边界最直接的办法是在入账 update 之后人为抛一个异常然后查两个账户的余额updateBalance(conn, toAccount, amount); throw new RuntimeException(人为制造异常验证回滚);逻辑说明人为异常强制跳过了 commit走 catch 里的 rollback。如果事务边界正确扣款和入账会一起撤销如果余额变了说明事务没包住扣款操作。参数说明验证时用两个测试账户先记录转账前余额快照跑完对比抛异常的方式不限于 RuntimeException直接在 DAO 层抛 SQLException 效果一样。验证完把抛异常那行删掉再跑一遍正常转账顺便验证幂等连续提交两次相同 order_no第二次必须被拦截。从那以后我每次接手转账、支付类的 Java 项目都会强制先确认三件事金额是不是 BigDecimal、事务边界是不是包住了两次余额更新、有没有幂等键。顺序乱一点线上迟早会还回来。希望帮到你。本文还有配套的精品资源点击获取
返回列表