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:拦截(终止请求)
postHandleController 执行之后,视图渲染之前修改 ModelAndView(如统一添加全局参数)无返回值(void)
afterCompletion视图渲染之后(整个请求完成)资源释放、异常处理、请求耗时统计无返回值(void)

关键注意点

  • preHandle返回false,后续的postHandleafterCompletion不会执行(包括当前拦截器和后续拦截器)。
  • afterCompletion无论preHandle是否放行、Controller 是否抛异常,都会执行(除非preHandle返回false),适合做 “最终资源清理”。

2.2 多拦截器执行顺序:谁先谁后?如何控制?

多个拦截器同时存在时,执行的顺序由配置顺序决定. 先配置谁, 谁就先执行.多个拦截器可以理解为拦截器栈, 先进后出(后进先出), 如图所示:

        实际项目中常配置多个拦截器(如 “登录拦截器”“日志拦截器”“权限拦截器”),其执行顺序由配置顺序决定,遵循 “preHandle 正序,postHandle/afterCompletion 逆序” 的规则。

2.2.1 实战配置:两个拦截器的协作
  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 拦截器实战:角色权限校验(精细化控制)

        仅登录校验不够,企业级项目需 “按角色控制访问权限”(如 “普通用户不能访问管理员页面”)。实现思路:

  1. 给 Controller 方法加 “角色注解”(如@RequireRole("ADMIN"))。
  2. 拦截器在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&amp;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.Empcom.jr.pojo.Dept(含 getter、setter、toString)。
  • Mapper 接口:com.jr.mapper.EmpMappercom.jr.mapper.DeptMapper(含基本 CRUD 方法)。
  • Mapper.xml:src/main/resources/mapper/EmpMapper.xmlDeptMapper.xml(含 SQL 语句)。

5.3 步骤 2:Spring 配置(applicationContext.xml)

Spring 配置的核心是 “整合 MyBatis” 和 “事务管理”,需注意数据源SqlSessionFactoryMapper 扫描事务管理器四大组件。

<?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 整合效果

  1. 启动 Tomcat,访问http://localhost:8080/ssm-demo/login.jsp,登录后跳转到员工列表页。
  2. 分页查询:访问http://localhost:8080/ssm-demo/emp/list.do?pageNum=1&pageSize=5,页面显示员工列表及分页控件。
  3. Ajax 接口测试:用 Postman 访问http://localhost:8080/ssm-demo/emp/getEmpById.do?empno=1,返回 JSON 格式的员工信息。

六、SSM 整合常见问题排查(关键避坑)

  1. Mapper 注入失败(No qualifying bean of type)

    • 原因:未配置MapperScannerConfigurer,或basePackage路径错误。
    • 解决:检查applicationContext.xmlMapperScannerConfigurerbasePackage是否为com.jr.mapper
  2. 事务不生效(添加员工后抛异常但数据未回滚)

    • 原因 1:事务管理器未关联数据源。
    • 原因 2:AOP 切入点表达式错误(未匹配到 Service 方法)。
    • 解决:检查transactionManagerdataSource配置,及aop:pointcutexpression是否为execution(* com.jr.service.impl.*.*(..))
  3. Spring 与 SpringMVC 重复扫描(BeanDefinitionOverrideException)

    • 原因:Spring 和 SpringMVC 都扫描了 Controller 或 Service,导致 Bean 重复定义。
    • 解决:Spring 扫描com.jr.service并排除 Controller;SpringMVC 扫描com.jr.controller并仅包含 Controller(见applicationContext.xmlspringmvc.xmlcontext:component-scan配置)。
  4. 文件上传报 400(The request sent by the client was syntactically incorrect)

    • 原因 1:未配置multipartResolver,或id不是multipartResolver(SpringMVC 固定 ID)。
    • 原因 2:上传文件超过maxUploadSize限制。
    • 解决:检查multipartResolver的配置,确保id正确且大小限制合理。

七、第三篇总结

本篇从 “深度” 和 “实战” 两个维度,彻底重构了 SpringMVC 进阶内容与 SSM 整合:

  1. 拦截器:深入执行机制与多拦截器协作,落地 “登录 + 权限” 双层拦截体系。
  2. 文件上传下载:解决重名、类型限制、中文乱码等企业级问题,实现安全存储。
  3. Ajax 交互:基于 Jackson 实现 JSON 自动转换,配置 CORS 跨域,结合 JSR303 参数校验。
  4. SSM 整合:通过逆向工程、分页插件、事务配置,搭建可直接复用的企业级项目骨架,并提供常见问题排查方案。

        至此,SpringMVC 系列博客已覆盖从 “基础入门” 到 “实战落地” 的完整知识体系,你已具备独立开发 SpringMVC 及 SSM 项目的能力。后续可进一步学习 Spring Boot(简化 SSM 配置)、Spring Cloud(微服务)等进阶技术,持续提升技术栈。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

森林-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值