ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java Swing+MyBatis ATM模拟系统:从课程设计到工程实践

Java Swing+MyBatis ATM模拟系统:从课程设计到工程实践

简介:这份资源是面向高校计算机专业学生与Java初学者的课程设计完整文档,围绕ATM柜员机模拟系统展开,帮助读者在缺乏真实银行环境的情况下完成自助业务系统的设计与编码实践。压缩包内共1个docx文件,约1.21MB,内容按需求分析、界面设计、功能实现、项目总结等章节组织,涵盖用户登录、账户查询、存款、取款、改密等模块,并给出取存款核心代码、逻辑校验思路与SQL语句示例,还讨论了数据管理、界面布局、安全机制与异常处理等重难点。文档同时梳理了Java Swing图形界面开发、面向对象封装继承等知识点,附有功能图与参考文献,适合作为课程设计报告模板或期末项目参考。目前已有387人学习,可帮助读者快速理清系统结构、对照实现功能并完善自己的设计文档。

1. 从一份课程设计说起:这个 ATM 模拟系统到底能跑出什么

很多人看到“课程设计”四个字就下意识划走,觉得无非是控制台打印几个菜单、if-else堆一堆。但我把这份 JAVA ATM 柜员机模拟系统源码拉下来跑了一遍,发现它并不是那种交完作业就扔的玩具:Swing 图形界面、CardLayout 多窗口切换、MyBatis 持久化到 MySQL、BigDecimal 处理金额、统一的Result<T>返回封装,这套组合放在学生项目里算是相当完整的工程骨架。它模拟的是真实柜员机的完整业务闭环——插卡登录、查余额、存取款、转账、改密、退出,每一步都有界面、有校验、有落库。适合两类人:一是刚学完 Java 语法、想找一个能跑通 GUI + 数据库全链路的练手项目的新手;二是需要一份结构清晰、方便二次改造的课程设计底稿的开发者。下面我按“它是什么 → 怎么跑起来 → 核心代码怎么写的 → 怎么改”的顺序拆给你看。

2. 技术栈与工程结构:Swing 界面层如何与 MyBatis 数据层对接

2.1 技术选型拆解:为什么是 Swing + MyBatis + MySQL

这个项目的分层意图很明显,虽然代码量不大,但职责切得比较干净。界面层用 Java Swing,因为课程设计场景下它零额外依赖、JDK 自带、拖控件就能出效果,比 JavaFX 省心;数据层用 MyBatis,通过 Mapper 接口 + XML 写 SQL,把“余额加多少、减多少”这种操作直接下沉到 SQL 语句里,避免在 Java 里读出对象再算再写回带来的并发覆盖问题;金额类型统一用BigDecimal,这是金融类项目的基本素养,double算钱迟早出玄学误差。数据库选 MySQL,用户表tb_user存卡号、密码、余额。

从源码里的HandleServiceImpl、MysqlOperateMapper、Result<T>、MainView这些类名能反推出典型的分层:MainView是主窗口容器,负责 CardLayout 卡片切换;各个XxxPanel是功能面板;HandleService/HandleServiceImpl是业务服务层;MysqlOperateMapper是 MyBatis 映射接口;Result<T>是统一返回体,带data和massage(原文拼写如此)两个字段。这种结构对二次开发很友好——加一个新功能,无非是加一个 Panel、加一个 Service 方法、加一条 Mapper SQL。

2.2 目录结构还原与关键文件职责

按源码引用关系,工程大致是这样组织的(包名以实际下载为准,这里给的是职责映射):

src/ ├── main/ │ ├── java/ │ │ ├── view/ │ │ │ ├── MainView.java # 主窗口,持有 CardLayout 和 currentId │ │ │ ├── LoginPanel.java # 登录卡片 │ │ │ ├── MenuPanel.java # 主菜单卡片 │ │ │ ├── ChangeMoneyPanel.java # 存取款共用面板(按 type 区分) │ │ │ ├── QueryPanel.java # 余额查询 │ │ │ ├── TransferPanel.java # 转账 │ │ │ └── ChangePwdPanel.java # 改密 │ │ ├── service/ │ │ │ ├── HandleService.java │ │ │ └── impl/HandleServiceImpl.java │ │ ├── mapper/MysqlOperateMapper.java │ │ ├── entity/User.java │ │ └── util/ │ │ ├── Result.java │ │ ├── StringUtils.java # isStrictlyNumeric 等校验 │ │ └── SqlSessionFactoryUtil.java │ └── resources/ │ ├── mybatis-config.xml │ ├── mapper/MysqlOperateMapper.xml │ └── images/ # 背景图资源

几个关键点值得注意:MainView里维护了一个currentId,登录成功后把当前用户主键写进去,后续所有业务面板都通过mainView.currentId拿用户身份,这是一种“会话状态挂在主窗口上”的简化做法,比到处传参省事,但也要注意退出登录时清空。ChangeMoneyPanel一个类同时承担存款和取款,靠构造参数type区分,type.substring(0,1)取出“存”或“取”字拼提示语,这种复用思路在小项目里很实用。

2.3 环境准备与启动步骤

跑起来需要 JDK 8 及以上、MySQL 5.7/8.0、Maven(或手动导入 jar)。步骤按顺序来:

第一步,建库建表。密码字段建议存明文先跑通,后面再改加密:

CREATE DATABASE atm_db DEFAULT CHARSET utf8mb4; USE atm_db; CREATE TABLE tb_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(32) NOT NULL UNIQUE, -- 卡号,登录用 password VARCHAR(64) NOT NULL, -- 密码 money DECIMAL(12,2) DEFAULT 0.00 -- 余额,用 DECIMAL 别用 double ); INSERT INTO tb_user(card_no, password, money) VALUES ('1001', '123456', 5000.00);

第二步,改mybatis-config.xml里的数据源,把 URL、用户名、密码换成你本机的:

<environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/atm_db?useSSL=false&amp;serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="你的密码"/> </dataSource> </environment>

第三步,确认MysqlOperateMapper.xml已被注册进mappers,然后从MainView的main方法启动。启动后应看到登录界面,输入1001 / 123456进入主菜单。如果背景图加载报空指针,检查images目录是否在 resources 根下、路径是否以/images/开头——源码里用的是getClass().getResource("/images/xxx.gif"),斜杠开头表示从 classpath 根找。

提示:DECIMAL(12,2)和 Java 侧BigDecimal是配套的,别图省事把数据库字段改成double,否则存取几次之后余额会出现4999.999999这种脏数据。

3. 核心功能实现:存取款面板与业务校验的完整链路

3.1 ChangeMoneyPanel:一个面板如何复用存款与取款

先看界面层。ChangeMoneyPanel继承JPanel,构造时接收type(“存入”或“取款”)、contentPane、cardLayout、mainView四个参数,前两个用于返回时切卡片,后两个用于拿当前用户和弹窗父组件。布局用setLayout(null)绝对定位,每个控件手动setLocation/setSize,这在课程设计里很常见,好处是所见即所得,坏处是换分辨率会错位,后面改造章节会说怎么换布局。

public ChangeMoneyPanel(String type, Container contentPane, CardLayout cardLayout, MainView mainView) { setLayout(null); final String substring = type.substring(0, 1); // "存" 或 "取" JLabel label = new JLabel("请输入" + substring + "款金额,ATM 机" + substring + "款必须是整百的"); final JTextField moneyText = new JTextField(); JButton enterButton = new JButton(type); final JButton backButton = new JButton("返回"); enterButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (type.equals("存入")) { HandleServiceImpl handleService = new HandleServiceImpl(); Result<Boolean> r = handleService.addMoney( mainView.currentId, moneyText.getText().trim()); JOptionPane.showMessageDialog(mainView, r.massage, "提示", JOptionPane.INFORMATION_MESSAGE); if (r.data) { backButton.doClick(); } // 成功才返回菜单 } else { HandleServiceImpl handleService = new HandleServiceImpl(); Result<Boolean> r = handleService.minusMoney( mainView.currentId, moneyText.getText().trim()); JOptionPane.showMessageDialog(mainView, r.massage, "提示", JOptionPane.INFORMATION_MESSAGE); if (r.data) { backButton.doClick(); } } } }); // ... 控件定位与 add 省略 }

这段代码的逻辑说明:type决定按钮文案和走哪个 Service 方法;mainView.currentId是登录时写入的用户主键,作为所有资金操作的定位依据;Result<Boolean>的data表示业务是否成功,massage是给用户看的提示语,成功才触发backButton.doClick()回到菜单,失败则停留在当前面板让用户改金额。参数上要注意moneyText.getText().trim()先去掉首尾空格再进 Service,否则" 100 "这种输入会在数字校验里翻车。

3.2 业务层校验:空值、非法字符、整百、余额不足四道关

界面只负责收集输入,真正的防线在HandleServiceImpl。存款和取款的校验顺序有讲究:先判空、再判非法字符、再判是否整百,取款还要多一道余额比较。这个顺序不能乱——如果先Double.parseDouble(money)再判空,空字符串会直接抛NumberFormatException,用户看到的就是一坨堆栈而不是友好提示。

@Override public Result<Boolean> addMoney(Long id, String money) { if (money.equals("")) { return new Result<Boolean>("不能为空", false); } if (!StringUtils.isStrictlyNumeric(money)) { return new Result<Boolean>("含有非法字符", false); } if (Double.parseDouble(money) % 100 != 0) { return new Result<Boolean>("ATM 中只支持百元大钞,零钱请前往柜台领取", false); } SqlSession sqlSession = sqlSessionFactory.openSession(); MysqlOperateMapper userMapper = sqlSession.getMapper(MysqlOperateMapper.class); userMapper.addMoneyById(id, new BigDecimal(money)); sqlSession.commit(); sqlSession.close(); return new Result<Boolean>("存款成功", true); } @Override public Result<Boolean> minusMoney(Long id, String money) { if (!StringUtils.isStrictlyNumeric(money)) { return new Result<Boolean>("含有非法字符", false); } if (money.equals("")) { return new Result<Boolean>("不能为空", false); } if (Double.parseDouble(money) % 100 != 0) { return new Result<Boolean>("ATM 中只支持百元大钞,零钱请前往柜台领取", false); } SqlSession sqlSession = sqlSessionFactory.openSession(); MysqlOperateMapper userMapper = sqlSession.getMapper(MysqlOperateMapper.class); User user = userMapper.selectById(id); if (user.getMoney().compareTo(new BigDecimal(money)) < 0) { return new Result<Boolean>("余额不足", false); } userMapper.minusMoneyById(id, new BigDecimal(money)); sqlSession.commit(); sqlSession.close(); return new Result<Boolean>("取款成功", true); }

参数说明:id是用户主键,money是界面传来的字符串。StringUtils.isStrictlyNumeric是自定义工具方法,判断是否全为数字(可含小数点),比try-catch包parseDouble更干净。BigDecimal.compareTo返回负数表示前者小于后者,这里用来判断余额是否够扣,注意别用equals比较BigDecimal,1.0和1.00用equals会返回 false,这是经典坑。取款这里有个小瑕疵:minusMoney里先判非法字符再判空,顺序和addMoney反了,空字符串会先被isStrictlyNumeric拦下返回“含有非法字符”,提示不够准确,二次开发时统一一下顺序即可。

3.3 Mapper 与 SQL:把加减法交给数据库而不是 Java

资金增减没有走“查出来、加一下、再 update 整个对象”的路子,而是直接在 SQL 里做money = money + #{money},这是这个项目里我最认可的一处设计。原因很实在:如果两个窗口同时操作同一账户,Java 侧读到的旧余额再写回,会互相覆盖;而update ... set money = money - ?由数据库保证原子性,天然避免这类丢失更新。

<update id="addMoneyById"> update tb_user set money = money + #{money} where id = #{id} </update> <update id="minusMoneyById"> update tb_user set money = money - #{money} where id = #{id} </update>

对应的 Mapper 接口方法签名是void addMoneyById(@Param("id") Long id, @Param("money") BigDecimal money),#{money}会以预编译参数形式传入,不存在 SQL 注入问题。这里要注意SqlSession的关闭:源码里每次操作openSession()后手动commit()再close(),没有用 try-with-resources,一旦中间抛异常 session 就泄漏了。改造时建议包一层try (SqlSession s = factory.openSession()) { ... s.commit(); },这是后面避坑章节会展开的点。

4. 避坑与排查:跑这个项目最容易翻车的五个地方

4.1 现象:启动报NullPointerException,定位在背景图那行

原因:getClass().getResource("/images/9602.gif_wh860.gif")返回 null,通常是 Maven 工程里images目录没放在src/main/resources下,或者 IDE 没把 resources 标记为资源根,编译后 classpath 里根本没有这个文件。解决:确认目录位置,mvn clean package后去target/classes/images/下看文件在不在;文件名带下划线和双扩展名这种命名也容易手误,复制时核对一遍。实在不行先把paintComponent里的绘制逻辑注释掉,让界面先出来。

4.2 现象:登录一直提示失败,但数据库里明明有这条记录

原因:多半是card_no和password的查询条件写反,或者密码字段在插入时带了空格。还有一种情况是 MyBatis 的mapUnderscoreToCamelCase没开,card_no映射不到cardNo属性,查出来对象字段全是 null。解决:在mybatis-config.xml的settings里加<setting name="mapUnderscoreToCamelCase" value="true"/>;登录 SQL 用where card_no = #{cardNo} and password = #{password},并在插入测试数据时用TRIM()检查有没有隐藏空格。

4.3 现象:取款成功但余额没变,或者变了两次

原因:SqlSession忘了commit(),JDBC 默认手动提交,不 commit 数据不落库;变两次则可能是按钮被重复绑定监听器,或者backButton.doClick()之后用户又点了一次。解决:检查每个写操作后面是否都有sqlSession.commit();按钮监听在构造里只addActionListener一次,别在show事件里重复加。这类问题没有后悔药,只能靠日志——在 Service 方法入口打一行System.out.println("minusMoney id=" + id + " money=" + money),跑一次看调用了几次。

4.4 现象:输入100.5提示“含有非法字符”,输入abc反而没拦住

原因:isStrictlyNumeric的实现如果只判断Character.isDigit逐字符,小数点会被判非法;如果用了正则但写错边界,字母又漏过去。解决:把校验方法改成明确的正则money.matches("\\d+(\\.\\d{1,2})?"),同时配合整百校验new BigDecimal(money).remainder(new BigDecimal("100")).compareTo(BigDecimal.ZERO) != 0,用BigDecimal做取余比double % 100更稳,避免100.000000001这种浮点误差导致的误判。

4.5 现象:改密后旧密码还能登录,或者新密码直接登录不上

原因:改密 SQL 的where条件只用了id没校验原密码,导致任何登录用户都能改;或者更新时把password和card_no字段搞混。解决:改密必须走“先select校验原密码 → 再update新密码”两步,SQL 写成update tb_user set password = #{newPwd} where id = #{id} and password = #{oldPwd},用影响行数判断是否成功,返回 0 就提示“原密码错误”。这样即使并发也不会改错人。

注意:以上五条里,第 3 条和第 5 条是真正会让数据出问题的,其余多是环境或提示层面的小坑。排查顺序建议永远是“先看控制台异常 → 再看 SQL 日志 → 最后看界面逻辑”,别一上来就怀疑 Swing。

5. 二次开发改造:从课程设计到能拿得出手的工程底稿

这份源码作为底稿,最值得动手改的有三处,改完整个项目的“工程味”会明显不一样。

第一处是密码加密。现在密码明文存库,登录 SQL 直接比对明文,这在任何评审场合都是扣分项。改造思路:引入MessageDigest做 SHA-256 加盐,注册/改密时存salt + hash,登录时用同样盐算一遍比对。不用上 Spring Security,课程设计场景下一个工具类足够:

public static String sha256(String raw, String salt) throws Exception { MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update((raw + salt).getBytes(StandardCharsets.UTF_8)); byte[] digest = md.digest(); StringBuilder sb = new StringBuilder(); for (byte b : digest) sb.append(String.format("%02x", b)); return sb.toString(); }

盐可以每个用户随机生成存一列salt,登录时先按卡号查出盐再算哈希。这样即使数据库被看到,密码也不是一眼可读。

第二处是SqlSession的生命周期管理。把每个 Service 方法里的openSession / commit / close抽成一个模板方法或者用 try-with-resources 包起来,异常时rollback。这一步改完,转账这种“扣 A 加 B”的复合操作才能保证要么全成功要么全回滚——现在的代码里转账如果扣款成功、加款失败,钱就凭空消失了,这是最该补的洞。

第三处是界面布局。绝对定位在 1366×768 上好看,换个高分屏就散架。可以把setLayout(null)换成GridBagLayout或BorderLayout + 嵌套 JPanel,背景图用ImageIcon缩放适配getWidth()/getHeight()。改动量不小,但改完这个项目才真正具备“给别人演示”的底气。

验证方法很直接:改完加密后,用旧数据登录应该失败,重新注册一个用户能正常登录;改完事务后,故意在转账的加款 SQL 里写个错表名,观察 A 的余额是否回滚;改完布局后,把窗口拖到不同尺寸看控件是否跟随。我自己的习惯是每改一处就跑一遍“登录 → 查询 → 存 100 → 取 100 → 转账 → 改密 → 退出”这条主链路,任何一步提示语不对就停下来查,别攒着一起调。从那以后我每次接手这类带数据库的 Swing 项目,都强制先把主链路手工走一遍再动代码,省下的调试时间远比这一圈多。希望这份拆解能帮你把这个 ATM 模拟系统真正跑起来、改下去。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进