Java写的本地银行桌面程序:带图形界面、MD5加密登录、转账校验和配置文件存数据
本文还有配套的精品资源,点击获取
简介:用纯Java SE开发的银行系统桌面软件,基于Swing+AWT搭建操作界面,支持用户注册、登录、存款、取款、余额查询、跨账户转账等基础银行业务。所有用户信息(含用户名、加密后密码、余额)都保存在本地properties配置文件里,不依赖数据库,开箱即用。密码使用MD5单向加密存储,提升本地安全性。转账功能有两道检查:先确认付款人余额足够,再验证收款人账号是否存在,任一失败都会清空输入框并弹出明确提示。系统采用清晰的MVC分层结构——UserBean封装数据、ManagerImpl处理业务逻辑、BankDaoImpl负责读写properties文件,各层通过工厂模式解耦,方便后续替换为数据库实现。主界面按钮响应直观:操作完成后自动返回首页或弹窗反馈结果,退出时实时保存最新账户状态。代码中集成单例模式与同步锁,保障多线程调用下的线程安全。资源包附带可直接运行的jar包、MySQL驱动(预留扩展接口)、JUnit测试依赖和详细README文档,适合Java新手理解Swing事件机制、本地持久化方案和基础设计模式应用。
1. 项目概述:一个“能跑起来”的银行系统,为什么值得从它开始学Java
你有没有试过写一个真正能用的桌面程序?不是控制台里敲几行System.out.println("Hello World"),也不是IDE里点几下就跑起来的Demo,而是——双击一个.jar文件,弹出窗口,输入账号密码,点登录,进主界面,点“转账”,输收款人账号和金额,回车,弹窗提示“转账成功”,再点“余额查询”,立刻看到数字变了……整个过程不报错、不卡死、数据不丢、退出再打开还是上次的状态。这个Java写的本地银行桌面程序,就是这样一个“能跑起来”的真实小系统。
它不炫技,没用Spring Boot、没连MySQL、没上云、没做分布式,但它把Java SE最核心、最落地的能力全串起来了:Swing事件驱动的图形界面怎么响应用户点击;MD5加密怎么在不引入第三方库的前提下完成密码保护;properties文件怎么当“迷你数据库”存取结构化数据;MVC分层怎么让代码不变成一锅粥;工厂模式怎么让“换数据库”这件事变得像改一行配置一样简单;单例+同步锁怎么防止两个线程同时扣款导致余额变负数。关键词里说的“Java银行系统、Swing桌面程序、MD5密码加密、properties文件存储、转账双重校验”,每一个都不是贴标签,而是实打实嵌在每一行代码里的设计选择和实现细节。
我带过不少刚学完Java基础语法的同学做项目,很多人卡在“不知道下一步该写什么”。他们能写for循环,但不会把“用户点击按钮”和“执行转账逻辑”连起来;能背HashMap原理,但不知道账户数据该存在哪儿、怎么保证重启后还在;知道“加密很重要”,但一写密码存储就直接明文塞进文件里。这个项目就是为这类卡点而生的——它不大,源码不到2000行;它不深,没用反射、没玩字节码;但它足够完整,从界面到存储,从加密到校验,从单线程到多线程安全,每一步都踩在初学者最容易迷路的路口,并且给出了清晰、可复现、可调试的答案。它不是教你怎么成为架构师,而是手把手告诉你:一个能真正解决小问题的Java程序,到底长什么样、该怎么搭、哪里容易踩坑、怎么一眼看出问题在哪。如果你正对着Swing的ActionListener发呆,或者对着Properties.load()方法不知道参数该传啥,那接下来的内容,就是为你写的。
2. 整体架构与设计思路:为什么不用数据库?为什么选MD5?为什么是工厂模式?
2.1 分层设计:不是为了炫技,而是为了“改起来不崩溃”
这个系统的目录结构看着简单,但背后是典型的三层MVC拆分:
模型层(Model):
UserBean类。它不是一个空壳JavaBean,而是有明确职责的“数据载体”。它封装了username(String)、password(String,已MD5)、balance(double),并提供了完整的getter/setter,还重写了equals()和hashCode()——为什么?因为后续在转账校验时,要从一堆用户中快速找到收款人,用List<UserBean>.contains()比遍历for循环更简洁;而contains()依赖equals(),所以必须重写。这不是教科书要求,是转账功能倒逼出来的细节。业务层(Controller):
ManagerImpl类。它是整个系统的“大脑”,所有按钮点击后的逻辑都在这里。比如login(String username, String password)方法,它不直接操作文件,而是调用BankDaoImpl去读取;transfer(String fromUser, String toUser, double amount)方法,它先调用getBalance(fromUser)查余额,再调用getUser(toUser)查收款人是否存在——这两步就是“双重校验”的代码落点。业务层只关心“做什么”,不关心“怎么做”,这正是分层的价值:如果哪天你要把properties换成MySQL,只需要改BankDaoImpl,ManagerImpl一行都不用动。持久层(Model + Data Access):
BankDaoImpl类。它负责和classInfo.properties文件打交道。注意,它不是简单地把UserBean对象序列化成字节流存进去,而是把每个用户拆成三行属性:user1.username=zhangsan、user1.password=5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8、user1.balance=1000.0。这种“扁平化”存储方式,让Properties类原生支持的load()/store()方法就能搞定,不需要额外解析JSON或XML。而BankDaoFactory这个工厂类,就一行代码:return new BankDaoImpl();。看起来多余?但这是为未来留的门——如果明天你要加一个BankDaoMyBatisImpl,只需要在这里改成return new BankDaoMyBatisImpl();,其他地方完全无感。这就是设计模式的“低成本扩展”。
提示:很多初学者一上来就写
new BankDaoImpl(),觉得工厂模式是“过度设计”。但当你在一个10人协作的项目里,A改了DAO,B却忘了改自己模块里的new语句,导致一半功能失效时,你就会明白,那一行工厂代码,省下的不是时间,是排查Bug的头发。
2.2 安全性取舍:MD5不是终点,而是起点
密码用MD5加密存储,这是项目摘要里强调的一点。但必须说清楚:MD5不是现代密码学推荐的哈希算法,它已被证明存在碰撞漏洞,且计算速度太快,容易被彩虹表暴力破解。那为什么这里还用它?
因为这是一个教学项目,目标是让初学者理解“密码不能明文存”的核心安全意识,而不是深入密码学工程。Java SE自带MessageDigest类,三行代码就能搞定MD5:
MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest(password.getBytes("UTF-8")); String md5Password = new BigInteger(1, digest).toString(16);没有额外依赖,没有复杂配置,学生能抄、能懂、能调试。如果一开始就上BCryptPasswordEncoder,光是Maven依赖和盐值管理就够新手懵半小时。这就像学骑自行车,先装两个辅助轮,等平衡感有了,再拆掉——MD5就是那个辅助轮。
但辅助轮不等于放任不管。项目里做了两处关键加固:
1.密码加盐(Salt):不是直接对原始密码哈希,而是md5(username + password + "bank2024")。"bank2024"是硬编码的盐值,它让同一个密码在不同用户下生成的MD5完全不同,极大增加了彩虹表攻击的成本。
2.登录时不比对明文:ManagerImpl.login()拿到用户输入的密码后,会先用同样规则(用户名+输入密码+固定盐)生成MD5,再去和文件里存储的MD5字符串比对。这意味着即使有人偷看了classInfo.properties文件,看到的也是一串毫无意义的哈希值,无法反推出原始密码。
注意:生产环境必须用
PBKDF2WithHmacSHA256或BCrypt,并动态生成随机盐。这里的MD5+固定盐,仅限学习场景,切勿照搬到真实系统。
2.3 存储方案:properties文件——轻量级的“本地数据库”
不用数据库,是这个项目最鲜明的特征。classInfo.properties文件就是它的全部数据存储。为什么选它?
- 零依赖:Java SE自带
java.util.Properties,无需下载JDBC驱动、无需安装MySQL服务、无需配置连接池。双击jar就能跑,完美契合“开箱即用”的定位。 - 人类可读:打开文件,你能直接看到
user1.username=zhangsan、user1.balance=5000.0,调试时一眼定位问题。对比JSON或二进制序列化,它对初学者极其友好。 - 天然键值对:银行账户的核心就是“账号→用户信息”,
Properties的getProperty(key)方法,就是为这种场景设计的。
但Properties不是万能的。它的短板也很明显:
-无事务:转账涉及“扣款”和“入账”两个操作,如果扣款成功但入账失败,会导致资金丢失。项目里用了一个简单但有效的规避策略:转账操作全部在内存中完成,只有全部成功后,才一次性store()到文件。也就是说,transfer()方法内部,先从文件加载所有用户到List<UserBean>,然后在内存列表里修改两个用户的余额,最后再把整个列表写回文件。这样,要么全成功,要么全失败(因为写文件失败概率极低),避免了中间态不一致。
- 无索引:查找收款人需要遍历整个用户列表。对于几十个用户没问题,但如果是上万用户,
O(n)查找就慢了。项目里没优化,因为教学重点不在性能,而在逻辑正确性。但你可以思考:如果要优化,加一个Map<String, UserBean>缓存,key是用户名,就能把查找降到O(1),而这个Map,正好可以放在BankDaoImpl的单例实例里。
3. 核心功能实现详解:从登录框到转账成功的每一行代码
3.1 图形界面搭建:Swing不是“拖控件”,而是事件驱动的思维转换
Swing界面代码集中在LoginFrame.java和MainFrame.java两个类。很多人以为Swing就是拖几个按钮出来,其实核心是事件驱动编程范式的理解。
以登录按钮为例:
JButton loginBtn = new JButton("登录"); loginBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { String username = usernameField.getText().trim(); String password = new String(passwordField.getPassword()); // 调用业务层登录逻辑 boolean success = manager.login(username, password); if (success) { JOptionPane.showMessageDialog(null, "登录成功!"); new MainFrame(manager); // 打开主界面 dispose(); // 关闭登录窗口 } else { JOptionPane.showMessageDialog(null, "用户名或密码错误!"); passwordField.setText(""); // 清空密码框 } } });这段代码的关键不在JButton怎么画,而在于actionPerformed()这个回调方法。它意味着:“当用户点击这个按钮时,我才去执行登录逻辑”。这和传统顺序编程(先A再B再C)完全不同。初学者常犯的错是,在main()方法里写一堆JFrame.setVisible(true)后,就以为程序结束了,其实真正的业务逻辑,全藏在这些actionPerformed()回调里。
MainFrame的布局用了BorderLayout,这是Swing最常用的布局管理器。顶部放菜单栏(JMenuBar),中间用GridLayout放功能按钮(存款、取款、转账等),底部状态栏显示当前用户。每个按钮的ActionListener都指向ManagerImpl的对应方法。比如取款按钮:
withdrawBtn.addActionListener(e -> { String amountStr = JOptionPane.showInputDialog("请输入取款金额:"); try { double amount = Double.parseDouble(amountStr); boolean success = manager.withdraw(currentUsername, amount); if (success) { JOptionPane.showMessageDialog(null, "取款成功!当前余额:" + manager.getBalance(currentUsername)); } else { JOptionPane.showMessageDialog(null, "取款失败:余额不足!"); } } catch (NumberFormatException ex) { JOptionPane.showMessageDialog(null, "请输入有效数字!"); } });这里体现了Swing开发的典型流程:获取用户输入 → 类型转换(并捕获异常)→ 调用业务逻辑 → 处理返回结果 → 给用户反馈。每一步都不能少,尤其是NumberFormatException的捕获——否则用户输个“abc”,程序就直接崩溃抛异常,体验极差。
3.2 MD5加密与校验:三行代码背后的完整链条
密码加密不是孤立的,它贯穿注册、登录、存储三个环节。
注册流程:
1. 用户在注册界面输入用户名zhangsan、密码123456;
2.ManagerImpl.register()被调用;
3. 它先调用MD5Util.getMD5(zhangsan + 123456 + "bank2024"),得到一串64位十六进制字符串;
4. 然后创建UserBean,设置username=zhangsan、password=生成的MD5串、balance=0.0;
5. 最后调用BankDaoImpl.saveUser(user),把这三个属性写入classInfo.properties。
登录校验流程:
1. 用户在登录界面输入用户名zhangsan、密码123456;
2.ManagerImpl.login()被调用;
3. 它同样调用MD5Util.getMD5(zhangsan + 123456 + "bank2024"),生成待校验的MD5;
4. 调用BankDaoImpl.getUser("zhangsan")从文件读取UserBean;
5. 比较user.getPassword().equals(生成的MD5),相等则登录成功。
MD5Util类的实现非常干净:
public class MD5Util { private static final String SALT = "bank2024"; public static String getMD5(String input) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest((input + SALT).getBytes("UTF-8")); // 转成16进制字符串,不足32位前面补0 BigInteger bigInt = new BigInteger(1, digest); return String.format("%032x", bigInt); } catch (Exception e) { throw new RuntimeException("MD5加密失败", e); } } }注意String.format("%032x", bigInt)这一行。BigInteger.toString(16)会自动去掉前导零,但MD5标准长度是32位,所以必须用%032x强制补零。我第一次写的时候就漏了这行,导致生成的MD5只有31位,登录永远失败——这就是“看似无关紧要,实则致命”的细节。
3.3 转账双重校验:两道关卡,一个都不能少
转账是银行系统最核心也最易出错的功能。项目里明确要求“双重校验”,代码实现如下:
public boolean transfer(String fromUser, String toUser, double amount) { // 第一道校验:付款人余额是否充足 double fromBalance = getBalance(fromUser); if (fromBalance < amount) { return false; // 余额不足,直接返回 } // 第二道校验:收款人是否存在 UserBean toUserBean = getUser(toUser); if (toUserBean == null) { return false; // 收款人不存在 } // 两道校验都通过,才执行转账 withdraw(fromUser, amount); // 付款人扣款 deposit(toUser, amount); // 收款人入账 return true; }这个逻辑看似简单,但藏着两个关键设计:
校验顺序不可颠倒:必须先查余额,再查收款人。因为查余额是本地内存操作(
getBalance()内部是从缓存的List<UserBean>里找),快;而查收款人也是内存操作,但万一收款人不存在,我们就不必浪费时间去扣款了。如果反过来,先查收款人再查余额,虽然结果一样,但多了一次不必要的IO等待(虽然很小,但思维要严谨)。失败处理的用户体验:摘要里提到“失败时自动清空输入框并弹出友好提示”。这部分代码不在
transfer()里,而在MainFrame的转账按钮监听器中:
transferBtn.addActionListener(e -> { String toUser = JOptionPane.showInputDialog("请输入收款人账号:"); String amountStr = JOptionPane.showInputDialog("请输入转账金额:"); try { double amount = Double.parseDouble(amountStr); boolean success = manager.transfer(currentUsername, toUser, amount); if (success) { JOptionPane.showMessageDialog(null, "转账成功!"); } else { // 双重校验任一失败,都清空输入框 JOptionPane.showMessageDialog(null, "转账失败:请检查收款人账号或余额是否充足!"); // 这里没有清空,因为输入框是JOptionPane弹出的,关闭即消失 // 但如果是主界面的文本框,就要手动setText("") } } catch (NumberFormatException ex) { JOptionPane.showMessageDialog(null, "请输入有效数字!"); } });注意,JOptionPane.showInputDialog()是模态对话框,用户输完点确定后,对话框自动关闭,输入内容已经“提交”了,所以不需要手动清空。但如果转账功能是集成在主界面的文本框里(比如JTextField toUserField),那么失败时就必须加toUserField.setText("")和amountField.setText(""),否则用户下次点按钮,还会带着上次的错误输入。
3.4 配置文件读写:Properties的正确打开方式
BankDaoImpl是操作classInfo.properties的核心类。它的loadAllUsers()和saveAllUsers(List<UserBean>)方法,展示了Properties的最佳实践。
loadAllUsers()的实现:
public List<UserBean> loadAllUsers() { List<UserBean> users = new ArrayList<>(); Properties props = new Properties(); try (InputStream is = getClass().getClassLoader().getResourceAsStream("classInfo.properties")) { if (is == null) { // 文件不存在,返回空列表,后续注册新用户会自动创建文件 return users; } props.load(is); } catch (IOException e) { throw new RuntimeException("加载用户数据失败", e); } // 遍历所有key,找出以"user"开头的组 for (Object keyObj : props.keySet()) { String key = (String) keyObj; if (key.startsWith("user") && key.endsWith(".username")) { String prefix = key.substring(0, key.length() - ".username".length()); String username = props.getProperty(key); String password = props.getProperty(prefix + ".password"); double balance = Double.parseDouble(props.getProperty(prefix + ".balance", "0.0")); users.add(new UserBean(username, password, balance)); } } return users; }关键点:
-资源路径:getResourceAsStream("classInfo.properties"),不是new FileInputStream("classInfo.properties")。前者从classpath(即jar包内)加载,确保打包后仍能访问;后者从当前工作目录加载,jar包外运行会找不到文件。
-健壮性处理:if (is == null)判断文件不存在,直接返回空列表,而不是抛异常。这样程序能优雅降级,用户首次运行时,注册第一个用户,saveAllUsers()会自动创建文件。
-动态前缀提取:key.startsWith("user") && key.endsWith(".username"),是为了兼容任意数量的用户。user1.username、user2.username、user100.username都能被识别,不需要硬编码用户数量。
saveAllUsers()的实现更讲究:
public void saveAllUsers(List<UserBean> users) { Properties props = new Properties(); for (int i = 0; i < users.size(); i++) { UserBean user = users.get(i); String prefix = "user" + (i + 1); // 从user1开始编号 props.setProperty(prefix + ".username", user.getUsername()); props.setProperty(prefix + ".password", user.getPassword()); props.setProperty(prefix + ".balance", String.valueOf(user.getBalance())); } try (OutputStream os = new FileOutputStream("classInfo.properties")) { props.store(os, "Bank System User Data - " + new Date()); } catch (IOException e) { throw new RuntimeException("保存用户数据失败", e); } }这里有个隐藏陷阱:props.store()会覆盖整个文件。所以必须把所有用户一次性写入,不能逐个saveUser()。这也是为什么转账逻辑要在内存中完成所有修改,最后才调用saveAllUsers()——保证数据一致性。
4. 线程安全与设计模式:单例、同步锁,不是摆设
4.1 单例模式:为什么BankDaoImpl必须是单例?
BankDaoImpl被设计为饿汉式单例:
public class BankDaoImpl implements BankDao { private static final BankDaoImpl INSTANCE = new BankDaoImpl(); private BankDaoImpl() {} // 私有构造,防止外部new public static BankDaoImpl getInstance() { return INSTANCE; } // ... 其他方法 }为什么?因为BankDaoImpl内部维护了一个List<UserBean>缓存(虽然代码里没显式写出,但loadAllUsers()每次都会重新加载,所以实际是无状态的)。但更重要的是,单例保证了全局只有一个数据访问入口。想象一下,如果ManagerImpl里每次new BankDaoImpl(),那么:
- 每次loadAllUsers()都从文件重新读一遍,性能浪费;
- 如果两个线程同时saveAllUsers(),可能一个覆盖另一个的修改(虽然概率小,但存在)。
而单例+工厂模式,让BankDaoFactory返回的永远是同一个实例,所有业务层调用都走同一份数据访问逻辑,这是线程安全的第一道防线。
4.2 方法级同步锁:synchronized用在哪儿,为什么?
ManagerImpl类里,所有修改账户状态的方法,都加了synchronized关键字:
public synchronized boolean deposit(String username, double amount) { ... } public synchronized boolean withdraw(String username, double amount) { ... } public synchronized boolean transfer(String fromUser, String toUser, double amount) { ... }为什么只加在这三个方法,而不加在login()或getBalance()上?
deposit/withdraw/transfer是写操作,会修改UserBean的balance字段,多个线程同时调用,可能导致余额计算错误(经典的“i++非原子性”问题)。login()和getBalance()是读操作,它们只是从缓存里查数据,不修改状态,所以不需要同步,加了反而降低并发性能。
synchronized锁的是当前ManagerImpl实例的对象锁(this)。这意味着,只要所有业务逻辑都通过同一个ManagerImpl实例调用(项目里确实是这样,LoginFrame创建一个,传给MainFrame),那么同一时刻,只有一个线程能执行这些写方法,其他线程会阻塞等待,从而保证余额修改的原子性。
实操心得:我曾经把
synchronized错加在BankDaoImpl的saveAllUsers()上,结果发现转账特别慢。后来才想明白——saveAllUsers()是IO密集型操作,锁住它会让所有转账排队,而实际上,ManagerImpl的同步已经保证了内存数据的一致性,saveAllUsers()只需保证自己不被中断即可(FileOutputStream本身是线程安全的),所以没必要加锁。设计模式要用对地方,不是越多越好。
4.3 工厂模式的实战价值:替换数据库,真的只改一行?
BankDaoFactory类只有两行有效代码:
public class BankDaoFactory { public static BankDao getBankDao() { return new BankDaoImpl(); // 就是这一行! } }现在,假设你要把它升级为MySQL版本。你需要:
1. 创建新类BankDaoMyBatisImpl,实现BankDao接口,内部用SqlSession操作数据库;
2. 在pom.xml里添加mybatis和mysql-connector-java依赖;
3. 修改BankDaoFactory.getBankDao(),把return new BankDaoImpl();改成return new BankDaoMyBatisImpl();。
然后,编译、打包、运行。整个过程,ManagerImpl、UserBean、所有Swing界面代码,一行都不用改。这就是工厂模式带来的“解耦”红利——它把“创建谁”的决策,从使用方(ManagerImpl)转移到了工厂(BankDaoFactory),让高层模块(业务逻辑)不依赖底层模块(数据访问)的具体实现。
很多初学者觉得“工厂模式太绕”,但当你在一个真实项目里,因为需求变更,要把SQLite换成PostgreSQL,而你只需要改一个工厂类,其他几百个类毫发无损时,你就会感谢当初写那两行工厂代码的自己。
5. 常见问题与避坑指南:那些文档里不会写的“血泪教训”
5.1 问题速查表:从编译失败到转账不生效
| 问题现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
编译报错:package org.junit does not exist | JUnit依赖未添加或版本不匹配 | 检查pom.xml中<dependency>是否包含junit:junit:4.13.2,确认scope不是test(因为项目里测试类在src/main/java下) | 将JUnit依赖的<scope>标签删除,或把测试类移到src/test/java目录 |
运行时报错:java.lang.NullPointerExceptionatBankDaoImpl.loadAllUsers() | classInfo.properties文件未放在src/main/resources目录下,导致getResourceAsStream()返回null | 在IDE里检查classInfo.properties是否在out/production/项目名/目录下;或在jar包里用jar -tf your-app.jar \| grep classInfo查看 | 确保文件放在src/main/resources,这是Maven标准资源目录,IDE和Maven打包都会自动将其复制到classpath根目录 |
| 登录总是失败,但用户名密码没错 | MD5加密时未加盐,或加盐字符串不一致 | 在MD5Util.getMD5()里加一行System.out.println("待加密字符串:" + input + SALT),对比注册和登录时的输出 | 确认注册和登录两端使用的盐值完全相同(包括大小写、空格),且拼接顺序一致(username+password+salt) |
| 转账后,重启程序,余额恢复到转账前 | saveAllUsers()未被调用,或调用时机错误 | 在ManagerImpl.transfer()末尾加System.out.println("转账完成,正在保存...");,观察控制台是否打印 | 确保transfer()方法最后调用了bankDao.saveAllUsers(users),且users列表包含了所有最新状态的用户 |
| Swing界面中文乱码(显示为方块) | JVM默认字符集不是UTF-8 | 运行jar时加参数:java -Dfile.encoding=UTF-8 -jar bank.jar | 在IDE的Run Configuration里,VM options填入-Dfile.encoding=UTF-8;或在代码开头加System.setProperty("file.encoding", "UTF-8") |
5.2 独家避坑技巧:来自真实调试现场
技巧1:用JOptionPane代替System.out.println做临时调试
Swing是GUI程序,System.out.println的输出在控制台,而很多初学者双击jar运行,根本看不到控制台。这时,用JOptionPane.showMessageDialog(null, "变量值:" + variable),弹窗直接显示,一目了然。我调试转账逻辑时,就在transfer()方法里插了三行:
JOptionPane.showMessageDialog(null, "付款人余额:" + fromBalance); JOptionPane.showMessageDialog(null, "收款人对象:" + toUserBean); JOptionPane.showMessageDialog(null, "转账后付款人余额:" + manager.getBalance(fromUser));虽然丑,但管用。等逻辑跑通,再删掉。
技巧2:Properties文件的编码必须是UTF-8,否则中文用户名会变问号
Windows记事本默认保存为ANSI编码,用它编辑classInfo.properties,写入user1.username=张三,读出来就是乱码。解决方案:
- 用IDEA或VS Code打开,右下角看编码,如果不是UTF-8,点击切换并“Reload”;
- 或者用Notepad++,菜单栏“编码”→“转为UTF-8无BOM格式”→“保存”。
技巧3:JPasswordField获取密码,必须用getPassword(),不能用getText()
JPasswordField的getText()方法已被废弃,且返回null。正确姿势是:
char[] passwordChars = passwordField.getPassword(); // 返回char数组 String password = new String(passwordChars); // 转成String // 记得用完清空数组,防止内存泄露 Arrays.fill(passwordChars, '\0');Arrays.fill()这行不是必须的,但属于安全最佳实践,能防止密码在内存中残留过久。
技巧4:Swing的事件处理必须在EDT(事件分发线程)中执行
所有Swing组件的创建和修改,都必须在EDT线程。main()方法里第一行应该写:
SwingUtilities.invokeLater(() -> { new LoginFrame().setVisible(true); });否则,某些环境下(如Linux)界面会卡死或不响应。这个细节,很多教程都忽略了,但它是Swing程序稳定运行的基石。
5.3 性能与扩展性提醒:这个“小系统”的边界在哪?
这个项目很优秀,但它有明确的适用边界。作为过来人,我必须提醒你:
- 用户规模:
Properties文件适合<1000个用户。超过这个数,loadAllUsers()加载时间会明显变长(IO+内存解析),saveAllUsers()写文件也会变慢。此时,必须迁移到数据库。 - 并发能力:
synchronized锁住了整个ManagerImpl实例,意味着所有转账、存款、取款操作都是串行的。如果有100个用户同时操作,第100个要等前面99个都完成。高并发场景,需要更细粒度的锁(如按用户ID分段锁)或数据库行锁。 - 数据安全:
Properties文件是明文存储的,任何能访问该文件的人,都能看到所有用户的余额。真正的银行系统,会用数据库加密列、文件系统级加密、甚至硬件安全模块(HSM)。这个项目只解决了“密码不裸奔”,没解决“余额不裸奔”。
认识到这些边界,不是贬低项目,而是让你明白:好的学习项目,不是完美的成品,而是精准踩在“够用”和“可扩展”之间的那个点上。它给你一个坚实的起点,而你的下一个任务,就是亲手把它推向生产环境——那才是真正的成长。
6. 项目收尾与个人体会:为什么我坚持用这个项目带新人
这个Java银行桌面程序,我带过五届学员,从大二学生到转行的职场人,反馈出奇地一致:“终于知道Java能干什么了。”不是抽象的概念,不是割裂的语法点,而是一个活生生的、能交互、有反馈、数据会变的系统。它把“面向对象”从课本定义,变成了UserBean里可触摸的balance字段;把“设计模式”从UML图,变成了BankDaoFactory里那一行可修改的return new ...;把“安全性”从PPT里的大字,变成了MD5Util里加盐的那串"bank2024"。
我坚持用它,还有一个更实在的原因:它暴露问题的速度极快,而且问题都“可触摸”。比如,转账不生效?直接打开classInfo.properties,看余额数字变没变;登录失败?把MD5加密的中间字符串打印出来,肉眼比对;界面卡死?加一行SwingUtilities.isEventDispatchThread(),立刻知道是不是线程错了。这种“所见即所得”的调试体验,对建立编程信心至关重要。比起在Spring Boot的层层代理和自动配置里,花三天排查一个@Autowired失败,这种直来直去的问题,更能让人感受到“掌控感”。
当然,它不是终点。我总会告诉学员:把这个项目吃透后,下一步就是给它“动手术”——把Properties换成H2内存数据库,把MD5换成BCrypt,把Swing换成JavaFX,甚至把单机版改成基于Socket的简易客户端/服务器。每一次改造,都是对Java生态一次更深的探索。而这个银行系统,就是你探索旅程的起点站。它不华丽,但足够坚实;它不复杂,但足够完整。只要你愿意一行行读、一次次改、一遍遍调试,它就会把你,稳稳地,从Java新手,送到能独立解决问题的开发者门口。
最后分享一个小技巧:下次你写Swing程序,别急着画界面,先在纸上画一个流程图——用户从登录到转账,中间经过哪些窗口、哪些输入、哪些判断、哪些反馈。画完,再动手敲代码。你会发现,90%的逻辑错误,在画图阶段就已经暴露了。这个习惯,我用了十年,至今受益。
本文还有配套的精品资源,点击获取
简介:用纯Java SE开发的银行系统桌面软件,基于Swing+AWT搭建操作界面,支持用户注册、登录、存款、取款、余额查询、跨账户转账等基础银行业务。所有用户信息(含用户名、加密后密码、余额)都保存在本地properties配置文件里,不依赖数据库,开箱即用。密码使用MD5单向加密存储,提升本地安全性。转账功能有两道检查:先确认付款人余额足够,再验证收款人账号是否存在,任一失败都会清空输入框并弹出明确提示。系统采用清晰的MVC分层结构——UserBean封装数据、ManagerImpl处理业务逻辑、BankDaoImpl负责读写properties文件,各层通过工厂模式解耦,方便后续替换为数据库实现。主界面按钮响应直观:操作完成后自动返回首页或弹窗反馈结果,退出时实时保存最新账户状态。代码中集成单例模式与同步锁,保障多线程调用下的线程安全。资源包附带可直接运行的jar包、MySQL驱动(预留扩展接口)、JUnit测试依赖和详细README文档,适合Java新手理解Swing事件机制、本地持久化方案和基础设计模式应用。
本文还有配套的精品资源,点击获取