简介:这是一份面向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.version>5.3.30</spring.version> <mybatis.version>3.5.13</mybatis.version> <mybatis.spring.version>2.1.2</mybatis.spring.version> <mysql.version>8.0.33</mysql.version> <druid.version>1.2.20</druid.version> </properties> <dependencies> <!-- Spring 核心:IoC 容器 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <!-- SpringMVC:处理 Web 层请求 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <!-- Spring JDBC:事务管理依赖 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <!-- MyBatis 核心 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <!-- MyBatis 与 Spring 整合包 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>${mybatis.spring.version}</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <!-- Druid 连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</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 和 ContextLoaderListener,applicationContext.xml管 Service、Mapper、事务、数据源,spring-mvc.xml只管 Controller、视图解析器、注解驱动。
<!-- web.xml 核心片段 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <!-- 监听器:启动时加载 Spring 父容器 --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- 子容器配置,只扫描 Controller --> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>逻辑说明:父容器(ContextLoaderListener 加载的)负责 Service 和 DAO,子容器(DispatcherServlet 加载的)负责 Controller。子容器能拿到父容器的 Bean,反过来不行。所以spring-mvc.xml里只扫@Controller,applicationContext.xml里扫@Service和@Repository,各扫各的,别互相串。参数上<url-pattern>/</url-pattern>会拦截所有请求包括静态资源,所以spring-mvc.xml里必须加<mvc:default-servlet-handler/>或<mvc:resources>,否则你的 CSS、JS 全 404。
2.3 数据源与 MyBatis 整合配置
数据源用 Druid,MyBatis 的SqlSessionFactoryBean要注入数据源和 mapper 位置,MapperScannerConfigurer负责把接口变成 Bean。
<!-- applicationContext.xml 数据源与 MyBatis 整合 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/atm_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"/> <property name="username" value="root"/> <property name="password" value="你的密码"/> <property name="initialSize" value="5"/> <property name="maxActive" value="20"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <!-- 别名包,让 XML 里直接写类名 --> <property name="typeAliasesPackage" value="com.atm.entity"/> <!-- mapper 映射文件位置 --> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <!-- 扫描 Mapper 接口,生成代理 Bean --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.atm.mapper"/> </bean> <!-- 事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <!-- 开启注解事务 --> <tx:annotation-driven transaction-manager="transactionManager"/>逻辑说明:typeAliasesPackage让你在 mapper XML 的resultType里直接写Account而不是全限定名。mapperLocations指向 XML 文件,接口和 XML 同名同包是约定,但这里用mapperLocations显式指定更稳。MapperScannerConfigurer会为每个接口生成 JDK 动态代理,你注入AccountMapper时拿到的就是代理对象。事务管理器必须配,否则@Transactional就是个摆设。参数上serverTimezone不写,MySQL 8 连接会报时区错误;useSSL=false在本地开发时省掉证书握手。
提示:Druid 的
maxActive别设太大,本地开发 20 足够,设 100 反而容易把 MySQL 连接数占满。
3. 分层架构与核心业务实现:一笔转账到底经过了哪些类
3.1 实体、Mapper、Service、Controller 四层怎么切
这套 ATM 的表不多,核心就三张:account(卡号、密码、余额、状态)、transaction_record(交易流水)、user(柜员或管理员,可选)。实体类跟表字段一一对应,Mapper 接口定义方法,XML 写 SQL,Service 做业务组合和事务控制,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 id="selectByCardNo" resultType="Account"> SELECT card_no, password, balance, status FROM account WHERE card_no = #{cardNo} </select> <update id="updateBalance"> UPDATE account SET balance = #{balance} WHERE card_no = #{cardNo} </update> <insert id="insertTransaction" parameterType="TransactionRecord"> INSERT INTO transaction_record(card_no, type, amount, balance_after, create_time) VALUES(#{cardNo}, #{type}, #{amount}, #{balanceAfter}, NOW()) </insert>逻辑说明:#{}是预编译占位符,能防 SQL 注入;${}是字符串拼接,除非动态表名否则别用。@Param注解在多个参数时必须加,否则 MyBatis 不知道#{cardNo}对应哪个。resultType="Account"能写简名是因为前面配了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 反序列化成 DTO,@PostMapping限定 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-package="com.atm.controller"/>的包路径是否写对。还有一个隐蔽原因:web.xml里 DispatcherServlet 的url-pattern写成/*而不是/,导致所有请求包括 JSP 都被拦截。
4.2 500:MyBatis 绑定异常 BindingException
现象:启动不报错,一调用就抛Invalid bound statement (not found)。原因:Mapper 接口和 XML 的 namespace 对不上,或者 XML 没被mapperLocations扫到。解决:确认 XML 里<mapper namespace="com.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出来的对象事务不生效;原因三:方法不是public,Spring AOP 默认只代理 public 方法。解决:把@Transactional放在 Service 的 public 方法上,确保 Service 是@Service注解的 Bean,且通过@Autowired注入而不是手动 new。
4.4 静态资源 404:CSS/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-8,forceEncoding设 true。同时检查数据库连接 URL 里有没有characterEncoding=utf8,以及数据库表和字段的字符集是不是utf8mb4。
<!-- web.xml 编码过滤器,必须放在其他过滤器之前 --> <filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>逻辑说明:forceEncoding=true会同时设置请求和响应的编码,只设encoding有时响应还是乱码。参数上url-pattern用/*覆盖所有请求。这个过滤器要放在web.xml最前面,否则其他过滤器可能已经读过参数了。
5. 进阶调试与验证:用日志和断点把一条链路看穿
5.1 打开 MyBatis SQL 日志,看它到底发了什么
配置再对,也不如亲眼看到 SQL。在applicationContext.xml的sqlSessionFactory里加一个configuration属性,或者用mybatis-config.xml开启日志。
<!-- 方式一:在 SqlSessionFactoryBean 里内联配置 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="typeAliasesPackage" value="com.atm.entity"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="configuration"> <bean class="org.apache.ibatis.session.Configuration"> <!-- 打印 SQL 到控制台 --> <property name="logImpl" value="org.apache.ibatis.logging.stdout.StdOutImpl"/> <!-- 开启驼峰映射:card_no -> cardNo --> <property name="mapUnderscoreToCamelCase" value="true"/> </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?cardNo=6222020200112233"逻辑说明:-H指定 JSON 内容类型,-d传请求体。转账后立刻查两边余额,确认扣款和入账金额一致。参数上金额传100.00而不是100,避免前端精度问题。如果返回{"code":500,"msg":"余额不足"},就去数据库看account表的balance字段,确认初始余额够不够。
5.4 一个我常用的验证习惯
从那以后我每次拆这类 SSM 项目,都强制走一遍“先看表结构、再跑单条 SQL、再调 Service、最后走 HTTP”的顺序。表结构决定实体,单条 SQL 验证 Mapper,Service 验证事务,HTTP 验证整条链路。哪一层出问题就停在哪一层,别一上来就调接口,否则 404 和 500 混在一起,排查成本翻倍。这套 ATM 源码的价值不在于业务多复杂,而在于它把 SSM 的每一层都暴露给你,让你能一层一层地验证。希望帮到你。
本文还有配套的精品资源,点击获取