SSM毕业设计新手实战指南:从零搭建到避坑实践
摘要:许多计算机专业学生在毕业设计中首次接触SSM(Spring + Spring MVC + MyBatis)框架,常因配置复杂、依赖冲突或事务管理不当导致项目卡壳。本文面向新手,系统梳理SSM整合的核心流程,提供可运行的最小化代码模板,详解Web层与Service层解耦、MyBatis映射规范及事务回滚机制,并总结开发环境常见陷阱(如Mapper扫描失败、静态资源拦截等),帮助你高效完成一个结构清晰、可扩展的毕业设计项目。
1. 背景痛点:为什么“Hello SSM”总跑不起来
第一次做毕业设计,老师一句“用 SSM 吧”听起来轻飘飘,真动手才发现全是坑:
- 依赖版本对不上:Spring 5.x 配 MyBatis 3.5.x,一不小心就出现“ClassNotFound”或“NoSuchMethod”。
- XML 文件到处放:spring-context.xml、spring-mvc.xml、mybatis-config.xml、*Mapper.xml,路径写错就扫描不到。
- 事务不生效:Service 方法抛出 RuntimeException,数据依旧写进数据库,回滚像空气。
- 静态资源 404:CSS、JS 被 DispatcherServlet 拦截,页面裸奔。
- Mapper 接口注入爆红:IDEA 提示“Could not autowire”,运行却正常,新手直接原地爆炸。
这些痛点的根源只有一句话:对“三大框架各自负责什么”没有体感。下面先用“最小可运行骨架”带你跑通第一个请求,再逐步加功能。
2. 技术选型:SSM 还是 Spring Boot?教学场景怎么选
| 对比维度 | SSM | Spring Boot |
|---|---|---|
| 配置风格 | XML+注解混合,显式声明 | 自动装配,约定大于配置 |
| 启动方式 | 外置 Tomcat,WAR 包 | 内置容器,JAR 包 |
| 学习曲线 | 陡峭,但“看得见”底层 | 平缓,封装黑盒 |
| 教学价值 | 高,能体会 IoC、AOP、MVC 各环节 | 中,容易“只写业务,不知所以” |
| 毕业设计答辩 | 老师熟悉,易解释 | 需讲自动配置原理,稍难 |
结论:教学级、答辩友好、可展示“我懂底层”——SSM 仍是稳妥选择;Spring Boot 更适合快速原型或公司项目。
3. 核心实现细节:让 XML 不再玄学
3.1 整体启动顺序
- Tomcat 启动 → 读取 web.xml
- ContextLoaderListener 初始化 Spring 根容器(Service、Dao、事务)
- DispatcherServlet 初始化 Spring MVC 子容器(Controller、ViewResolver)
- 客户端请求 → DispatcherServlet → HandlerMapping → Controller → Service → Mapper → DB
3.2 配置文件职责一览
| 文件 | 所在位置 | 核心职责 |
|---|---|---|
| web.xml | /src/main/webapp/WEB-INF | 注册 Spring 容器、DispatcherServlet、字符编码过滤器 |
| spring-context.xml | classpath | 开启注解扫描(除 Controller)、配置数据源、SqlSessionFactory、Mapper 扫描、事务 |
| spring-mvc.xml | classpath | 开启 Controller 扫描、注解驱动、静态资源放行、视图解析器 |
| mybatis-config.xml | classpath | 全局设置(驼峰映射、日志、二级缓存开关) |
| *Mapper.xml | classpath/mapper/ | SQL 与接口映射 |
3.3 关键配置片段
- web.xml —— 让 Spring 跟 MVC 各就各位
<!-- 1. 根容器 --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-context.xml</param-value> </context-param> <!-- 2. MVC 子容器 --> <servlet> <servlet-name>spring</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <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>spring</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <!-- 3. 字符编码 --> <filter> <filter-name>encoding</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> </filter> <filter-mapping> <filter-name>encoding</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>- spring-context.xml —— 数据源 + Mapper 扫描 + 事务
<!-- 仅扫描 Service 与 Dao --> <context:component-scan base-package="com.example.demo"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan> <!-- 数据源 --> <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/ssm_db?useSSL=false&serverTimezone=UTC"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean> <!-- SqlSessionFactory --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <!-- Mapper 接口扫描 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.example.demo.mapper"/> </bean> <!-- 事务 --> <tx:annotation-driven/> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean>- spring-mvc.xml —— 静态资源放行与视图解析
<!-- 只扫 Controller --> <context:component-scan base-package="com.example.demo.controller"/> <mvc:annotation-driven/> <!-- 静态资源 --> <mvc:resources mapping="/static/**" location="/static/"/> <!-- 视图解析器 --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean>4. 最小可运行代码:用户登录模块
目标:浏览器提交 username/password → 后端验证 → 返回 success/fail
4.1 表结构
CREATE TABLE t_user( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) UNIQUE, password VARCHAR(64) -- 存加密后 );4.2 实体类
package com.example.demo.entity; public class User { private Integer id; private String username; private String password; // getter/setter 略 }4.3 Mapper 接口
package com.example.demo.mapper; import com.example.demo.entity.User; import org.apache.ibatis.annotations.Param; public interface UserMapper { User findByUsername(@Param("username") String username); }4.4 UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.demo.mapper.UserMapper"> <select id="findByUsername" resultType="com.example.demo.entity.User"> SELECT id, username, password FROM t_user WHERE username = #{username} </select> </mapper>4.5 Service 层(含密码加密与事务)
package com.example.demo.service; import com.example.demo.entity.User; import com.example.demo.mapper.UserMapper; import com.example.demo.util.PasswordUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class UserService { @Autowired private UserMapper userMapper; /** * 登录:匹配数据库密文 */ @Transactional public boolean login(String username, String rawPassword){ User user = userMapper.findByUsername(username); if(user == null) return false; return PasswordUtil.match(rawPassword, user.getPassword()); } }4.6 Controller
package com.example.demo.controller; import com.example.demo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpSession; @Controller @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @PostMapping("/login") public String login(@RequestParam String username, @RequestParam String password, HttpSession session){ boolean ok = userService.login(username, password); if(ok){ session.setAttribute("user", username); return "redirect:/index.jsp"; }else{ return "redirect:/login.jsp?error=1"; } } }4.7 PasswordUtil(SHA-256+随机盐)
public class PasswordUtil { private static final String SALT = "ssm_demo_2024"; public static String encode(String raw){ return DigestUtils.sha256Hex(SALT + raw); } public static boolean match(String raw, String encoded){ return encoded.equals(encode(raw)); } }以上代码遵循 Clean Code:方法短小、意图明确、注释只说“为什么”而非“做什么”。
5. 性能与安全:别让毕业设计变成“脱库现场”
- SQL 注入:MyBatis
#{}已预编译,但${}直接拼接,禁用。 - 密码存储:明文→MD5→SHA-256+盐→未来可升级 BCrypt。
- 会话固定:登录后
session.invalidate()再request.getSession(true)。 - 敏感接口加拦截器:
public class LoginInterceptor implements HandlerInterceptor { public boolean preHandle(...){ if(session.getAttribute("user")==null){ response.sendRedirect("/login.jsp"); return false; } return true; } }- 数据库连接池:Druid 自带防火墙,开启 SQL 监控可发现慢查询。
6. 生产环境避坑 20 条(精华版)
@ComponentScan路径写错 → bean 注入失败;IDEA 提示“Could not autowire”先检查包名。- spring-context.xml 扫到 Controller → 事务失效;确保 exclude-filter。
- spring-mvc.xml 未开
<mvc:annotation-driven/>→ 新日期转换失败。 - Mapper.xml 不在
mapperLocations路径 → “Invalid bound statement”。 - 接口方法与 XML id 不一致 → 同上。
- 忘记在 web.xml 配 CharacterEncodingFilter → 中文乱码。
- DispatcherServlet 拦截 “/” 却没放静态资源 → 404;用
<mvc:resources>。 - Druid 低版本与 JDK 17 不兼容 → 升 1.2.15+。
- 事务方法不是 public 或自调用(this.) → 不回滚。
- 多数据源未指定
@Primary→ 启动报错。 - log4j 仅配根日志,SQL 不打印 → 加
<logger name="com.example.demo.mapper" level="DEBUG"/>。 - Tomcat 10 用 Jakarta 包名 → 与 Spring 5 冲突;降级 9.x 或等 Spring 6。
- 热部署 devtools 与 Druid 监控冲突 → 页面 502;关 devtools 或换版本。
- 同时引 spring-jdbc 5.3 与 mybatis-spring 2.0 → 事务管理器找不到类;统一 mybatis-spring 2.1+。
- IDEA 自动把
*.xml放到resources外 → 打包丢失;标记资源目录。 - 误用 JSP 内置对象 EL 忽略 → 页面空白;web.xml 开
<el-ignored>false</el-ignored>。 - Maven 打包含测试 → 连不到本地库;加
-DskipTests。 - 上传文件过大 → 默认 2 M;配 MultipartResolver 的 maxUploadSize。
- 浏览器缓存导致静态资源不更新 → 资源加版本号
xxx.css?v=1.0。 - 服务器时钟与数据库时区差 8 小时 → JDBC URL 加
&serverTimezone=Asia/Shanghai。
7. 下一步:把“登录”升级成完整“用户管理”模块
- 写 UserController 的 CRUD 四个方法,REST 风格路径。
- Service 层加
@Transactional(rollbackFor = Exception.class)保证非运行时异常也回滚。 - Mapper 写
<insert useGeneratedKeys="true">拿到自增主键。 - 列表页引入 PageHelper:
- Maven 引
pagehelper - mybatis-config.xml 加插件
<plugin interceptor="com.github.pagehelper.PageInterceptor"/> - Controller 接收
pageNum、pageSize,返回PageInfo。
- Maven 引
- 日志:整合 logback + SLF4J,按天滚动,控制台只留 INFO,文件留 DEBUG。
8. 结尾:先跑起来,再谈优化
把上面的登录模块按目录结构原样拷进 IDEA,Maven 刷依赖,启动 Tomcat,看到“登录成功”那一刻,SSM 的神秘感就破功了。接下来别急着加“高大上”的微服务,先把用户管理的分页、搜索、头像上传、日志切面一个个啃下来,答辩时你能讲清楚“为什么事务回滚”“SQL 怎么防注入”“分页插件做了什么”,就已经赢过大多数“复制粘贴党”。等你真正踩完这些坑,再回头看 Spring Boot 的自动配置,会由衷感叹:“原来它帮我省了这么多事”——而那一刻,你已不再是 SSM 新手。祝你毕业设计一遍过,代码不报错,答辩不怼人。