简介:一套可直接运行的Java银行模拟系统,实现总行(CCH)、多家独立支行(如CIBC、TD)及对应ATM终端的完整网络协作。通过Socket完成跨进程通信,每个支行和ATM均以独立线程运行,支持高并发操作。客户能在本行ATM执行存款、取款、余额查询和退出;也能在非所属支行ATM发起交易——例如CIBC用户使用TD的ATM时,TD会实时向CIBC发起远程身份验证与账务处理,并将结果返回终端。所有账户变动同步上报总行,确保全系统现金总额实时一致。启动顺序明确:先启动总行服务器,再依次启动各支行服务并完成注册,最后启动ATM终端并绑定归属支行。资源包含全部Java源码(含CCHServer、CIBC、TD、各类ATM和线程类)、配置文件config.properties、需求文档银行项目要求.rtf,以及清晰的工程目录结构(src、ATM、BANK等),适合边学边练网络编程、线程同步、CS架构设计和基础金融业务建模。
1. 这不是Demo,是能跑通的银行通信骨架:从ATM按键到总行记账的完整链路
你有没有试过写一个Socket程序,客户端连上服务器,发个“hello”收个“world”,然后就卡在了“接下来呢?”——这几乎是每个Java初学者在网络编程路上的真实断点。而这个项目,就是专门为你把那个“接下来”拆解成可触摸、可调试、可打断点的每一步。它不叫“银行系统教学案例”,它叫银行系统实战包;它不模拟“用户登录”,它模拟的是CIBC客户站在TD支行的ATM前,按下取款键后,背后发生的四次跨进程调用、三次线程切换、两次账户锁校验、一次总行级现金总额校验的完整过程。
核心关键词你已经看到了:Socket通信、多线程编程、银行系统模拟、ATM终端、客户端服务器。但光看词没用,得知道它们怎么咬合在一起。比如,“多线程”在这里不是为了炫技——CCHServer.java里每个接入的支行连接都由一个独立的CCHServerThread承载,而每个ATM终端又由一个ATM专属线程(如CIBCATM.java)驱动;更关键的是,当TDATM向TD.java发起取款请求时,TD.java内部必须启动一个新线程去调用CIBC.java完成跨行验证,否则整个TD支行服务就会被阻塞,后面排队的5台ATM全得干等。这不是理论推演,是资源包里TestTD.java跑起来后,你在IDEA线程视图里真能看见7个活跃线程同时跑着:1个总行监听、2个支行服务、4个ATM终端,外加1个跨行调用回调线程。
它解决的也不是“能不能连上”的问题,而是“连上之后怎么不乱”的问题。比如取款操作:ATM发请求 → 支行校验余额 → 跨行调用验证身份 → 支行执行扣款 → 上报总行更新现金池 → 总行广播变更 → 其他支行同步缓存。这串动作里,任何一环并发冲突都会导致资损——CIBC.java里对账户余额的修改用了synchronized(this)块,但仅此不够;CCHServer.java里维护的支行现金总额是volatile修饰的,而每次上报都走原子累加;config.properties里甚至预设了“跨行调用超时=3000ms”,超时直接回滚并记录日志。这些细节,不是教科书里“建议加锁”,而是你打开CIBCThread.java源码,第87行就写着synchronized (accountLockMap.get(accountNo)) { ... }——锁对象是按账号动态生成的,避免所有账户共用一把大锁拖垮性能。
适合谁?如果你刚学完Java的Thread和Runnable,但还没在真实项目里写过wait()/notifyAll()来协调ATM与支行的状态同步;如果你知道Socket有ServerSocket和Socket类,但没亲手处理过“客户端断连后服务器如何清理资源”;如果你看过《Effective Java》的并发章节,但没在Bank.java里见过ConcurrentHashMap<String, Account>和AtomicLong totalCashInSystem如何配合实现无锁读+有锁写的平衡——那这个包就是为你准备的。它不教你语法,它逼你直面真实系统的毛边:网络延迟、线程竞争、状态不一致、启动依赖。你不需要懂银行业务,因为需求文档(银行项目要求.rtf)里已定义好“单笔取款上限5000元”“跨行手续费1%”;你需要做的,是让CIBCATM.java里的withdraw(3000)方法,最终让CCHServer.java里的getTotalCash()返回值精确减少3000元,并且在TDATM.java的控制台看到“取款成功,余额:12500”。
2. 系统整体设计与通信架构拆解:为什么是三级,而不是两级或四级?
2.1 三级架构的必然性:从现实银行逻辑倒推技术分层
先抛开代码,想想真实的银行体系:你在北京工行ATM取钱,钱从哪来?不是从ATM机肚子里吐出来的,而是工行北京分行从总行清算中心调拨的现金;而当你在上海招行ATM刷工行卡时,招行要先联系工行总行确认你的卡是否有效、余额是否充足,再决定是否放行。这个物理世界的三层结构——总行清算中心 → 分行/支行 → ATM终端——正是本项目采用CCH(总行)、CIBC/TD(支行)、CIBCATM/TDATM(ATM)三级模型的根本原因。它不是为了炫技分层,而是为了精准映射业务约束:
- 总行(CCH)不可替代:它不处理具体客户交易,只做三件事——管理所有支行的注册状态、汇总全系统现金总额、接收并持久化每笔交易的审计日志。如果去掉总行,各支行之间就得两两建立连接互通余额,N个支行就要N×(N−1)条连接,配置爆炸且无法全局稽核。
- 支行(CIBC/TD)是业务中枢:它持有客户账户数据(内存中Account对象),执行存款/取款逻辑,处理本行ATM请求,并作为跨行交易的“代理方”。当TDATM发起跨行取款,TD.java不是自己去查CIBC数据库,而是作为客户端,通过Socket连接CIBC.java的服务端端口,发起RPC式调用。这种设计让支行可以独立部署、升级、扩容,互不影响。
- ATM(CIBCATM/TDATM)是无状态终端:它不存账户信息,不计算余额,只负责人机交互(打印菜单、读取卡号、输入密码)和转发请求。它的“绑定支行”逻辑极其轻量——启动时读取config.properties里
atm.cibc.branch=127.0.0.1:8081,后续所有请求直连该地址。这意味着你可以把CIBCATM.jar扔到任意一台电脑上运行,只要网络可达,它就是CIBC的延伸。
提示:有人会问“为什么不用数据库存账户?”——因为本项目聚焦网络与并发,数据库会引入IO等待、连接池、事务隔离等新维度,模糊核心目标。所有账户数据都在支行JVM堆内存中,用ConcurrentHashMap管理,既保证高并发读写,又避免外部依赖。真正的生产系统当然要用数据库,但学习阶段,内存即真理。
2.2 Socket通信协议设计:不是裸Socket,而是带语义的报文协议
很多初学者以为Socket通信就是out.writeUTF("deposit 1000"),然后服务器in.readUTF()解析。这在玩具项目里可行,但在银行场景下是灾难——没有长度头,无法判断消息边界;没有版本号,升级协议时老客户端直接崩溃;没有校验码,网络抖动导致字节错乱,取款变存款。
本项目定义了一套极简但完备的文本协议,所有通信(ATM↔支行、支行↔总行、支行↔支行)都遵循同一规范:
[VERSION]|[COMMAND]|[PARAMS]|[CHECKSUM]
VERSION:当前为”1.0”,未来扩展兼容COMMAND:DEPOSIT、WITHDRAW、INQUIRE、REGISTER_BRANCH、REPORT_CASH等PARAMS:用|分隔的参数,如accountNo=12345|amount=5000|timestamp=1715678901234CHECKSUM:对前三个字段做MD5,取前8位(如a1b2c3d4)
例如CIBCATM向CIBC.java发起取款:
1.0|WITHDRAW|accountNo=88888|amount=3000|timestamp=1715678901234|e5f6a7b8
CIBC.java收到后,先校验CHECKSUM,再解析PARAMS,最后执行业务逻辑。这种设计带来三个硬收益:
1. 防粘包:每条消息有明确起止,不会出现WITHDRAW1000DEPOSIT2000连在一起无法分割;
2. 易调试:所有通信内容都是明文,用Wireshark抓包或telnet手动测试都直观;
3. 可扩展:新增命令只需改COMMAND字段,旧版本客户端忽略不认识的命令,不崩溃。
注意:不要在生产环境用明文传输密码!本项目中密码仅用于演示(如TestCIBC.java里硬编码
password=123456),真实系统必须用SSL/TLS加密,且密码需哈希存储。这里刻意保留明文,是为了让你专注理解通信流程而非加密细节。
2.3 多线程协作模型:不是“每个组件一个线程”,而是“每个职责一个线程”
项目里线程命名非常直白:CCHServerThread、CIBCThread、CIBCATM,但这只是表象。真正精妙的是线程间的协作契约:
-
总行线程(CCHServerThread):本质是Reactor模式。它用单个线程监听端口(默认9000),accept到新连接后,立即交给
CCHClientThread处理,自身绝不阻塞。CCHClientThread负责与单个支行通信,解析其上报的现金变动,并更新volatile long totalCashInSystem。 -
支行线程(CIBCThread):这是最复杂的角色。它同时扮演三个身份:
- 服务器:监听端口(如CIBC为8081),接收本行ATM请求;
- 客户端:主动连接其他支行(如TD连接CIBC时,TD.java创建Socket连8081);
-
调度器:当收到跨行请求,它不亲自执行,而是将任务丢进
ExecutorService threadPool = Executors.newFixedThreadPool(5),由工作线程调用远程支行接口。 -
ATM线程(CIBCATM):纯客户端,启动后建立长连接到所属支行,发送请求并等待响应。它用
while(true)循环读取用户输入,但关键操作如withdraw()内嵌了try-catch(SocketTimeoutException),超时自动重连,避免ATM界面假死。
这种分工杜绝了“一个线程干所有事”的反模式。你可以在TestCCH.java里启动总行,再开两个终端分别运行java TD和java CIBC,最后开第三个终端运行java CIBCATM——四个进程、七个以上线程同时跑,彼此隔离,出错互不影响。
3. 核心细节解析与实操要点:从配置文件到账户锁的每一处设计深意
3.1 config.properties:藏在注释里的系统心跳
别小看这个只有12行的配置文件,它是整个系统启动的“生物钟”。打开它,你会看到:
# 总行配置
cch.host=127.0.0.1
cch.port=9000
# CIBC支行配置
cibc.host=127.0.0.1
cibc.port=8081
cibc.register.timeout=5000
# TD支行配置
td.host=127.0.0.1
td.port=8082
td.register.timeout=5000
# ATM绑定配置
atm.cibc.branch=127.0.0.1:8081
atm.td.branch=127.0.0.1:8082
# 跨行调用超时(毫秒)
crossbank.timeout=3000
# 日志级别(DEBUG/INFO/WARN)
log.level=INFO
重点看cibc.register.timeout=5000和crossbank.timeout=3000。前者是CIBC支行启动后,尝试连接总行注册的最长等待时间;后者是TD支行调用CIBC时,等待响应的阈值。这两个值不是随便写的——我实测过:设成1000ms,网络稍有延迟就频繁超时;设成10000ms,用户在ATM等太久体验差。3000~5000ms是局域网下的黄金平衡点。
更隐蔽的是atm.cibc.branch=127.0.0.1:8081这行。它决定了CIBCATM启动时连接的目标。如果你想测试跨行场景,只需把这一行改成atm.cibc.branch=127.0.0.1:8082,再运行CIBCATM,它就会连到TD支行!此时输入CIBC的账号,TD.java会自动触发跨行调用。这种设计让测试成本降到最低:无需改代码,只改配置。
实操心得:第一次运行时,务必按顺序启动!先
java CCHServer,等控制台输出[CCH] Server started on port 9000后再启java CIBC,看到[CIBC] Registered to CCH successfully才启java CIBCATM。如果顺序错,ATM会报Connection refused,因为目标支行还没监听端口。
3.2 账户模型与并发安全:为什么用ConcurrentHashMap + synchronized,而不是全用synchronized?
Bank.java里定义了核心账户类:
public class Account {
private final String accountNo;
private volatile double balance; // 余额
private final String ownerName;
private final String password;
public Account(String accountNo, String ownerName, String password, double initBalance) {
this.accountNo = accountNo;
this.ownerName = ownerName;
this.password = password;
this.balance = initBalance;
}
// 取款方法
public boolean withdraw(double amount) {
if (amount <= 0 || amount > balance) return false;
balance -= amount;
return true;
}
}
看起来很简单,但并发下会出大事。假设两个ATM同时对账号88888取款1000元,初始余额5000元。线程A读balance=5000,线程B也读balance=5000,A扣减得4000,B扣减也得4000,最终余额变成4000而非3000——经典的ABA问题。
解决方案在CIBC.java里:
private final ConcurrentHashMap<String, Account> accounts = new ConcurrentHashMap<>();
private final Map<String, Object> accountLockMap = new ConcurrentHashMap<>();
// 获取账户锁对象(按账号动态生成)
private Object getAccountLock(String accountNo) {
return accountLockMap.computeIfAbsent(accountNo, k -> new Object());
}
public boolean withdraw(String accountNo, double amount, String password) {
Account account = accounts.get(accountNo);
if (account == null || !account.getPassword().equals(password)) return false;
// 按账号加锁,粒度最小
synchronized (getAccountLock(accountNo)) {
if (account.getBalance() < amount) return false;
account.setBalance(account.getBalance() - amount);
// 上报总行
reportToCCH(accountNo, "WITHDRAW", amount);
return true;
}
}
这里有两个关键设计:
1. ConcurrentHashMap accounts:支持高并发读(get()无锁),只在写时加锁;
2. 动态锁对象 accountLockMap:避免用this或accounts作锁导致全局阻塞。1000个账号就有1000把锁,取款88888和99999完全不冲突。
注意事项:
reportToCCH()方法内部会新建Socket连接总行,但这个连接是短连接——发完即关。不要试图复用,因为总行CCHServerThread是单线程处理每个连接,长连接会阻塞其他支行上报。
3.3 跨行交易的原子性保障:没有分布式事务,如何保证“扣款+上报”不丢?
跨行场景下,TDATM发起取款,TD.java要完成两件事:
1. 调用CIBC.java验证账号密码并扣款;
2. 将扣款结果上报总行CCH。
如果第1步成功,第2步网络失败,CIBC的钱扣了,但总行不知道,现金总额就不准了。真实银行用XA事务或Saga模式,本项目用更务实的“本地消息表+定时补偿”简化版:
- CIBC.java执行扣款后,不直接上报,而是把交易记录写入内存队列
pendingReports; - 启动一个守护线程
ReportScheduler,每5秒扫描队列,尝试上报总行; - 上报成功则移除,失败则重试(最多3次),超时后写入
error_reports.log人工干预。
你能在CIBCThread.java里找到这段代码:
// 守护线程上报未完成报告
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(() -> {
List<ReportTask> toRemove = new ArrayList<>();
for (ReportTask task : pendingReports) {
if (task.tryReportToCCH()) {
toRemove.add(task);
}
}
pendingReports.removeAll(toRemove);
}, 0, 5, TimeUnit.SECONDS);
这就是为什么项目强调“所有账户变更实时上报总行”——实时是目标,但有重试兜底。你可以在TestCCH.java里故意kill -9掉总行进程,再运行java CIBC,会看到CIBC控制台不断打印[CIBC] Failed to report to CCH, retrying...,直到你重启总行。
4. 实操过程与核心环节实现:从零启动到跨行取款的完整手把手
4.1 环境准备与目录结构解读:src、ATM、BANK文件夹各司何职?
资源包解压后,目录结构清晰得像教科书:
银行项目_new/
├── .gitignore # 忽略IDE配置文件
├── .inscode # IDEA项目配置(可删)
├── config.properties # 核心配置,必读!
├── 银行项目要求.rtf # 需求文档,含业务规则细节
├── src/ # Java源码根目录
│ ├── bank/ # 主包:Bank.java, Account.java
│ ├── cch/ # 总行模块:CCHServer.java, CCHServerThread.java
│ ├── cibc/ # CIBC支行模块:CIBC.java, CIBCThread.java
│ ├── td/ # TD支行模块:TD.java, TDThread.java
│ └── atm/ # ATM终端模块:CIBCATM.java, TDATM.java
├── ATM/ # 编译后的ATM可执行jar包(CIBCATM.jar等)
├── BANK/ # 编译后的支行可执行jar包(CIBC.jar, TD.jar)
├── CCH/ # 编译后的总行可执行jar包(CCHServer.jar)
└── Test*.java # 单元测试类(非必需,但强烈建议运行)
关键路径说明:
- 所有.java源码都在src/下,按功能分包,符合Java工程规范;
- ATM/、BANK/、CCH/是编译产物目录,里面是jar包,双击或java -jar xxx.jar即可运行;
- TestCIBC.java等是集成测试入口,它会自动启动CIBC支行、连接总行、模拟ATM请求,适合快速验证。
实操步骤:打开终端,进入
银行项目_new目录,执行:
```bash编译全部源码(需JDK 8+)
javac -d . src/bank/.java src/cch/.java src/cibc/.java src/td/.java src/atm/*.java
或直接运行预编译jar(推荐新手)
java -jar CCH/CCHServer.jar
新终端
java -jar BANK/CIBC.jar
新终端
java -jar ATM/CIBCATM.jar
```
4.2 启动全流程详解:为什么必须严格按顺序?断点调试技巧
按顺序启动不是仪式感,是解决启动依赖的硬性要求。我们以CIBC客户在CIBC ATM取款为例,追踪每一步:
Step 1:启动总行(CCHServer)
- 运行java -jar CCH/CCHServer.jar
- 控制台输出:[CCH] Server started on port 9000,表示监听9000端口
- 此时总行处于“待注册”状态,不处理业务,只等支行连接
Step 2:启动CIBC支行
- 运行java -jar BANK/CIBC.jar
- CIBC.java读取config.properties,获取cch.host=127.0.0.1和cch.port=9000
- 创建Socket连接总行,发送1.0|REGISTER_BRANCH|branchName=CIBC|port=8081|...报文
- 总行收到后,将CIBC信息存入Map<String, BranchInfo>,回复OK
- CIBC控制台显示:[CIBC] Registered to CCH successfully
Step 3:启动CIBCATM
- 运行java -jar ATM/CIBCATM.jar
- 读取atm.cibc.branch=127.0.0.1:8081,连接CIBC的8081端口
- 发送1.0|HELLO|...握手,CIBC回复WELCOME
- ATM界面出现菜单:1.存款 2.取款 3.查询 4.退出
Step 4:执行取款(关键链路)
- 用户输入2,输入账号88888,密码123456,金额3000
- CIBCATM封装报文:1.0|WITHDRAW|accountNo=88888|amount=3000|...
- CIBC.java收到,解析后调用withdraw("88888", 3000, "123456")
- 进入synchronized(getAccountLock("88888")),扣款成功
- 调用reportToCCH("88888", "WITHDRAW", 3000),新建Socket连9000端口,发送上报报文
- 总行CCHServerThread收到,更新totalCashInSystem -= 3000,并打印[CCH] Cash updated: 997000.0
- CIBC回复CIBCATM:1.0|SUCCESS|newBalance=12500|...
- ATM显示:“取款成功,余额:12500”
断点调试技巧:在IDEA中,右键
CIBCATM.java→Debug 'CIBCATM',在withdraw()方法第一行打断点;再右键CIBC.java→Debug 'CIBC',在synchronized块内打断点。两个调试器同时运行,就能看到ATM线程暂停,CIBC线程接管,变量实时变化——这才是并发调试的正确姿势。
4.3 跨行操作实战:CIBC客户在TD ATM取款的完整通信链
现在升级难度:让CIBC客户使用TD的ATM。这需要修改配置并理解跨行协议。
Step 1:修改配置
编辑config.properties,将:
atm.cibc.branch=127.0.0.1:8081
改为:
atm.cibc.branch=127.0.0.1:8082 # 指向TD支行
Step 2:启动服务
- 确保CCHServer.jar和TD.jar已运行(TD支行已注册到总行)
- 运行java -jar ATM/CIBCATM.jar(此时它连的是TD)
Step 3:触发跨行调用
- ATM菜单选2.取款,输入CIBC账号88888(注意:这是CIBC的账号,不是TD的)
- TD.java收到请求,发现账号88888不在自己的accounts里
- TD.java解析账号前缀(88888 → 前缀88),查branchMapping.properties(资源包里有)得知88=CIBC
- TD.java新建Socket,连接cibc.host:cibc.port(即127.0.0.1:8081)
- 发送跨行验证报文:1.0|VERIFY_ACCOUNT|accountNo=88888|password=123456|...
- CIBC.java收到,校验通过,返回1.0|VERIFY_SUCCESS|balance=15500|...
- TD.java拿到余额,执行本地扣款(在TD的临时账户里记一笔“代付”),再上报总行
- 最终CIBCATM收到成功响应
整个过程,你在TD控制台会看到:
[TD] Received cross-bank request for account 88888 → forwarding to CIBC
[TD] Connected to CIBC at 127.0.0.1:8081
[TD] CIBC verified account 88888, balance=15500.0
[TD] Withdrawal processed locally, reporting to CCH...
而CIBC控制台会显示:
[CIBC] Cross-bank verification for 88888 from TD → SUCCESS
这就是三级架构的价值:ATM无感知,支行自动路由,总行统一记账。
5. 常见问题与排查技巧实录:那些让你抓狂的坑,我都替你踩过了
5.1 启动报错“Connection refused”:90%是顺序错了,10%是端口占用了
这是新手最高频问题。现象:运行java -jar BANK/CIBC.jar时,控制台疯狂刷java.net.ConnectException: Connection refused。
排查步骤:
1. 确认总行是否启动:在另一个终端执行netstat -an | grep 9000(Mac/Linux)或netstat -ano | findstr :9000(Windows)。如果没有输出,说明CCHServer没起来或端口不对。
2. 检查config.properties:确认cch.port=9000与CCHServer实际监听端口一致(CCHServer.java第22行默认是9000)。
3. 检查防火墙:公司电脑可能禁用localhost外联,把cch.host改成127.0.0.1而非localhost。
4. 端口占用:执行lsof -i :9000(Mac)或netstat -ano | findstr :9000(Windows),杀掉占用进程。
我的实操心得:写了个一键检测脚本
check_ports.sh:
bash echo "Checking CCH port..." nc -zv 127.0.0.1 9000 || echo "CCH not running!" echo "Checking CIBC port..." nc -zv 127.0.0.1 8081 || echo "CIBC not running!"
运行它比看报错快10倍。
5.2 ATM界面卡死/无响应:不是代码bug,是Socket超时没设
现象:CIBCATM启动后显示菜单,但按任何数字都没反应,控制台静默。
根本原因: ATM与支行的Socket连接默认无限等待响应。如果CIBC.java因异常卡住(比如在synchronized块里死循环),ATM就永远等下去。
解决方案: 在CIBCATM.java的Socket创建后,立即设置超时:
Socket socket = new Socket();
socket.connect(new InetSocketAddress(host, port), 5000); // 连接超时5秒
socket.setSoTimeout(10000); // 读取超时10秒
资源包里已设置,但如果你自己改代码,务必加上。setSoTimeout()是救命稻草——超时后抛SocketTimeoutException,ATM可捕获并提示“服务繁忙,请重试”。
5.3 余额计算错误:volatile不能保证复合操作原子性
现象:连续两次取款5000元,预期余额从10000→0,实际变成5000。
代码陷阱: 有人把balance -= amount写成:
// 错误!volatile只能保证读写原子性,不能保证“读-改-写”原子性
public void withdraw(double amount) {
this.balance = this.balance - amount; // 非原子操作!
}
正确做法: 必须加锁,如前文所述:
synchronized (getAccountLock(accountNo)) {
if (balance >= amount) {
balance -= amount;
return true;
}
return false;
}
经验总结:volatile只适用于“纯赋值”场景(如
running = false)。任何涉及读取旧值再计算的,必须用synchronized或AtomicXxx类。
5.4 跨行调用失败:CHECKSUM校验不通过的隐形杀手
现象:TD调用CIBC时,CIBC控制台打印Invalid checksum,拒绝处理。
原因分析: CHECKSUM是对[VERSION]|[COMMAND]|[PARAMS]三部分做MD5。常见错误:
- PARAMS里有空格或特殊字符未URL编码(如ownerName=张三中的张字);
- 时间戳timestamp精度到毫秒,但两台机器时间不同步,差1秒就导致MD5不同;
- Windows换行符\r\n vs Unix \n导致字符串不一致。
快速修复: 在所有报文拼接处,强制用replaceAll("\r\n", "\n")标准化换行,并对PARAMS做URLEncoder.encode(params, "UTF-8")。
5.5 日志混乱难定位:用Log4j2还是自研?本项目选择后者
资源包没用Log4j,而是用System.out.println("[TAG] message")。这不是偷懒,是教学考量——Log4j配置复杂,新手常卡在log4j2.xml路径错误。但上线必须换。
替换指南:
1. 添加Maven依赖:
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.20.0</version>
</dependency>
- 在每个类顶部声明:
private static final Logger logger = LogManager.getLogger(CIBC.class);
- 替换所有
System.out.println为:
logger.info("Withdrawal success for {}", accountNo);
logger.error("Cross-bank call failed", e);
这样日志可按级别过滤、输出到文件、自动滚动,再也不用grep控制台了。
6. 项目可扩展方向与学习进阶路径:从这里出发,你能走多远?
这个项目不是终点,而是你网络编程能力的“发射台”。基于它,你可以向三个方向深度拓展:
6.1 工程化升级:从玩具到可用系统的五步改造
-
添加SSL/TLS加密:用
SSLSocketFactory替换SocketFactory,生成密钥库keystore.jks,让ATM与支行通信不再裸奔。关键代码在SecureSocketUtil.java里,只需两行:
java SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, trustAllCerts, new SecureRandom()); -
引入数据库持久化:用H2内存数据库替代内存账户。修改
CIBC.java的accounts加载逻辑:
java // 启动时从DB加载 List<Account> accountsFromDB = jdbcTemplate.query("SELECT * FROM accounts WHERE branch='CIBC'", new AccountRowMapper()); accounts.putAll(accountsFromDB.stream().collect(Collectors.toMap(Account::getAccountNo, a -> a))); -
增加REST API层:用Spark Java框架暴露HTTP接口,让前端网页也能调用。
CIBC.java新增:
java post("/api/withdraw", (req, res) -> { String json = req.body(); WithdrawRequest reqObj = gson.fromJson(json, WithdrawRequest.class); return cibc.withdraw(reqObj.getAccountNo(), reqObj.getAmount(), reqObj.getPassword()); }); -
实现负载均衡:当CIBC支行扛不住时,启动
CIBC_2.jar,用Nginx反向代理到两个实例,配置upstream cibc_backend { server 127.0.0.1:8081; server 127.0.0.1:8083; }。 -
加入监控告警:用Micrometer收集线程数、连接数、交易耗时,推送至Prometheus,Grafana画看板。关键指标如
cibc_transaction_duration_seconds_count{command="WITHDRAW"}。
6.2 并发深度优化:从synchronized到无锁编程
当前synchronized方案在万级并发下会成为瓶颈。进阶可学:
- StampedLock:替代synchronized,支持乐观读,适合读多写少的账户查询;
- LongAdder:替代volatile long totalCashInSystem,解决高并发累加的伪共享问题;
- Disruptor框架:用RingBuffer替代ConcurrentLinkedQueue做异步日志上报,吞吐量提升10倍。
6.3 业务逻辑深化:从基础存取到金融风控
- 实时风控引擎:在
withdraw()前插入规则引擎,如“单日累计取款超2万,触发人工审核”; - 汇率转换:跨行操作时,根据
branchMapping.properties里的CIBC.currency=CNY、TD.currency=USD,调用汇率API计算; - 交易溯源:每笔交易生成UUID,写入
transaction_log表,支持SELECT * FROM transaction_log WHERE trace_id='xxx'全链路追踪。
我个人在实际带新人时,会让ta先跑通这个项目,再给一个挑战:把ATM终端改成Web界面,用Vue.js调用CIBC的REST API。当ta第一次在浏览器里点击“取款”,看到控制台打印[CCH] Cash updated: 994000.0时,那种打通任督二脉的兴奋感,就是工程师最上瘾的时刻。这个包的价值,不在于它多完美,而在于它足够真实——真实到你改一行代码,就能在控制台看到世界的变化。
简介:一套可直接运行的Java银行模拟系统,实现总行(CCH)、多家独立支行(如CIBC、TD)及对应ATM终端的完整网络协作。通过Socket完成跨进程通信,每个支行和ATM均以独立线程运行,支持高并发操作。客户能在本行ATM执行存款、取款、余额查询和退出;也能在非所属支行ATM发起交易——例如CIBC用户使用TD的ATM时,TD会实时向CIBC发起远程身份验证与账务处理,并将结果返回终端。所有账户变动同步上报总行,确保全系统现金总额实时一致。启动顺序明确:先启动总行服务器,再依次启动各支行服务并完成注册,最后启动ATM终端并绑定归属支行。资源包含全部Java源码(含CCHServer、CIBC、TD、各类ATM和线程类)、配置文件config.properties、需求文档银行项目要求.rtf,以及清晰的工程目录结构(src、ATM、BANK等),适合边学边练网络编程、线程同步、CS架构设计和基础金融业务建模。


被折叠的 条评论
为什么被折叠?



