☰
完整SSM项目实战:多模块搭建、MyBatis与Spring MVC高频避坑
2026/10/17 11:08:41 网站建设 项目流程

简介:这套资源基于Spring、Spring MVC与MyBatis三大框架整合而成,是一套可直接运行的完整Java Web项目,很适合初学SSM的开发者理解框架整合方式,也可作为课程设计或二次开发的基础工程。项目源码中包含完整的目录结构,controller、service、dao分层清晰,配置方面涵盖Spring的bean配置、Spring MVC的请求映射与视图解析、MyBatis的SQL映射以及数据库连接属性文件;运行调试时能直观看到依赖注入、面向切面编程、MVC模式和持久层操作等机制如何协作。压缩包共226个文件,约56MB,其中包含32个Java源码、32个Class文件、26个Jar依赖库、20个JSP页面、20个XML配置,另有JS、CSS、图片等前端静态资源,还带有少量APK文件,整体结构完整。已有2386人学习下载。通过阅读和修改源码,可掌握常见配置文件的标准写法,理清请求从Controller到Service再到DAO的完整调用流程,并在此基础上继续扩展模块,逐步积累企业级Java Web项目开发经验。

1. 为什么一个完整的SSM项目值得你下载:一个能直接启动的业务闭环

很多刚入行 Java 开发的同学,手里堆了十几个零散的 SSM 练习代码,有的只有 Service 层,有的把 SQL 写死在控制器里,真正能从git clone到mvn tomcat7:run一键起服务、还能处理登录鉴权和分页查询的完整项目,反而不多。这份资源之所以值得下载,是因为它不是一个 hello world,而是一个带用户权限、商品分类、订单流转的模拟项目X,也就是某公司内部 OA 系统的简化版。它把 Spring、Spring MVC、MyBatis 三者的分工落到了每一个请求里:MyBatis 管数据库读写,Spring 管 Service 层事务,Spring MVC 管 HTTP 出入参。如果你正在为毕业设计或入职笔试准备项目经验,或者你想快速搞清楚 SSM 项目在真实部署时怎么拆分模块、怎么统一异常处理,这份代码比看十篇博客都直接。

2. 搭建骨架:Maven多模块与数据库初始化

2.1 Maven模块划分与POM配置要点

拿到资源后,第一件事不是看代码,是看顶层pom.xml的模块声明。这个项目不是单模块结构,而是拆成了common、dao、service、web四个子模块。这样拆的好处是:后续微服务化时,dao和service可以直接被抽取为独立 JAR 包,不需要动页面代码。在父 POM 里统一管理依赖版本,我用惯了这种做法,子模块不再重复填版本号,直接继承:

<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.20</version> </dependency> <!-- 其他依赖按相同模式管理 --> </dependencies> </dependencyManagement>

dependencyManagement段只是声明依赖版本,不直接引入依赖,子模块需要显式声明dependency才会加载。我建议你维护这个段时,把 SSM 三件套、Jackson、MySQL 驱动和 Druid 连接池这五类放一起,方便后面排查版本冲突。

2.2 数据库脚本设计:权限与业务表的拆分

项目里sql文件夹下有三个脚本:init.sql、data.sql、permission.sql。init.sql只建表结构,data.sql写测试数据,permission.sql单独管理用户、角色、菜单三张关系表。这里有个细节值得模仿:业务表的主键用BIGINT UNSIGNED自增,而三张权限表的关联字段用INT,刻意区分。因为权限表的数据量永远小于业务表,用更短的整型做关联,可以减少索引空间。

CREATE TABLE `user` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录名', `password` varchar(128) NOT NULL COMMENT 'BCrypt密文', `status` tinyint(4) DEFAULT '1' COMMENT '1-启用,0-禁用', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';

status字段设计成tinyint,是因为后面做登录拦截时只需要查询一次状态,不需要 join 大表。所有脚本都用了utf8mb4,这是给中文字段预留四字节的 emoji 空间,实际业务里,用户昵称都可能带特殊符号,老项目用utf8mb3遇到 emoji 会直接报Incorrect string value。

3. 打通Spring与MyBatis:数据源、事务与Mapper层的细节

3.1 Spring配置文件中的关键参数说明

这个项目的applicationContext.xml用的是 JavaConfig 加 XML 混合方式,因为有些老配置在 XML 里维护更直观。核心数据源配的是 Druid,不是 DBCP。为什么不用 DBCP?Druid 的监控页能直接看到慢查询和活跃连接数,排查线上问题时比 DBCP 的黑匣子强太多。数据源部分参数如下:

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/ssm_demo?useUnicode=true&amp;characterEncoding=utf8mb4&amp;serverTimezone=Asia/Shanghai" /> <property name="initialSize" value="5" /> <property name="maxActive" value="20" /> <property name="maxWait" value="60000" /> <property name="validationQuery" value="SELECT 1" /> </bean>

validationQuery一定不要漏,否则连接闲置八小时后,MySQL 主动断开,Druid 还持有旧连接,第一次请求会报Communications link failure。maxWait设为 60000 毫秒,是给连接池排队请求一个保底时间,避免线程无限挂起。

3.2 通用Mapper与手写SQL:两种风格的取舍

代码里dao模块同时存在两种 Mapper 风格。BaseMapper.java是通用 Mapper,通过继承tk.mybatis.mapper.common.Mapper<T>直接获得insert、selectByPrimaryKey等单表 CRUD,适合用户表这种字段稳定、操作简单的场景。而OrderMapper.java手写了selectOrderWithItems这种多表联查 SQL,因为订单和订单明细的聚合逻辑在通用 Mapper 里写不出来,必须用@Select注解或 XML:

@Select({ "<script>", "SELECT o.id, o.order_no, SUM(oi.quantity * oi.price) AS total_amount", "FROM `order` o LEFT JOIN order_item oi ON o.id = oi.order_id", "WHERE o.user_id = #{userId}", "<if test='status != null'> AND o.status = #{status}</if>", "GROUP BY o.id, o.order_no", "</script>" }) List<OrderVO> selectUserOrders(@Param("userId") Long userId, @Param("status") Integer status);

注意<script>标签里的#{userId}是预编译占位符,MyBatis 会生成?绑定参数,这样能防 SQL 注入。如果换成${userId},就是字符串拼接,等于把 SQL 注入的坑留给自己踩。后面章节专门说这个坑。

4. Spring MVC到前端:接口设计、拦截器与页面渲染

4.1 控制器层的返回规范与异常处理

控制器层没有像很多博客那样把业务逻辑堆在 Controller 里,而是每层分明。UserController.java里有个很关键的习惯:所有接口返回ResultVO对象,这个对象从common模块引用,包含code、message、data三个字段。这么做的好处是前后端分离时,接口不用改协议,直接返回 JSON。

@RestController @RequestMapping("/api/user") public class UserController { @Resource private UserService userService; @PostMapping("/login") public ResultVO login(@RequestBody LoginDTO dto) { String token = userService.login(dto.getUsername(), dto.getPassword()); return ResultVO.success(token); } }

@RestController是@Controller加@ResponseBody的合成注解,省得每个方法都写@ResponseBody。@RequestBody接收 JSON 请求体,如果前端传的是表单application/x-www-form-urlencoded,用这个注解就会报HttpMediaTypeNotSupportedException,原因就是你注解和请求头不匹配,后面避坑章节细说。

4.2 Thymeleaf模板与静态资源的版本管理

页面部分没有用 JSP,而是用了 Thymeleaf。理由很简单:JSP 在 Spring Boot 里的支持越来越边缘化,Thymeleaf 的语法和 HTML 天然兼容,前端拿到模板直接改样式,也不会报 JSP 那种Unable to find tag library descriptor的错。

项目里web模块的application.properties配置了模板前缀和后缀:

spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html spring.thymeleaf.cache=true spring.mvc.static-path-pattern=/static/**

spring.mvc.static-path-pattern=/static/**这行是配置静态资源的访问路径,默认是/**。为什么这里要缩窄到/static/下?因为你把 JS、CSS 都丢在static目录之后,可以单独配置 CDN 或 Nginx 对/static路径做缓存,不会和动态请求混在一起。cache=true在生产环境开着,本地调试时建议临时改成false,不然改了模板刷新不生效,容易误以为代码没改对。

5. 避坑手册:SSM项目中遇到的高频故障与排查记录

5.1 故障一:Spring容器启动时Bean创建嵌套异常

现象:

项目启动直接抛BeanCreationException,根因是Error creating bean with name 'userService': Injection of autowired dependencies failed。看起来像是 UserService 的问题,但把@Autowired的代码删掉后,异常变成了 DataSource 初始化失败,一会儿报驱动类找不到,一会儿又报数据库 URL 前缀不对。

原因:

Maven 多模块依赖没拉完整。dao模块编译时能通过,但web模块运行时缺mysql-connector.jar。还有一例是 Druid 的driverClassName配错,MySQL 8.x 之后的驱动类名是com.mysql.cj.jdbc.Driver,老项目里写成com.mysql.jdbc.Driver,5.x 和 8.x 两个版本都存在,但 8.x 的驱动类会打警告日志,某些低版本连接池直接罢工。

解决:

第一步不是看代码,是跑mvn dependency:tree,检查web模块的依赖路径里有没有mysql-connector-java。第二步,确认 URL 里带不带serverTimezone,不带的话,MySQL 8.x 默认要求指定时区,报错信息里会提示The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized。这两种问题我都遇到过,前者是依赖范围搞错,后者是配置惯性。

5.2 故障二:MyBatis的#{}和${}导致SQL报错

现象:

Mapper 里的排序字段用${orderByColumn}写,正常情况下没问题,但传入orderByColumn = "id desc; drop table user;",数据库直接执行了多条语句,控制台报SQLSyntaxErrorException。另一个场景是#{columnName}用在ORDER BY后,查询结果永远是固定排序,怎么传参都不变。

原因:

#{}是预编译占位符,MyBatis 会把参数包装成PreparedStatement的?,所以传进去的内容永远被当数据,不能用它替换字段名和关键字;${}是字符串替换,直接把参数拼进 SQL,可以换字段,但引入注入风险。

解决:

排序字段这类位置,用白名单校验。在 Service 层写一个静态方法,只允许id、create_time、update_time三个值通过,其他一律抛IllegalArgumentException。

public void validateSortField(String field) { if (!Arrays.asList("id", "create_time", "update_time").contains(field)) { throw new IllegalArgumentException("非法的排序字段"); } }

从那以后,我再写 MyBatis 的 XML 时,遇到动态排序、动态表名这类场景,就把可选项枚举出来,宁可有十个 if 判断,也不开放${}入口。

5.3 故障三:前端传参格式不对导致400/415错误

现象:

前端用axios.post('/api/user/login', { username: 'admin', password: '123456' })提交,后端login方法用@RequestBody LoginDTO接收,结果请求直接返回 415 Unsupported Media Type。前端改成表单提交方式,后端login用普通 POJO 接收,又报 400 Bad Request,后端getParameter拿到的全是 null。

原因:

415 是请求体内容和接口期望的内容类型不匹配。axios 默认把对象序列化成 JSON,Content-Type是application/json;charset=UTF-8,后端接口如果没有@RequestBody,Spring MVC 不认为它该接收 JSON。400 则相反,后端用@RequestBody期望 JSON,前端却传了表单格式application/x-www-form-urlencoded,所以解析失败。

解决:

统一前后端约定。项目里所有 POST 接口一律用@RequestBody接收 JSON,前端 axios 请求里显式加Content-Type: application/json请求头。如果某些接口必须兼容表单,比如文件上传,那就在multipart/form-data场景下独立写接口,不要和 JSON 接口混在一起。

axios.post('/api/user/login', { username: this.username, password: this.password }, { headers: { 'Content-Type': 'application/json' } });

这里还有一个隐蔽问题:如果LoginDTO里有个字段是Integer status,前端传了空字符串"",Jackson 反序列化时会报Can not deserialize instance of java.lang.Integer out of START_OBJECT token。原因不是代码问题,是前端没有对空值做过滤。后来我在全局配置里给 ObjectMapper 加了一个DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT,才算从根上解决,但这毕竟掩盖了前端的类型不严谨,还是建议前端做校验。

6. 进阶收尾:从能跑通到跑得好

6.1 性能排查的简单手段

项目跑通后,第一件事是去 Druid 的监控页看一眼。路径默认是/druid/index.html,本地启动后访问可以看到 SQL 执行次数、慢查询列表、活跃连接数。我一般会重点看SlowSql标签,只要出现耗时超过 500ms 的 SQL,先去看对应表有没有索引。比如订单明细表order_item里order_id字段,如果建表脚本里没有加索引,查询一个包含几十个订单的列表,就可能触发全表扫描。

mysql> EXPLAIN SELECT * FROM order_item WHERE order_id = 10086;

看输出里的type列,如果是ALL,说明没走索引,回头检查sql/init.sql里有没有给order_id建索引。这是最简单的性能排查动作,不用 JProfiler 这类重量级工具。

6.2 安全加固:注意密码存储与接口防刷

资源里的登录密码存储用的是 BCrypt,不是 MD5。原因很简单,MD5 加盐的套路还是能被 GPU 批量碰撞,BCrypt 的强度参数是动态成本因子,单次计算就需要几十毫秒,暴力破解性价比太低。如果你自己在改项目,记得BCryptPasswordEncoder的strength参数选 10 到 12,太低等于没加密。

接口防刷方面,代码里对/api/user/login做了简单的请求频率限制:同一个 IP 在 60 秒内最多请求 20 次,超过就返回TooManyRequestsException。实现是写了一个拦截器配合 Guava 的RateLimiter,不是精确计数,但足够挡住初学者写的 for 循环脚本。

其实这套项目最让我意外的不是代码本身,而是注释里的坑记录。写OrderServiceImpl的开发者留下了一段话:事务方法必须走代理,this.orderMethod()自己调自己不会触发@Transactional。因为 Spring 的事务是基于 AOP 代理实现的,内部调用拿不到代理对象。从那以后,我每次写事务方法,都在另一个 Service 里注入当前 Service,保证入口是代理对象。如果你也是拿着 SSM 项目做毕业设计或者准备面试,把这几个坑写进文档里,比把功能清单写满更有说服力,因为面试官问的就是这些真实问题。希望这份资源能帮到你,少踩几个我已经踩过的坑。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询