目录
二、SpringMVC 拦截器:从 “简单拦截” 到 “权限控制体系”
三、文件上传与下载:从 “基础功能” 到 “企业级安全控制”
四、SpringMVC 与 Ajax 交互:从 “基础 JSON” 到 “跨域与校验”
4.1 JSON 自动转换:@ResponseBody 与 Jackson 的协同
4.1.1 步骤 1:添加 Jackson 依赖(pom.xml)
4.1.3 步骤 3:自定义 JSON 格式(日期、null 值处理)
4.2.1 方案 1:通过注解局部配置(@CrossOrigin)
4.2.2 方案 2:通过 SpringMVC 全局配置(生产环境推荐)
4.3 Ajax 参数校验:JSR303(避免后端重复校验)
4.3.1 步骤 1:添加 JSR303 依赖(pom.xml)
5.2 步骤 1:MyBatis 逆向工程(生成 POJO、Mapper)
5.2.1 步骤 1.1:添加逆向工程依赖(pom.xml)
5.2.2 步骤 1.2:编写逆向工程配置文件(generatorConfig.xml)
5.3 步骤 2:Spring 配置(applicationContext.xml)
5.4 步骤 3:SpringMVC 配置(springmvc.xml)
5.6 步骤 5:开发 Service 层与 Controller 层(实战 CRUD)
一、引言
前两篇博客已覆盖 SpringMVC 的基础概念与核心功能,但企业级开发中,拦截器的复杂场景应用、文件上传的安全控制、Ajax 的跨域与 JSON 处理,以及最终的SSM 框架无缝整合,才是区分 “入门” 与 “实战” 的关键。本篇将彻底摒弃 “流程化堆砌”,聚焦 “深度解析 + 问题解决 + 实战落地”,带你真正掌握 SpringMVC 进阶能力,并完成可直接复用的 SSM 企业级项目骨架。
二、SpringMVC 拦截器:从 “简单拦截” 到 “权限控制体系”
拦截器绝非 “登录校验” 的单一用途,其核心价值在于构建 “请求增强与权限拦截体系”。本节将深入拦截器的执行机制、多拦截器协作,并落地 “登录拦截 + 角色权限校验” 的实战场景。

2.1 拦截器核心机制:三个方法的执行时机与作用
HandlerInterceptor接口的三个方法,决定了拦截器的 “生命周期”,必须理解其执行逻辑才能灵活应用:
| 方法名 | 执行时机 | 核心作用 | 返回值意义(仅 preHandle) |
|---|---|---|---|
preHandle | 请求到达 Controller之前 | 权限校验、参数预处理、日志记录(前置增强) | true:放行;false:拦截(终止请求) |
postHandle | Controller 执行之后,视图渲染之前 | 修改 ModelAndView(如统一添加全局参数) | 无返回值(void) |
afterCompletion | 视图渲染之后(整个请求完成) | 资源释放、异常处理、请求耗时统计 | 无返回值(void) |
关键注意点:
- 若
preHandle返回false,后续的postHandle和afterCompletion不会执行(包括当前拦截器和后续拦截器)。 afterCompletion无论preHandle是否放行、Controller 是否抛异常,都会执行(除非preHandle返回false),适合做 “最终资源清理”。
2.2 多拦截器执行顺序:谁先谁后?如何控制?
多个拦截器同时存在时,执行的顺序由配置顺序决定. 先配置谁, 谁就先执行.多个拦截器可以理解为拦截器栈, 先进后出(后进先出), 如图所示:

实际项目中常配置多个拦截器(如 “登录拦截器”“日志拦截器”“权限拦截器”),其执行顺序由配置顺序决定,遵循 “preHandle 正序,postHandle/afterCompletion 逆序” 的规则。
2.2.1 实战配置:两个拦截器的协作
- 自定义两个拦截器:
LoginInterceptor:负责登录校验(优先级高,先执行)。LogInterceptor:负责记录请求日志(优先级低,后执行)。
// 1. 登录拦截器(优先级1)
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("LoginInterceptor:preHandle(登录校验)");
// 登录校验逻辑:Session中无用户则重定向到登录页
Object loginUser = request.getSession().getAttribute("loginUser");
if (loginUser == null) {
// 传递重定向参数(告知登录页“从哪个页面跳转过来”)
String redirectUrl = request.getRequestURI() + "?" + request.getQueryString();
response.sendRedirect(request.getContextPath() + "/login.jsp?redirectUrl=" + URLEncoder.encode(redirectUrl, "UTF-8"));
return false;
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("LoginInterceptor:postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("LoginInterceptor:afterCompletion");
}
}
// 2. 日志拦截器(优先级2)
public class LogInterceptor implements HandlerInterceptor {
private long startTime; // 记录请求开始时间
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("LogInterceptor:preHandle(记录请求开始时间)");
startTime = System.currentTimeMillis();
// 记录请求基本信息(URL、IP、请求方法)
String url = request.getRequestURI();
String ip = request.getRemoteAddr();
String method = request.getMethod();
System.out.printf("请求信息:URL=%s, IP=%s, Method=%s%n", url, ip, method);
return true; // 日志拦截器不拦截,仅记录
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("LogInterceptor:postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 计算请求耗时
long costTime = System.currentTimeMillis() - startTime;
System.out.printf("LogInterceptor:afterCompletion(请求耗时:%dms)%n", costTime);
// 若有异常,记录异常信息
if (ex != null) {
System.out.printf("请求异常:%s%n", ex.getMessage());
}
}
}
SpringMVC 配置(按优先级排序):
<mvc:interceptors>
<!-- 1. 登录拦截器(先配置,先执行) -->
<mvc:interceptor>
<mvc:mapping path="/**"/> <!-- 拦截所有请求 -->
<!-- 排除登录相关路径,避免死循环 -->
<mvc:exclude-mapping path="/login.jsp"/>
<mvc:exclude-mapping path="/user/login.do"/>
<mvc:exclude-mapping path="/js/**"/>
<mvc:exclude-mapping path="/css/**"/>
<bean class="com.jr.interceptor.LoginInterceptor"/>
</mvc:interceptor>
<!-- 2. 日志拦截器(后配置,后执行) -->
<mvc:interceptor>
<mvc:mapping path="/**"/> <!-- 拦截所有请求 -->
<mvc:exclude-mapping path="/js/**"/>
<mvc:exclude-mapping path="/css/**"/>
<bean class="com.jr.interceptor.LogInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
2.2.2 执行顺序输出(关键结论)
当访问/emp/list.do(已登录状态)时,控制台输出如下:
LoginInterceptor:preHandle(登录校验)
LogInterceptor:preHandle(记录请求开始时间)
请求信息:URL=/ssm-demo/emp/list.do, IP=0:0:0:0:0:0:0:1, Method=GET
LoginInterceptor:postHandle
LogInterceptor:postHandle
LogInterceptor:afterCompletion(请求耗时:12ms)
LoginInterceptor:afterCompletion
- preHandle:按配置顺序执行(Login→Log)。
- postHandle:按配置逆序执行(Log→Login)。
- afterCompletion:按配置逆序执行(Log→Login)。
2.3 拦截器实战:角色权限校验(精细化控制)
仅登录校验不够,企业级项目需 “按角色控制访问权限”(如 “普通用户不能访问管理员页面”)。实现思路:
- 给 Controller 方法加 “角色注解”(如
@RequireRole("ADMIN"))。 - 拦截器在
preHandle中解析注解,判断当前用户角色是否匹配。
2.3.1 步骤 1:定义角色注解
import java.lang.annotation.*;
// 自定义角色注解,用于标记Controller方法需要的角色
@Target(ElementType.METHOD) // 仅作用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时生效(拦截器可反射获取)
public @interface RequireRole {
String[] value(); // 允许的角色列表(如{"ADMIN", "MANAGER"})
}
2.3.2 步骤 2:自定义权限拦截器
public class RoleInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 判断handler是否为Controller方法(排除非Controller请求)
if (!(handler instanceof HandlerMethod)) {
return true; // 非Controller方法(如静态资源),直接放行
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 2. 获取方法上的@RequireRole注解(无注解则放行)
RequireRole requireRole = handlerMethod.getMethodAnnotation(RequireRole.class);
if (requireRole == null) {
return true; // 无需角色校验,放行
}
// 3. 获取当前用户的角色(从Session中获取,实际项目可能从数据库查询)
User loginUser = (User) request.getSession().getAttribute("loginUser");
String userRole = loginUser.getRole(); // 如"USER"或"ADMIN"
// 4. 校验角色是否匹配
String[] allowRoles = requireRole.value();
boolean hasPermission = Arrays.asList(allowRoles).contains(userRole);
if (!hasPermission) {
// 无权限,跳转到403页面
response.sendRedirect(request.getContextPath() + "/403.jsp");
return false;
}
return true;
}
}
2.3.3 步骤 3:在 Controller 中使用注解
@Controller
@RequestMapping("/emp")
public class EmpController {
// 普通用户和管理员都能访问(无注解,放行)
@RequestMapping("/list.do")
public ModelAndView getEmpList() {
// 业务逻辑...
}
// 仅管理员能访问(加@RequireRole注解)
@RequireRole("ADMIN")
@RequestMapping("/delete.do")
public String deleteEmp(Integer empno) {
// 业务逻辑...
}
}
2.3.4 配置拦截器(注意优先级)
权限拦截器需在登录拦截器之后执行(先登录,再校验权限):
<mvc:interceptors>
<!-- 1. 登录拦截器(先执行) -->
<mvc:interceptor>...</mvc:interceptor>
<!-- 2. 权限拦截器(后执行) -->
<mvc:interceptor>
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/js/**"/>
<mvc:exclude-mapping path="/css/**"/>
<bean class="com.jr.interceptor.RoleInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
三、文件上传与下载:从 “基础功能” 到 “企业级安全控制”
基础的文件上传下载仅能满足 “能用”,企业级项目需解决文件覆盖、类型限制、大小控制、断点续传、安全存储等问题。本节将逐一攻克这些痛点。

3.1 文件上传:解决 4 大核心问题
3.1.1 问题 1:文件重名覆盖
原因:若多个用户上传同名文件(如 “头像.jpg”),后上传的会覆盖先上传的。
解决方案:生成 “唯一文件名”(如 “用户 ID + 时间戳 + 原文件后缀”)。
3.1.2 问题 2:恶意文件上传(如.exe、.jsp)
原因:直接允许上传所有类型文件,可能导致恶意文件执行(如上传.jsp 文件并访问,执行恶意代码)。
解决方案:1. 白名单限制文件类型;2. 禁止文件直接存储在 Web 可访问目录。
3.1.3 问题 3:文件过大导致 OOM
原因:未限制上传文件大小,超大文件会耗尽服务器内存。
解决方案:配置maxUploadSize限制单个文件大小,maxInMemorySize限制内存缓存大小。
3.1.4 问题 4:中文文件名乱码
原因:浏览器提交中文文件名时编码不一致,导致服务器接收乱码。
解决方案:配置defaultEncoding="UTF-8",并在保存文件时手动处理编码。
3.1.5 企业级文件上传实战代码
SpringMVC 配置(完善版):
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="defaultEncoding" value="UTF-8"/> <!-- 解决中文文件名乱码 -->
<property name="maxUploadSize" value="10485760"/> <!-- 单个文件最大10MB(1024*1024*10) -->
<property name="maxUploadSizePerFile" value="5242880"/> <!-- 单个文件最大5MB(细分控制) -->
<property name="maxInMemorySize" value="102400"/> <!-- 内存缓存最大100KB,超过则写入临时文件 -->
</bean>
Controller 实现(带安全控制):
@Controller
@RequestMapping("/file")
public class FileUploadController {
// 允许上传的文件类型(白名单)
private static final List<String> ALLOWED_EXTENSIONS = Arrays.asList("jpg", "png", "gif", "pdf", "doc");
// 文件存储路径(非Web可访问目录,避免恶意文件执行)
private static final String UPLOAD_BASE_PATH = "D:/ssm-upload/";
@RequestMapping("/upload.do")
public String upload(
@RequestParam("uploadFile") MultipartFile file,
String uploader,
HttpServletRequest request) throws Exception {
// 1. 校验文件是否为空
if (file.isEmpty()) {
request.setAttribute("msg", "错误:文件不能为空!");
return "upload.jsp";
}
// 2. 校验文件大小(双重校验,避免配置失效)
long fileSize = file.getSize();
if (fileSize > 5 * 1024 * 1024) {
request.setAttribute("msg", "错误:文件大小不能超过5MB!");
return "upload.jsp";
}
// 3. 校验文件类型(白名单)
String originalFilename = file.getOriginalFilename();
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf(".") + 1).toLowerCase();
if (!ALLOWED_EXTENSIONS.contains(fileExtension)) {
request.setAttribute("msg", "错误:仅允许上传jpg、png、gif、pdf、doc类型文件!");
return "upload.jsp";
}
// 4. 生成唯一文件名(避免覆盖)
String uniqueFileName = UUID.randomUUID().toString() + "." + fileExtension;
// 按日期分目录存储(如2024/05/17/xxx.jpg),便于管理
String dateDir = new SimpleDateFormat("yyyy/MM/dd").format(new Date());
String finalUploadPath = UPLOAD_BASE_PATH + dateDir + "/";
// 5. 创建目录(若不存在)
File uploadDir = new File(finalUploadPath);
if (!uploadDir.exists()) {
uploadDir.mkdirs(); // 递归创建多级目录
}
// 6. 保存文件到服务器(非Web目录)
File destFile = new File(finalUploadPath + uniqueFileName);
file.transferTo(destFile);
// 7. 记录文件信息到数据库(实际项目需持久化,此处省略)
System.out.printf("文件上传成功:上传人=%s, 原文件名=%s, 存储路径=%s%n",
uploader, originalFilename, destFile.getAbsolutePath());
// 8. 跳转成功页(传递文件访问路径,需通过Controller转发访问)
request.setAttribute("fileUrl", "/file/download.do?fileName=" + uniqueFileName + "&dateDir=" + dateDir);
return "uploadSuccess.jsp";
}
}
上传页面(upload.jsp):
<form action="${pageContext.request.contextPath}/file/upload.do" method="post" enctype="multipart/form-data">
上传人:<input type="text" name="uploader" required><br>
选择文件:<input type="file" name="uploadFile" required accept=".jpg,.png,.gif,.pdf,.doc"><br>
<input type="submit" value="上传">
<span style="color:red">${msg}</span>
</form>
3.2 文件下载:解决 2 大核心问题
3.2.1 问题 1:中文文件名下载乱码
原因:不同浏览器对下载文件名的编码处理不同(IE 用 UTF-8,Chrome 用 GBK)。
解决方案:根据浏览器类型动态设置编码。
3.2.2 问题 2:直接暴露文件路径(安全风险)
原因:若文件存储在 Web 目录下,用户可能通过 URL 直接访问未授权文件。
解决方案:文件存储在非 Web 目录,通过 Controller 统一鉴权后下载。
3.2.3 企业级文件下载实战代码
@RequestMapping("/download.do")
public void download(
String fileName, // 唯一文件名(如UUID)
String dateDir, // 日期目录(如2024/05/17)
String originalFileName, // 原文件名(用于下载时显示)
HttpServletRequest request,
HttpServletResponse response) throws Exception {
// 1. 权限校验(仅示例,实际项目需结合用户角色)
User loginUser = (User) request.getSession().getAttribute("loginUser");
if (loginUser == null) {
response.sendError(403, "未登录,无下载权限!");
return;
}
// 2. 拼接文件实际路径(非Web目录)
String finalFilePath = UPLOAD_BASE_PATH + dateDir + "/" + fileName;
File file = new File(finalFilePath);
// 3. 校验文件是否存在
if (!file.exists()) {
response.sendError(404, "文件不存在或已被删除!");
return;
}
// 4. 解决中文文件名下载乱码(根据浏览器类型设置编码)
String userAgent = request.getHeader("User-Agent");
if (userAgent.contains("MSIE") || userAgent.contains("Trident")) {
// IE浏览器:UTF-8编码
originalFileName = URLEncoder.encode(originalFileName, "UTF-8");
} else {
// Chrome/Firefox:ISO-8859-1编码
originalFileName = new String(originalFileName.getBytes("UTF-8"), "ISO-8859-1");
}
// 5. 设置响应头(告诉浏览器是下载操作)
response.setContentType("application/octet-stream"); // 二进制流(通用文件类型)
response.setContentLength((int) file.length()); // 设置文件大小
response.setHeader("Content-Disposition", "attachment;filename=\"" + originalFileName + "\"");
// 6. 高效读取文件并写入响应流(使用缓冲流,避免大文件内存溢出)
try (InputStream in = new BufferedInputStream(new FileInputStream(file));
OutputStream out = new BufferedOutputStream(response.getOutputStream())) {
byte[] buffer = new byte[1024 * 8]; // 8KB缓冲
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
out.flush(); // 及时刷新缓冲
}
} catch (Exception e) {
e.printStackTrace();
response.sendError(500, "文件下载失败!");
}
}
四、SpringMVC 与 Ajax 交互:从 “基础 JSON” 到 “跨域与校验”
前后端分离已成主流,SpringMVC 与 Ajax 的交互需解决JSON 自动转换、跨域请求、参数校验三大核心问题。本节将基于 Jackson(SpringMVC 默认 JSON 工具,替代 Gson)实现企业级交互方案。
4.1 JSON 自动转换:@ResponseBody 与 Jackson 的协同
SpringMVC 通过@ResponseBody注解,结合 Jackson 依赖,可自动将 Java 对象(POJO、List、Map)转为 JSON 字符串,无需手动调用toJson()方法。
4.1.1 步骤 1:添加 Jackson 依赖(pom.xml)
<!-- Jackson核心依赖(SpringMVC默认JSON转换器) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.15.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.15.2</version>
</dependency>
4.1.2 步骤 2:自动转换 POJO 为 JSON
@Controller
@RequestMapping("/user")
public class UserAjaxController {
// 注入Service(实际项目需从数据库查询)
@Autowired
private UserService userService;
// @ResponseBody:自动将返回的User对象转为JSON
@RequestMapping("/getUserById.do")
@ResponseBody
public User getUserById(Integer userId) {
// 从Service获取用户信息(模拟数据)
User user = userService.getUserById(userId);
return user; // Jackson自动转为JSON:{"userId":1,"username":"admin","role":"ADMIN"}
}
}
4.1.3 步骤 3:自定义 JSON 格式(日期、null 值处理)
默认情况下,Jackson 会将Date类型转为时间戳(如1684320000000),且会序列化null值字段。可通过注解或配置自定义格式:
通过注解自定义(POJO 类):
public class User {
private Integer userId;
private String username;
private String role;
// 自定义日期格式(转为"yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
// 忽略null值字段(null不序列化到JSON)
@JsonInclude(JsonInclude.Include.NON_NULL)
private String email;
// getter、setter...
}
通过 SpringMVC 配置全局自定义(springmvc.xml):
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="objectMapper">
<bean class="com.fasterxml.jackson.databind.ObjectMapper">
<!-- 全局日期格式 -->
<property name="dateFormat">
<bean class="java.text.SimpleDateFormat">
<constructor-arg value="yyyy-MM-dd HH:mm:ss"/>
</bean>
</property>
<!-- 全局忽略null值字段 -->
<property name="serializationInclusion" value="NON_NULL"/>
<!-- 允许序列化空对象(避免报异常) -->
<property name="serializationFeature" value="FAIL_ON_EMPTY_BEANS" />
</bean>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
4.2 跨域请求处理:CORS 配置(解决前后端分离跨域)
前后端分离项目中,前端(如 Vue)和后端(SpringMVC)通常部署在不同域名,会触发浏览器 “同源策略”,导致 Ajax 请求被拦截。解决方案是配置CORS(跨域资源共享)。
4.2.1 方案 1:通过注解局部配置(@CrossOrigin)
在需要跨域的 Controller 或方法上添加@CrossOrigin:
// 允许所有域名跨域访问(开发环境用,生产环境需指定具体域名)
@CrossOrigin(origins = "*", maxAge = 3600)
@Controller
@RequestMapping("/user")
public class UserAjaxController {
// 方法级注解(优先级高于类级)
@CrossOrigin(origins = "http://localhost:8081") // 仅允许http://localhost:8081跨域
@RequestMapping("/getUserById.do")
@ResponseBody
public User getUserById(Integer userId) {
// 业务逻辑...
}
}
4.2.2 方案 2:通过 SpringMVC 全局配置(生产环境推荐)
在springmvc.xml中配置 CORS 拦截器,统一处理所有跨域请求:
<mvc:interceptors>
<!-- CORS跨域拦截器 -->
<bean class="org.springframework.web.servlet.handler.HandlerInterceptorAdapter">
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 允许的源域名(生产环境替换为实际前端域名,如"http://www.xxx.com")
response.setHeader("Access-Control-Allow-Origin", "http://localhost:8081");
// 2. 允许的请求方法(GET、POST、PUT、DELETE等)
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
// 3. 允许的请求头(如Content-Type、Authorization)
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
// 4. 允许前端携带Cookie(需前端配置withCredentials: true)
response.setHeader("Access-Control-Allow-Credentials", "true");
// 5. 预检请求(OPTIONS)的缓存时间(3600秒,避免频繁预检)
response.setHeader("Access-Control-Max-Age", "3600");
// 处理预检请求(OPTIONS):直接返回200
if ("OPTIONS".equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return false;
}
return true;
}
</bean>
</mvc:interceptors>
4.3 Ajax 参数校验:JSR303(避免后端重复校验)
前端校验(如 “用户名不能为空”)可提升体验,但后端必须再次校验(防止恶意请求)。使用 JSR303(如 Hibernate Validator)可简化参数校验逻辑。
4.3.1 步骤 1:添加 JSR303 依赖(pom.xml)
<!-- Hibernate Validator(JSR303实现) -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.5.Final</version>
</dependency>
<!-- JSR303核心API -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
4.3.2 步骤 2:在 POJO 中添加校验注解
public class User {
// 用户名不能为空,且长度在2-20之间
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在2-20之间")
private String username;
// 密码不能为空,且必须包含字母和数字(正则表达式)
@NotBlank(message = "密码不能为空")
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d).{6,20}$", message = "密码必须包含字母和数字,长度6-20")
private String password;
// 邮箱格式校验
@Email(message = "邮箱格式不正确")
@JsonInclude(JsonInclude.Include.NON_NULL)
private String email;
// getter、setter...
}
4.3.3 步骤 3:在 Controller 中触发校验
@RequestMapping("/register.do")
@ResponseBody
public Map<String, Object> register(
// @Valid:触发参数校验,BindingResult:接收校验结果
@Valid @RequestBody User user,
BindingResult bindingResult) {
Map<String, Object> result = new HashMap<>();
// 1. 判断是否有校验错误
if (bindingResult.hasErrors()) {
// 2. 收集所有错误信息
Map<String, String> errorMap = new HashMap<>();
bindingResult.getFieldErrors().forEach(error -> {
// error.getField():错误字段名,error.getDefaultMessage():错误信息
errorMap.put(error.getField(), error.getDefaultMessage());
});
result.put("code", 400);
result.put("msg", "参数校验失败");
result.put("errors", errorMap);
return result;
}
// 3. 校验通过,执行注册逻辑
userService.register(user);
result.put("code", 200);
result.put("msg", "注册成功");
return result;
}
4.3.4 前端 Ajax 处理(Vue 示例)
this.$axios.post("/user/register.do", this.user)
.then(response => {
if (response.data.code === 200) {
alert("注册成功!");
} else {
// 显示校验错误(如用户名长度不够)
let errors = response.data.errors;
for (let field in errors) {
this.$message.error(errors[field]);
}
}
})
.catch(error => {
console.error("请求失败:", error);
});
五、SSM 框架整合:企业级项目骨架实战(完整流程)
SSM 整合的核心是 “各司其职”:
- MyBatis:负责 SQL 执行与结果映射(持久层)。
- Spring:负责 IOC 容器、依赖注入、事务管理(服务层)。
- SpringMVC:负责请求处理、视图跳转、参数绑定(Web 层)。
本节将基于 “员工信息管理系统”,实现完整的 SSM 整合,包含逆向工程、事务配置、分页插件、父子容器四大关键环节。
5.1 整合前准备:数据库与表结构
以emp(员工表)和dept(部门表)为例,SQL 脚本如下:
CREATE DATABASE IF NOT EXISTS ssm_demo;
USE ssm_demo;
-- 部门表
CREATE TABLE dept (
deptno INT PRIMARY KEY AUTO_INCREMENT,
dname VARCHAR(50) NOT NULL,
loc VARCHAR(100)
);
-- 员工表(关联部门表)
CREATE TABLE emp (
empno INT PRIMARY KEY AUTO_INCREMENT,
ename VARCHAR(50) NOT NULL,
job VARCHAR(50),
mgr INT, -- 上级员工编号
hiredate DATE, -- 入职日期
sal DECIMAL(10,2), -- 薪资
comm DECIMAL(10,2), -- 奖金
deptno INT, -- 关联部门表deptno
FOREIGN KEY (deptno) REFERENCES dept(deptno)
);
-- 插入测试数据
INSERT INTO dept (dname, loc) VALUES ('研发部', '北京'), ('销售部', '上海');
INSERT INTO emp (ename, job, hiredate, sal, deptno) VALUES
('张三', '开发工程师', '2020-01-15', 15000.00, 1),
('李四', '测试工程师', '2021-03-20', 12000.00, 1),
('王五', '销售经理', '2019-05-10', 20000.00, 2);
5.2 步骤 1:MyBatis 逆向工程(生成 POJO、Mapper)
手动编写 POJO 和 Mapper 效率低且易出错,使用 MyBatis 逆向工程可自动生成,大幅提升开发效率。
5.2.1 步骤 1.1:添加逆向工程依赖(pom.xml)
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.4.2</version>
</dependency>
5.2.2 步骤 1.2:编写逆向工程配置文件(generatorConfig.xml)
在src/main/resources下创建,配置数据库连接、生成路径、表映射:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-3-generator-config_1_0.dtd">
<generatorConfiguration>
<!-- 1. 配置数据库驱动(本地MySQL驱动路径,需替换为自己的路径) -->
<classPathEntry location="D:/maven/repository/mysql/mysql-connector-java/5.1.36/mysql-connector-java-5.1.36.jar"/>
<context id="DB2Tables" targetRuntime="MyBatis3">
<!-- 去除注释 -->
<commentGenerator>
<property name="suppressAllComments" value="true"/>
</commentGenerator>
<!-- 2. 配置数据库连接 -->
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/ssm_demo?useUnicode=true&characterEncoding=UTF-8"
userId="root"
password="root">
</jdbcConnection>
<!-- 3. 配置Java类型处理器(避免数值类型精度丢失) -->
<javaTypeResolver>
<property name="forceBigDecimals" value="false"/>
</javaTypeResolver>
<!-- 4. 配置POJO生成路径(包名:com.jr.pojo) -->
<javaModelGenerator targetPackage="com.jr.pojo" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<!-- 5. 配置Mapper.xml生成路径(resources/mapper) -->
<sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources">
<property name="enableSubPackages" value="true"/>
</sqlMapGenerator>
<!-- 6. 配置Mapper接口生成路径(com.jr.mapper) -->
<javaClientGenerator type="XMLMAPPER" targetPackage="com.jr.mapper" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
</javaClientGenerator>
<!-- 7. 配置表映射(tableName:数据库表名,domainObjectName:POJO类名) -->
<table tableName="emp" domainObjectName="Emp" enableCountByExample="false"
enableUpdateByExample="false" enableDeleteByExample="false"
enableSelectByExample="false" selectByExampleQueryId="false"/>
<table tableName="dept" domainObjectName="Dept" enableCountByExample="false"
enableUpdateByExample="false" enableDeleteByExample="false"
enableSelectByExample="false" selectByExampleQueryId="false"/>
</context>
</generatorConfiguration>
5.2.3 步骤 1.3:执行逆向工程(生成代码)
编写 Java 执行类,运行后自动生成 POJO、Mapper 接口、Mapper.xml:
public class GeneratorRun {
public static void main(String[] args) throws Exception {
List<String> warnings = new ArrayList<>();
boolean overwrite = true; // 覆盖已存在的文件
File configFile = new File("src/main/resources/generatorConfig.xml");
ConfigurationParser cp = new ConfigurationParser(warnings);
Configuration config = cp.parseConfiguration(configFile);
DefaultShellCallback callback = new DefaultShellCallback(overwrite);
MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
myBatisGenerator.generate(null);
// 打印生成结果
for (String warning : warnings) {
System.out.println(warning);
}
System.out.println("逆向工程执行完成!");
}
}
生成结果:
- POJO:
com.jr.pojo.Emp、com.jr.pojo.Dept(含 getter、setter、toString)。 - Mapper 接口:
com.jr.mapper.EmpMapper、com.jr.mapper.DeptMapper(含基本 CRUD 方法)。 - Mapper.xml:
src/main/resources/mapper/EmpMapper.xml、DeptMapper.xml(含 SQL 语句)。
5.3 步骤 2:Spring 配置(applicationContext.xml)
Spring 配置的核心是 “整合 MyBatis” 和 “事务管理”,需注意数据源、SqlSessionFactory、Mapper 扫描、事务管理器四大组件。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 1. 加载数据库配置文件(jdbc.properties) -->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- 2. 配置数据源(DBCP连接池,生产环境推荐Druid) -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="${mysql.driver}"/>
<property name="url" value="${mysql.url}"/>
<property name="username" value="${mysql.username}"/>
<property name="password" value="${mysql.password}"/>
<property name="maxActive" value="20"/> <!-- 最大活跃连接数 -->
<property name="maxIdle" value="5"/> <!-- 最大空闲连接数 -->
<property name="minIdle" value="2"/> <!-- 最小空闲连接数 -->
<property name="initialSize" value="5"/> <!-- 初始连接数 -->
<property name="timeBetweenEvictionRunsMillis" value="60000"/> <!-- 连接检测间隔 -->
<property name="minEvictableIdleTimeMillis" value="300000"/> <!-- 连接最小空闲时间(5分钟) -->
</bean>
<!-- 3. 配置MyBatis的SqlSessionFactory(整合核心) -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/> <!-- 关联数据源 -->
<property name="configLocation" value="classpath:SqlMapConfig.xml"/> <!-- MyBatis核心配置 -->
<property name="mapperLocations" value="classpath:mapper/*.xml"/> <!-- Mapper.xml路径 -->
<!-- 配置分页插件(PageHelper,需先添加依赖) -->
<property name="plugins">
<array>
<bean class="com.github.pagehelper.PageInterceptor">
<property name="properties">
<props>
<prop key="helperDialect">mysql</prop> <!-- 数据库方言 -->
<prop key="reasonable">true</prop> <!-- 合理化分页(避免页码越界) -->
<prop key="supportMethodsArguments">true</prop> <!-- 支持方法参数分页 -->
</props>
</property>
</bean>
</array>
</property>
</bean>
<!-- 4. 扫描Mapper接口(自动生成Mapper实现类,注入Spring容器) -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.jr.mapper"/> <!-- Mapper接口所在包 -->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> <!-- 关联SqlSessionFactory -->
</bean>
<!-- 5. 扫描Service层注解(@Service),排除Controller(交给SpringMVC扫描) -->
<context:component-scan base-package="com.jr.service">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!-- 6. 配置事务管理器(DataSourceTransactionManager) -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/> <!-- 关联数据源 -->
</bean>
<!-- 7. 配置事务通知(XML方式,适合复杂事务规则) -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 增删改操作: REQUIRED(无事务则新建,有则加入) -->
<tx:method name="add*" propagation="REQUIRED" isolation="DEFAULT"/>
<tx:method name="delete*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<!-- 查询操作: SUPPORTS(有事务则加入,无则无事务) -->
<tx:method name="select*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="get*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
</tx:attributes>
</tx:advice>
<!-- 8. 配置AOP切入点(将事务通知织入Service层方法) -->
<aop:config>
<aop:pointcut id="txPointcut" expression="execution(* com.jr.service.impl.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>
</beans>
5.4 步骤 3:SpringMVC 配置(springmvc.xml)
SpringMVC 配置的核心是 “请求处理”,需注意注解扫描、静态资源放行、视图解析器三大组件,且需与 Spring 的 “父子容器” 划清边界。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 1. 扫描Controller注解(@Controller),仅扫描Web层,避免与Spring重复扫描 -->
<context:component-scan base-package="com.jr.controller">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!-- 2. 开启注解驱动(自动加载处理器映射器、适配器、JSON转换器) -->
<mvc:annotation-driven>
<mvc:message-converters>
<!-- 配置Jackson JSON转换器(解决中文乱码) -->
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>application/json;charset=UTF-8</value>
<value>text/html;charset=UTF-8</value>
</list>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<!-- 3. 静态资源放行(JS、CSS、图片,避免被DispatcherServlet拦截) -->
<mvc:resources location="/js/" mapping="/js/**"/>
<mvc:resources location="/css/" mapping="/css/**"/>
<mvc:resources location="/images/" mapping="/images/**"/>
<!-- 4. 配置视图解析器(JSP视图,可选,适合非前后端分离项目) -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"/> <!-- 视图前缀(JSP文件所在目录) -->
<property name="suffix" value=".jsp"/> <!-- 视图后缀 -->
<property name="order" value="1"/> <!-- 视图解析器优先级 -->
</bean>
<!-- 5. 配置拦截器(登录拦截、权限拦截等) -->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/login.jsp"/>
<mvc:exclude-mapping path="/user/login.do"/>
<mvc:exclude-mapping path="/js/**"/>
<mvc:exclude-mapping path="/css/**"/>
<bean class="com.jr.interceptor.LoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
</beans>
5.5 步骤 4:Web 配置(web.xml)
web.xml是 SSM 整合的 “入口”,需配置Spring 监听器、SpringMVC 前端控制器、乱码过滤器,并指定配置文件路径。
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<!-- 1. 配置Spring监听器(加载Spring配置文件,创建Spring容器) -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value> <!-- Spring配置文件路径 -->
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 2. 配置乱码过滤器(解决POST请求参数乱码,必须放在所有过滤器之前) -->
<filter>
<filter-name>characterEncodingFilter</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>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern> <!-- 拦截所有请求 -->
</filter-mapping>
<!-- 3. 配置SpringMVC前端控制器(DispatcherServlet,Web层入口) -->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value> <!-- SpringMVC配置文件路径 -->
</init-param>
<load-on-startup>1</load-on-startup> <!-- Tomcat启动时加载Servlet -->
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern> <!-- 拦截所有请求(除JSP外) -->
</servlet-mapping>
<!-- 4. 配置欢迎页 -->
<welcome-file-list>
<welcome-file>login.jsp</welcome-file>
</welcome-file-list>
</beans>
5.6 步骤 5:开发 Service 层与 Controller 层(实战 CRUD)
5.6.1 Service 层(业务逻辑)
Service 接口(EmpService.java):
public interface EmpService {
// 分页查询员工列表
PageInfo<Emp> getEmpListByPage(Integer pageNum, Integer pageSize);
// 根据ID查询员工(关联部门信息)
Emp getEmpById(Integer empno);
// 添加员工
int addEmp(Emp emp);
// 修改员工
int updateEmp(Emp emp);
// 删除员工
int deleteEmp(Integer empno);
}
Service 实现类(EmpServiceImpl.java):
@Service("empService")
public class EmpServiceImpl implements EmpService {
// 注入Mapper(Spring自动生成实现类)
@Autowired
private EmpMapper empMapper;
@Autowired
private DeptMapper deptMapper;
@Override
public PageInfo<Emp> getEmpListByPage(Integer pageNum, Integer pageSize) {
// 分页插件:设置当前页和每页条数(PageHelper自动拦截SQL添加limit)
PageHelper.startPage(pageNum, pageSize);
// 查询员工列表(MyBatis逆向工程生成的方法)
List<Emp> empList = empMapper.selectByExample(null);
// 关联部门信息(手动关联,也可通过MyBatis关联查询实现)
for (Emp emp : empList) {
Integer deptno = emp.getDeptno();
if (deptno != null) {
Dept dept = deptMapper.selectByPrimaryKey(deptno);
emp.setDept(dept); // 给Emp对象设置Dept属性
}
}
// 封装分页信息(总条数、总页数等)
return new PageInfo<>(empList);
}
@Override
public Emp getEmpById(Integer empno) {
return empMapper.selectByPrimaryKey(empno);
}
@Override
public int addEmp(Emp emp) {
return empMapper.insertSelective(emp); // 选择性插入(null字段不插入)
}
@Override
public int updateEmp(Emp emp) {
return empMapper.updateByPrimaryKeySelective(emp); // 选择性更新
}
@Override
public int deleteEmp(Integer empno) {
return empMapper.deleteByPrimaryKey(empno);
}
}
5.6.2 Controller 层(请求处理)
@Controller
@RequestMapping("/emp")
public class EmpController {
@Autowired
private EmpService empService;
// 1. 分页查询员工列表(跳转JSP页面,非前后端分离)
@RequestMapping("/list.do")
public String getEmpList(
@RequestParam(defaultValue = "1") Integer pageNum, // 默认第1页
@RequestParam(defaultValue = "5") Integer pageSize, // 默认每页5条
Model model) {
// 调用Service获取分页数据
PageInfo<Emp> pageInfo = empService.getEmpListByPage(pageNum, pageSize);
// 传递分页数据到视图
model.addAttribute("pageInfo", pageInfo);
return "empList"; // 视图解析器解析为/WEB-INF/jsp/empList.jsp
}
// 2. 根据ID查询员工(Ajax接口,前后端分离)
@RequestMapping("/getEmpById.do")
@ResponseBody
public Map<String, Object> getEmpById(Integer empno) {
Map<String, Object> result = new HashMap<>();
try {
Emp emp = empService.getEmpById(empno);
result.put("code", 200);
result.put("data", emp);
} catch (Exception e) {
e.printStackTrace();
result.put("code", 500);
result.put("msg", "查询失败:" + e.getMessage());
}
return result;
}
// 3. 添加员工(Ajax接口)
@RequestMapping("/addEmp.do")
@ResponseBody
public Map<String, Object> addEmp(@Valid @RequestBody Emp emp, BindingResult bindingResult) {
Map<String, Object> result = new HashMap<>();
// 参数校验
if (bindingResult.hasErrors()) {
Map<String, String> errorMap = new HashMap<>();
bindingResult.getFieldErrors().forEach(error -> {
errorMap.put(error.getField(), error.getDefaultMessage());
});
result.put("code", 400);
result.put("errors", errorMap);
return result;
}
// 业务处理
try {
int rows = empService.addEmp(emp);
if (rows > 0) {
result.put("code", 200);
result.put("msg", "添加成功!");
} else {
result.put("code", 400);
result.put("msg", "添加失败!");
}
} catch (Exception e) {
e.printStackTrace();
result.put("code", 500);
result.put("msg", "添加异常:" + e.getMessage());
}
return result;
}
// 4. 修改员工(Ajax接口)
@RequestMapping("/updateEmp.do")
@ResponseBody
public Map<String, Object> updateEmp(@Valid @RequestBody Emp emp, BindingResult bindingResult) {
// 逻辑与addEmp类似,省略...
}
// 5. 删除员工(Ajax接口)
@RequestMapping("/deleteEmp.do")
@ResponseBody
public Map<String, Object> deleteEmp(Integer empno) {
Map<String, Object> result = new HashMap<>();
try {
int rows = empService.deleteEmp(empno);
if (rows > 0) {
result.put("code", 200);
result.put("msg", "删除成功!");
} else {
result.put("code", 400);
result.put("msg", "员工不存在!");
}
} catch (Exception e) {
e.printStackTrace();
result.put("code", 500);
result.put("msg", "删除异常:" + e.getMessage());
}
return result;
}
}
5.7 步骤 6:测试 SSM 整合效果
- 启动 Tomcat,访问
http://localhost:8080/ssm-demo/login.jsp,登录后跳转到员工列表页。 - 分页查询:访问
http://localhost:8080/ssm-demo/emp/list.do?pageNum=1&pageSize=5,页面显示员工列表及分页控件。 - Ajax 接口测试:用 Postman 访问
http://localhost:8080/ssm-demo/emp/getEmpById.do?empno=1,返回 JSON 格式的员工信息。
六、SSM 整合常见问题排查(关键避坑)
-
Mapper 注入失败(No qualifying bean of type):
- 原因:未配置
MapperScannerConfigurer,或basePackage路径错误。 - 解决:检查
applicationContext.xml中MapperScannerConfigurer的basePackage是否为com.jr.mapper。
- 原因:未配置
-
事务不生效(添加员工后抛异常但数据未回滚):
- 原因 1:事务管理器未关联数据源。
- 原因 2:AOP 切入点表达式错误(未匹配到 Service 方法)。
- 解决:检查
transactionManager的dataSource配置,及aop:pointcut的expression是否为execution(* com.jr.service.impl.*.*(..))。
-
Spring 与 SpringMVC 重复扫描(BeanDefinitionOverrideException):
- 原因:Spring 和 SpringMVC 都扫描了 Controller 或 Service,导致 Bean 重复定义。
- 解决:Spring 扫描
com.jr.service并排除 Controller;SpringMVC 扫描com.jr.controller并仅包含 Controller(见applicationContext.xml和springmvc.xml的context:component-scan配置)。
-
文件上传报 400(The request sent by the client was syntactically incorrect):
- 原因 1:未配置
multipartResolver,或id不是multipartResolver(SpringMVC 固定 ID)。 - 原因 2:上传文件超过
maxUploadSize限制。 - 解决:检查
multipartResolver的配置,确保id正确且大小限制合理。
- 原因 1:未配置
七、第三篇总结
本篇从 “深度” 和 “实战” 两个维度,彻底重构了 SpringMVC 进阶内容与 SSM 整合:
- 拦截器:深入执行机制与多拦截器协作,落地 “登录 + 权限” 双层拦截体系。
- 文件上传下载:解决重名、类型限制、中文乱码等企业级问题,实现安全存储。
- Ajax 交互:基于 Jackson 实现 JSON 自动转换,配置 CORS 跨域,结合 JSR303 参数校验。
- SSM 整合:通过逆向工程、分页插件、事务配置,搭建可直接复用的企业级项目骨架,并提供常见问题排查方案。
至此,SpringMVC 系列博客已覆盖从 “基础入门” 到 “实战落地” 的完整知识体系,你已具备独立开发 SpringMVC 及 SSM 项目的能力。后续可进一步学习 Spring Boot(简化 SSM 配置)、Spring Cloud(微服务)等进阶技术,持续提升技术栈。
1735

被折叠的 条评论
为什么被折叠?



