SpringMVC 系列博客(三):进阶功能与 SSM 整合实战
目录
一、引言
二、SpringMVC 拦截器:从 “简单拦截” 到 “权限控制体系”
2.1 拦截器核心机制:三个方法的执行时机与作用
2.2 多拦截器执行顺序:谁先谁后?如何控制?
2.2.1 实战配置:两个拦截器的协作
2.2.2 执行顺序输出(关键结论)
2.3 拦截器实战:角色权限校验(精细化控制)
2.3.1 步骤 1:定义角色注解
2.3.2 步骤 2:自定义权限拦截器
2.3.3 步骤 3:在 Controller 中使用注解
2.3.4 配置拦截器(注意优先级)
三、文件上传与下载:从 “基础功能” 到 “企业级安全控制”
3.1 文件上传:解决 4 大核心问题
3.1.1 问题 1:文件重名覆盖
3.1.2 问题 2:恶意文件上传(如.exe、.jsp)
3.1.3 问题 3:文件过大导致 OOM
3.1.4 问题 4:中文文件名乱码
3.1.5 企业级文件上传实战代码
3.2 文件下载:解决 2 大核心问题
3.2.1 问题 1:中文文件名下载乱码
3.2.2 问题 2:直接暴露文件路径(安全风险)
3.2.3 企业级文件下载实战代码
四、SpringMVC 与 Ajax 交互:从 “基础 JSON” 到 “跨域与校验”
4.1 JSON 自动转换:@ResponseBody 与 Jackson 的协同
4.1.1 步骤 1:添加 Jackson 依赖(pom.xml)
4.1.2 步骤 2:自动转换 POJO 为 JSON
4.1.3 步骤 3:自定义 JSON 格式(日期、null 值处理)
4.2 跨域请求处理:CORS 配置(解决前后端分离跨域)
4.2.1 方案 1:通过注解局部配置(@CrossOrigin)
4.2.2 方案 2:通过 SpringMVC 全局配置(生产环境推荐)
4.3 Ajax 参数校验:JSR303(避免后端重复校验)
4.3.1 步骤 1:添加 JSR303 依赖(pom.xml)
4.3.2 步骤 2:在 POJO 中添加校验注解
4.3.3 步骤 3:在 Controller 中触发校验
4.3.4 前端 Ajax 处理(Vue 示例)
五、SSM 框架整合:企业级项目骨架实战(完整流程)
5.1 整合前准备:数据库与表结构
5.2 步骤 1:MyBatis 逆向工程(生成 POJO、Mapper)
5.2.1 步骤 1.1:添加逆向工程依赖(pom.xml)
5.2.2 步骤 1.2:编写逆向工程配置文件(generatorConfig.xml)
5.2.3 步骤 1.3:执行逆向工程(生成代码)
5.3 步骤 2:Spring 配置(applicationContext.xml)
5.4 步骤 3:SpringMVC 配置(springmvc.xml)
5.5 步骤 4:Web 配置(web.xml)
5.6 步骤 5:开发 Service 层与 Controller 层(实战 CRUD)
5.6.1 Service 层(业务逻辑)
5.6.2 Controller 层(请求处理)
5.7 步骤 6:测试 SSM 整合效果
六、SSM 整合常见问题排查(关键避坑)
七、第三篇总结
一、引言
前两篇博客已覆盖 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 {@Overridepublic 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;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {System.out.println("LoginInterceptor:postHandle");}@Overridepublic 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; // 记录请求开始时间@Overridepublic 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; // 日志拦截器不拦截,仅记录}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {System.out.println("LogInterceptor:postHandle");}@Overridepublic 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 {@Overridepublic 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(实际项目需从数据库查询)@Autowiredprivate UserService userService;// @ResponseBody:自动将返回的User对象转为JSON@RequestMapping("/getUserById.do")@ResponseBodypublic 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")@ResponseBodypublic User getUserById(Integer userId) {// 业务逻辑...}
}
4.2.2 方案 2:通过 SpringMVC 全局配置(生产环境推荐)
在springmvc.xml
中配置 CORS 拦截器,统一处理所有跨域请求:
<mvc:interceptors><!-- CORS跨域拦截器 --><bean class="org.springframework.web.servlet.handler.HandlerInterceptorAdapter">@Overridepublic 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):直接返回200if ("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, -- 关联部门表deptnoFOREIGN 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 generatorConfigurationPUBLIC "-//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/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/txhttp://www.springframework.org/schema/tx/spring-tx.xsdhttp://www.springframework.org/schema/aophttp://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/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/mvchttp://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/javaeehttp://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自动生成实现类)@Autowiredprivate EmpMapper empMapper;@Autowiredprivate DeptMapper deptMapper;@Overridepublic 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);}@Overridepublic Emp getEmpById(Integer empno) {return empMapper.selectByPrimaryKey(empno);}@Overridepublic int addEmp(Emp emp) {return empMapper.insertSelective(emp); // 选择性插入(null字段不插入)}@Overridepublic int updateEmp(Emp emp) {return empMapper.updateByPrimaryKeySelective(emp); // 选择性更新}@Overridepublic int deleteEmp(Integer empno) {return empMapper.deleteByPrimaryKey(empno);}
}
5.6.2 Controller 层(请求处理)
@Controller
@RequestMapping("/emp")
public class EmpController {@Autowiredprivate 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")@ResponseBodypublic 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")@ResponseBodypublic 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")@ResponseBodypublic Map<String, Object> updateEmp(@Valid @RequestBody Emp emp, BindingResult bindingResult) {// 逻辑与addEmp类似,省略...}// 5. 删除员工(Ajax接口)@RequestMapping("/deleteEmp.do")@ResponseBodypublic 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(微服务)等进阶技术,持续提升技术栈。