简介这是一份面向Java Web初学者与SSM框架学习者的实践型教学案例以模拟ATM自动取款机业务为载体帮助开发者理解Spring、SpringMVC与MyBatis三大框架的整合方式。项目围绕用户管理、存取款与转账交易、登录验证、日志记录及异常处理等模块展开覆盖依赖注入、AOP事务、MVC请求分发与MyBatis持久化等核心知识点适合作为课程设计或框架入门的练手素材。资源包为rar格式共83个文件约15.98MB其中以39个jar依赖库、16个xml配置、6个java源码与6个class文件为主另含5个jsp页面及properties、classpath等工程配置完整保留了Eclipse项目结构与数据库脚本便于直接导入运行和二次修改。目前已有386人学习下载可帮助读者快速理清SSM分层结构与配置流程掌握从控制器到DAO的完整调用链路。1. 从零手搓一套 SSM 模拟 ATM这套源码到底能帮你省下多少调试时间如果你正在学 Spring SpringMVC MyBatis却一直卡在“单个框架会用、三个凑一起就报 404 或 500”的阶段那这套 SSM 模拟 ATM 的源码包值得你花一个晚上拆一遍。它把银行柜员机最核心的几条业务线——开户、登录、查询余额、存款、取款、转账、修改密码、交易流水——全部用 SSM 三层架构串了起来Controller 收请求、Service 管事务、Mapper 落库一条链路走到底。适合谁刚学完三大框架、想找一个“业务闭环完整、表结构不复杂、能跑起来看到钱数变化”的练手项目的人。它不炫技没有微服务、没有缓存中间件就是老老实实把 SSM 的配置和分层写清楚让你把注意力放在“请求怎么从浏览器走到数据库再走回来”这件事上。下面我按自己拆包的顺序把配置、分层、事务、避坑和进阶调试一次讲透。2. 环境搭建与 SSM 三大件配置先把“能跑”这件事钉死2.1 依赖版本怎么选别让版本冲突吃掉你一晚上SSM 项目翻车十有八九死在版本上。Spring 5.x 配 MyBatis 3.5.x 是这套源码的基线JDK 用 8 或 11 都行Tomcat 建议 8.5 或 9.0。我一般会先把pom.xml里的版本属性统一抽出来避免 Spring 各模块版本打架。!-- pom.xml 关键依赖版本统一管理 -- properties spring.version5.3.30/spring.version mybatis.version3.5.13/mybatis.version mybatis.spring.version2.1.2/mybatis.spring.version mysql.version8.0.33/mysql.version druid.version1.2.20/druid.version /properties dependencies !-- Spring 核心IoC 容器 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency !-- SpringMVC处理 Web 层请求 -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency !-- Spring JDBC事务管理依赖 -- dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency !-- MyBatis 核心 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency !-- MyBatis 与 Spring 整合包 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version${mybatis.spring.version}/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version${mysql.version}/version /dependency !-- Druid 连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version${druid.version}/version /dependency /dependencies逻辑说明spring-context提供 IoC 容器spring-webmvc负责 DispatcherServlet 那套请求分发spring-jdbc是后面声明式事务的基础。mybatis-spring这个整合包是关键它提供了SqlSessionFactoryBean和MapperScannerConfigurer没有它你就得手动管理 SqlSession。参数上MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver别再用老版本的com.mysql.jdbc.Driver否则控制台会甩一堆警告。2.2 web.xml 与 spring 配置文件的职责划分很多人把applicationContext.xml和spring-mvc.xml写成一个文件结果事务不生效、静态资源被拦截。这套源码分得很清楚web.xml注册 DispatcherServlet 和 ContextLoaderListenerapplicationContext.xml管 Service、Mapper、事务、数据源spring-mvc.xml只管 Controller、视图解析器、注解驱动。!-- web.xml 核心片段 -- context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext.xml/param-value /context-param !-- 监听器启动时加载 Spring 父容器 -- listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class !-- 子容器配置只扫描 Controller -- init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mapping逻辑说明父容器ContextLoaderListener 加载的负责 Service 和 DAO子容器DispatcherServlet 加载的负责 Controller。子容器能拿到父容器的 Bean反过来不行。所以spring-mvc.xml里只扫ControllerapplicationContext.xml里扫Service和Repository各扫各的别互相串。参数上url-pattern//url-pattern会拦截所有请求包括静态资源所以spring-mvc.xml里必须加mvc:default-servlet-handler/或mvc:resources否则你的 CSS、JS 全 404。2.3 数据源与 MyBatis 整合配置数据源用 DruidMyBatis 的SqlSessionFactoryBean要注入数据源和 mapper 位置MapperScannerConfigurer负责把接口变成 Bean。!-- applicationContext.xml 数据源与 MyBatis 整合 -- bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/atm_db?useSSLfalseamp;serverTimezoneAsia/Shanghaiamp;characterEncodingutf8/ property nameusername valueroot/ property namepassword value你的密码/ property nameinitialSize value5/ property namemaxActive value20/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ !-- 别名包让 XML 里直接写类名 -- property nametypeAliasesPackage valuecom.atm.entity/ !-- mapper 映射文件位置 -- property namemapperLocations valueclasspath:mapper/*.xml/ /bean !-- 扫描 Mapper 接口生成代理 Bean -- bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.atm.mapper/ /bean !-- 事务管理器 -- bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean !-- 开启注解事务 -- tx:annotation-driven transaction-managertransactionManager/逻辑说明typeAliasesPackage让你在 mapper XML 的resultType里直接写Account而不是全限定名。mapperLocations指向 XML 文件接口和 XML 同名同包是约定但这里用mapperLocations显式指定更稳。MapperScannerConfigurer会为每个接口生成 JDK 动态代理你注入AccountMapper时拿到的就是代理对象。事务管理器必须配否则Transactional就是个摆设。参数上serverTimezone不写MySQL 8 连接会报时区错误useSSLfalse在本地开发时省掉证书握手。提示Druid 的maxActive别设太大本地开发 20 足够设 100 反而容易把 MySQL 连接数占满。3. 分层架构与核心业务实现一笔转账到底经过了哪些类3.1 实体、Mapper、Service、Controller 四层怎么切这套 ATM 的表不多核心就三张account卡号、密码、余额、状态、transaction_record交易流水、user柜员或管理员可选。实体类跟表字段一一对应Mapper 接口定义方法XML 写 SQLService 做业务组合和事务控制Controller 接参数返 JSON 或视图。// AccountMapper.java 接口定义 public interface AccountMapper { // 根据卡号查账户 Account selectByCardNo(String cardNo); // 更新余额带乐观锁或直接更新 int updateBalance(Param(cardNo) String cardNo, Param(balance) BigDecimal balance); // 插入交易流水 int insertTransaction(TransactionRecord record); }!-- AccountMapper.xml 对应 SQL -- select idselectByCardNo resultTypeAccount SELECT card_no, password, balance, status FROM account WHERE card_no #{cardNo} /select update idupdateBalance UPDATE account SET balance #{balance} WHERE card_no #{cardNo} /update insert idinsertTransaction parameterTypeTransactionRecord INSERT INTO transaction_record(card_no, type, amount, balance_after, create_time) VALUES(#{cardNo}, #{type}, #{amount}, #{balanceAfter}, NOW()) /insert逻辑说明#{}是预编译占位符能防 SQL 注入${}是字符串拼接除非动态表名否则别用。Param注解在多个参数时必须加否则 MyBatis 不知道#{cardNo}对应哪个。resultTypeAccount能写简名是因为前面配了typeAliasesPackage。参数上BigDecimal用于金额别用double否则 0.1 0.2 这种玄学问题会在对账时让你怀疑人生。3.2 转账业务的事务边界与 Service 写法转账是这套源码里最值得看的一段因为它同时涉及“扣款”和“入账”两个写操作必须在一个事务里。Service public class TransferService { Autowired private AccountMapper accountMapper; Autowired private TransactionRecordMapper recordMapper; // 转账扣款 入账 两条流水必须原子 Transactional(rollbackFor Exception.class) public void transfer(String fromCard, String toCard, BigDecimal amount) { // 1. 查转出账户 Account from accountMapper.selectByCardNo(fromCard); if (from null || from.getBalance().compareTo(amount) 0) { throw new RuntimeException(余额不足或账户不存在); } // 2. 查转入账户 Account to accountMapper.selectByCardNo(toCard); if (to null) { throw new RuntimeException(转入账户不存在); } // 3. 扣款 BigDecimal fromAfter from.getBalance().subtract(amount); accountMapper.updateBalance(fromCard, fromAfter); // 4. 入账 BigDecimal toAfter to.getBalance().add(amount); accountMapper.updateBalance(toCard, toAfter); // 5. 记两条流水 recordMapper.insertTransaction(buildRecord(fromCard, 转出, amount, fromAfter)); recordMapper.insertTransaction(buildRecord(toCard, 转入, amount, toAfter)); } }逻辑说明Transactional(rollbackFor Exception.class)表示任何异常都回滚默认只回滚RuntimeException所以显式写Exception更保险。事务边界在 Service 方法上不在 Controller因为 Controller 不该管事务。参数上compareTo返回负数表示小于别用比较BigDecimal。如果转账过程中任何一步抛异常前面已执行的 update 会全部回滚这就是声明式事务的价值。3.3 Controller 参数接收与统一返回格式Controller 层用RestController或Controller ResponseBody接收前端 JSON返回统一结构。RestController RequestMapping(/api/account) public class AccountController { Autowired private TransferService transferService; PostMapping(/transfer) public Result transfer(RequestBody TransferDTO dto) { try { transferService.transfer(dto.getFromCard(), dto.getToCard(), dto.getAmount()); return Result.success(转账成功); } catch (RuntimeException e) { // 业务异常返回失败信息 return Result.fail(e.getMessage()); } } }逻辑说明RequestBody把 JSON 反序列化成 DTOPostMapping限定 POST。Result是自定义的统一返回类包含 code、msg、data。参数上TransferDTO里金额用BigDecimal前端传字符串或数字都能转。异常这里只捕获RuntimeException因为 Service 里抛的就是它如果捕获Exception会把系统异常也吞掉不利于排查。注意Controller 里不要写try-catch把异常吞了又不打日志否则线上出问题你连堆栈都看不到。我一般会加log.error(转账失败, e)再返回失败。4. 避坑与排查SSM 整合最容易被卡住的五个地方4.1 404请求进了 DispatcherServlet 但找不到映射现象浏览器访问/api/account/transfer返回 404控制台没有报错。原因通常是spring-mvc.xml没开注解驱动或者 Controller 没被扫描到。解决检查mvc:annotation-driven/是否存在context:component-scan base-packagecom.atm.controller/的包路径是否写对。还有一个隐蔽原因web.xml里 DispatcherServlet 的url-pattern写成/*而不是/导致所有请求包括 JSP 都被拦截。4.2 500MyBatis 绑定异常 BindingException现象启动不报错一调用就抛Invalid bound statement (not found)。原因Mapper 接口和 XML 的 namespace 对不上或者 XML 没被mapperLocations扫到。解决确认 XML 里mapper namespacecom.atm.mapper.AccountMapper全限定名和接口一致确认applicationContext.xml里mapperLocations的路径classpath:mapper/*.xml能匹配到文件如果 XML 放在src/main/java下还要在pom.xml的build里配resources把**/*.xml包含进去否则编译后 XML 不会进target/classes。4.3 事务不回滚Transactional 失效的三种姿势现象转账扣了款但没入账数据库数据不一致。原因一Transactional加在 Controller 上但 Controller 没被事务代理扫到原因二Service 类没被 Spring 管理自己new出来的对象事务不生效原因三方法不是publicSpring AOP 默认只代理 public 方法。解决把Transactional放在 Service 的 public 方法上确保 Service 是Service注解的 Bean且通过Autowired注入而不是手动 new。4.4 静态资源 404CSS/JS 被 DispatcherServlet 拦截现象页面能出来但样式全丢F12 看 CSS 请求返回 404。原因url-pattern//url-pattern会接管所有请求包括静态资源。解决在spring-mvc.xml里加mvc:default-servlet-handler/或者用mvc:resources mapping/static/** location/static//显式放行。我一般两个都加前者兜底后者精确控制。4.5 中文乱码POST 请求参数变问号现象前端提交中文备注数据库里存的是???。原因web.xml没配CharacterEncodingFilter或者配了但forceEncoding没开。解决在web.xml里加过滤器encoding设 UTF-8forceEncoding设 true。同时检查数据库连接 URL 里有没有characterEncodingutf8以及数据库表和字段的字符集是不是utf8mb4。!-- web.xml 编码过滤器必须放在其他过滤器之前 -- filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping逻辑说明forceEncodingtrue会同时设置请求和响应的编码只设encoding有时响应还是乱码。参数上url-pattern用/*覆盖所有请求。这个过滤器要放在web.xml最前面否则其他过滤器可能已经读过参数了。5. 进阶调试与验证用日志和断点把一条链路看穿5.1 打开 MyBatis SQL 日志看它到底发了什么配置再对也不如亲眼看到 SQL。在applicationContext.xml的sqlSessionFactory里加一个configuration属性或者用mybatis-config.xml开启日志。!-- 方式一在 SqlSessionFactoryBean 里内联配置 -- bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nametypeAliasesPackage valuecom.atm.entity/ property namemapperLocations valueclasspath:mapper/*.xml/ property nameconfiguration bean classorg.apache.ibatis.session.Configuration !-- 打印 SQL 到控制台 -- property namelogImpl valueorg.apache.ibatis.logging.stdout.StdOutImpl/ !-- 开启驼峰映射card_no - cardNo -- property namemapUnderscoreToCamelCase valuetrue/ /bean /property /bean逻辑说明logImpl设为StdOutImpl后每次执行 SQL 都会在控制台打印语句、参数和返回行数。mapUnderscoreToCamelCase开启后数据库的card_no会自动映射到实体的cardNo省掉一堆AS别名。参数上这两个配置只在开发环境开生产环境建议用 log4j 或 logback 控制级别别让 SQL 日志把磁盘刷满。5.2 用断点验证事务回滚光看日志还不够事务回滚要用断点验证。在transfer方法的入账之后、记流水之前打一个断点手动抛一个异常观察数据库里扣款是否回滚。// 临时在 transfer 方法里加一行验证回滚 if (true) { throw new RuntimeException(测试回滚); }逻辑说明这行代码放在扣款之后、入账之前如果事务生效扣款操作会回滚数据库余额不变。验证完记得删掉。参数上RuntimeException能被默认回滚规则捕获如果你抛的是CheckedException而Transactional没写rollbackFor就不会回滚这是很多人踩过的坑。5.3 用 Postman 或 curl 跑一遍完整业务流最后用接口工具把开户、登录、查询、存款、取款、转账、查流水串一遍确认每条链路都通。# 转账接口测试 curl -X POST http://localhost:8080/api/account/transfer \ -H Content-Type: application/json \ -d {fromCard:6222020200112233,toCard:6222020200112244,amount:100.00} # 查询余额 curl http://localhost:8080/api/account/balance?cardNo6222020200112233逻辑说明-H指定 JSON 内容类型-d传请求体。转账后立刻查两边余额确认扣款和入账金额一致。参数上金额传100.00而不是100避免前端精度问题。如果返回{code:500,msg:余额不足}就去数据库看account表的balance字段确认初始余额够不够。5.4 一个我常用的验证习惯从那以后我每次拆这类 SSM 项目都强制走一遍“先看表结构、再跑单条 SQL、再调 Service、最后走 HTTP”的顺序。表结构决定实体单条 SQL 验证 MapperService 验证事务HTTP 验证整条链路。哪一层出问题就停在哪一层别一上来就调接口否则 404 和 500 混在一起排查成本翻倍。这套 ATM 源码的价值不在于业务多复杂而在于它把 SSM 的每一层都暴露给你让你能一层一层地验证。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?