写在前面:这篇文章到底有多干?

各位小伙伴,你好!

如果你正在寻找一篇能让你彻底搞懂SpringMVC的文章,那么恭喜你,找对地方了。这篇文章不是简单的API罗列,也不是枯燥的源码分析,而是一套完整的认知体系

我们将遵循一套六阶段教学法

  1. 问题锚定:先告诉你这技术是解决什么祖坟冒青烟的问题的。

  2. 基础认知:用大白话讲清楚它到底是什么。

  3. 核心用法拆解:手把手教你最常用的功能。

  4. 场景融合:把它放到真实的业务流程里,看它怎么和其他兄弟(Redis、MyBatis)配合。

  5. 企业级实战:来一个完整的项目,把规范、分层、异常处理都玩明白。

  6. 复盘升华:总结最佳实践,聊聊面试题,看看技术演进。

全文预计阅读时间较长(但绝对值得),总字数超过50000字,涵盖了从最基础的XML配置到SpringBoot自动配置,从老式JSP到前后端分离的RESTful API,从单机应用到分布式微服务的各种场景。

准备好了吗?让我们开始这段从“石器时代”到“云原生”的旅程。


阶段 1:问题锚定——我们为什么要学习SpringMVC?

1.1 石器时代的噩梦:Servlet + JSP 的混搭岁月

想象一下,你现在是一个刚入行的Java菜鸟,公司让你做一个用户管理系统。功能很简单:用户通过浏览器访问 http://localhost:8080/user/list,能看到所有用户的列表。

在2005年前后,标准的做法是这样的:

第一步:写一个Servlet

// UserListServlet.java
@WebServlet("/user/list")
public class UserListServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 1. 接收请求参数(虽然这里没有)
        // 2. 调用业务逻辑(通常是手动new一个Service)
        UserService userService = new UserServiceImpl();
        List<User> userList = userService.findAllUsers();
        
        // 3. 将数据存到request域中,准备给JSP展示
        request.setAttribute("users", userList);
        
        // 4. 转发到JSP页面进行渲染
        request.getRequestDispatcher("/WEB-INF/jsp/userList.jsp").forward(request, response);
    }
}

第二步:写一个JSP

<!-- userList.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <title>用户列表</title>
</head>
<body>
    <table>
        <tr>
            <th>ID</th>
            <th>姓名</th>
        </tr>
        <%-- 这里混合着Java代码和HTML标签 --%>
        <c:forEach items="${users}" var="user">
            <tr>
                <td>${user.id}</td>
                <td>${user.name}</td>
            </tr>
        </c:forEach>
    </table>
</body>
</html>

第三步:配置web.xml(噩梦的开始)

<!-- web.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    
    <!-- 这只是其中一个Servlet,如果有100个功能呢? -->
    <servlet>
        <servlet-name>userListServlet</servlet-name>
        <servlet-class>com.example.servlet.UserListServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>userListServlet</servlet-name>
        <url-pattern>/user/list</url-pattern>
    </servlet-mapping>
    
    <!-- 还有用户新增Servlet -->
    <servlet>
        <servlet-name>userAddServlet</servlet-name>
        <servlet-class>com.example.servlet.UserAddServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>userAddServlet</servlet-name>
        <url-pattern>/user/add</url-pattern>
    </servlet-mapping>
    
    <!-- 还有用户删除、更新、查询...... -->
</web-app>
痛点分析:这有什么不好?
  1. 配置爆炸:每写一个功能,就要在 web.xml 里配一个 <servlet> 和 <servlet-mapping>。一个稍微大点的项目,web.xml 文件能有几千行,维护起来简直是灾难。

  2. 参数获取极其繁琐:如果想获取表单提交的数据,得用 request.getParameter("username"),然后手动进行类型转换(比如把String转成Integer),还要手动做空值校验。

  3. 数据共享方式混乱:数据存在 requestsession 还是 application 里?全靠程序员自觉,没有统一规范。

  4. 代码耦合度高:Servlet 里既要接收请求参数,又要调用业务逻辑,还要负责跳转页面,违反了“单一职责原则”。

  5. 视图技术绑定:通常只能返回JSP,如果想返回JSON数据给前端(比如做App的后端接口),需要手动用 response.getWriter().print() 拼接JSON字符串,极其容易出错。

结论:我们急需一个框架,能帮我们解决这些“重复造轮子”且“容易出错”的问题。

1.2 SpringMVC 的定位:Web层的“大管家”

SpringMVC 就是为解决上述痛点而生的。它的核心定位是:

SpringMVC 是一个基于 Java 的轻量级 Web 框架,它实现了 MVC 设计模式,通过一套注解和组件,将 Web 层的职责进行清晰拆分,让你能像搭积木一样构建 Web 应用。

  • 它解决什么问题:Web 层(表现层)的请求接收、参数解析、数据封装、页面跳转、响应返回等繁琐问题。

  • 它的核心价值解耦、简化、规范。让你专注于写业务逻辑(查数据库、处理数据),而不用关心请求是怎么来的、响应是怎么回去的。

  • 它能做什么

    • 自动将请求参数绑定到 Java 对象的属性上。

    • 通过简单的注解(如 @RequestMapping)来定义 URL 和方法的对应关系。

    • 支持多种视图技术(JSP, Freemarker, Velocity, JSON, XML等)。

    • 统一处理异常和拦截器。

  • 它不能做什么

    • 不能直接操作数据库:那是 MyBatis 或 JPA 的事。

    • 不能处理分布式事务:那是分布式事务中间件的事。

    • 不能替代容器:它必须运行在 Servlet 容器(如 Tomcat, Jetty)中。

1.3 技术边界:Spring MVC vs Spring Boot

很多人容易混淆这两个概念。我们通过一个对比表格来清晰定位:

维度 Spring MVC Spring Boot
核心定位 Web MVC 框架(仅解决 Web 层问题) 快速开发脚手架(基于 Spring 生态)
配置方式 需要手动配置(XML 或 JavaConfig) 自动配置(约定优于配置)
部署方式 通常打包成 WAR 包,部署到外部 Tomcat 内嵌 Tomcat/Jetty,打包成 JAR 包直接运行
依赖管理 手动导入各种 JAR 包,需处理版本冲突 提供 Starter 起步依赖,一键引入,版本官方锁定
关系 Spring Boot 底层就是使用 Spring MVC 来处理 Web 层的请求。Spring Boot 只是让配置 Spring MVC 变得更简单了。

一句话总结:Spring MVC 是“发动机”,Spring Boot 是“整车”。我们现在既要学习发动机的原理,也要看看现在整车是怎么整合的。


阶段 2:基础认知——MVC 是个什么鬼?SpringMVC 又是什么?

2.1 什么是 MVC?(去专业化的大白话解释)

MVC 是一种设计模式,它强制性地将应用程序的输入、处理、输出分开。分成三个核心部件:

  • Model(模型)“数据”和“业务规则”

    • 它是应用程序的主体部分。

    • 比如:一个 User 对象(包含 id 和 name),以及怎么从数据库查用户(findAllUsers() 方法)。

  • View(视图)“界面”

    • 就是用户能看到的东西。

    • 比如:用 JSP 写的 HTML 页面,或者接口返回的 JSON 数据。

  • Controller(控制器)“调度员”

    • 它负责接收用户的请求(比如点了“用户列表”按钮),然后决定调用哪个 Model 去处理数据,最后再决定用哪个 View 来展示结果。

生活化的类比:想象你去餐厅吃饭。

  1. 你(客户端):对服务员说:“我要一份红烧肉。”(发出请求)

  2. 服务员(控制器/Controller):收到你的点单,撕下小票递给后厨,并对厨师说:“做一份红烧肉。”(调用模型)

  3. 厨师(模型/Model):根据业务规则(菜谱)开始炒菜,做好了红烧肉(处理数据)。

  4. 服务员(控制器/Controller):把做好的红烧肉端到你的桌子上。

  5. 红烧肉(视图/View):最终呈现在你面前的样子。

在这个过程中,厨师只负责做菜,服务员只负责传话和端菜,你只负责吃。各司其职,互不干扰。即使厨师换了,只要菜的味道不变,服务员和你的工作流程都不受影响。

2.2 SpringMVC 的核心架构组件

SpringMVC 围绕 DispatcherServlet 这个“中央处理器”展开,它就像一个总调度员,手里有一本花名册,知道谁擅长干什么。

  • DispatcherServlet(前端控制器):整个流程的总开关。所有的请求都先打到它这里。

  • HandlerMapping(处理器映射器)“花名册”。DispatcherServlet 问它:“来了个 /user/list 的请求,该找谁处理?” HandlerMapping 翻翻本子,回答说:“找 UserController 里的 listUser() 方法。”

  • HandlerAdapter(处理器适配器)“翻译官”。DispatcherServlet 找到人了,但怎么调用这个方法?方法需要哪些参数?HandlerAdapter 负责去调用目标方法,并帮它准备好参数。

  • Handler(处理器/Controller)“干活的人”。就是你写的那个处理业务逻辑的方法(如 listUser())。它执行完后,会告诉 DispatcherServlet:“事情办妥了,这是要展示的数据(Model),顺便说一句,我想用 userList 这个页面展示。”

  • ViewResolver(视图解析器)“拼图师”。DispatcherServlet 听了 Handler 的话,但不知道 userList 页面在哪。它问 ViewResolver:“userList 页面在哪个文件夹下?” ViewResolver 回答:“在 /WEB-INF/jsp/ 下面,加上 .jsp 后缀,全路径是 /WEB-INF/jsp/userList.jsp。”

  • View(视图):最终的 JSP 或 JSON 数据。

2.3 最小可运行示例:史上最简单的 SpringMVC 应用(XML版)

光说不练假把式,我们来写一个能跑起来的最小 Demo。虽然现在大家都用 Spring Boot 了,但理解最原始的 XML 配置,能帮你更深刻地理解自动配置的好处。

环境:IDEA + Maven + Tomcat 8+

第一步:创建 Maven Web 项目
在 IDEA 里选择 maven-archetype-webapp 骨架创建项目。

第二步:导入依赖 (pom.xml)

<dependencies>
    <!-- Spring MVC 核心依赖 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.3.23</version>
    </dependency>
    
    <!-- Servlet API,Tomcat 自带,但编译时需要 -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <scope>provided</scope>
    </dependency>
    
    <!-- JSP API -->
    <dependency>
        <groupId>javax.servlet.jsp</groupId>
        <artifactId>javax.servlet.jsp-api</artifactId>
        <version>2.3.3</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

第三步:配置 web.xml(注册 DispatcherServlet)
这是将 SpringMVC 接入 Web 容器的入口。

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
  <display-name>Archetype Created Web Application</display-name>

  <!-- 配置 SpringMVC 的前端控制器 DispatcherServlet -->
  <servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!-- 配置 SpringMVC 配置文件的路径和名称,默认在 /WEB-INF/[servlet-name]-servlet.xml -->
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:springmvc.xml</param-value>
    </init-param>
    <!-- 随着服务器启动而加载,数字越小优先级越高 -->
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <!-- 拦截所有请求,注意这里是 / 不是 /*,/* 会拦截jsp -->
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</web-app>

第四步:配置 SpringMVC 的核心配置文件 (resources/springmvc.xml)

<?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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 1. 开启注解扫描,让 @Controller 等注解生效 -->
    <context:component-scan base-package="com.example.controller"/>

    <!-- 2. 配置视图解析器:告诉 SpringMVC 去哪里找页面 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!-- 前缀 -->
        <property name="prefix" value="/WEB-INF/views/"/>
        <!-- 后缀 -->
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>

第五步:写一个 Controller(处理器)
在 com.example.controller 包下创建 HelloController.java

package com.example.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller // 告诉 Spring,这个类是一个控制器
public class HelloController {

    @RequestMapping("/hello") // 当访问 /hello 时,执行这个方法
    public String sayHello(Model model) {
        // 向模型中添加数据,相当于 request.setAttribute("message", "Hello SpringMVC!");
        model.addAttribute("message", "Hello SpringMVC!");
        // 返回视图的逻辑名,视图解析器会拼成 /WEB-INF/views/success.jsp
        return "success";
    }
}

第六步:创建视图页面
在 /src/main/webapp/WEB-INF/views/ 目录下创建 success.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Success</title>
</head>
<body>
    <h1>${message}</h1>
</body>
</html>

第七步:配置 Tomcat 并运行
将项目部署到 Tomcat,启动后访问 http://localhost:8080/你的项目名/hello。如果看到页面显示 "Hello SpringMVC!",恭喜你,你已经亲手搭建了第一个 SpringMVC 应用!

2.4 核心原理极简讲解:请求在 SpringMVC 里到底走了一圈什么路?

结合上面的 Demo,我们来拆解一下当你在浏览器输入 http://localhost:8080/你的项目名/hello 并回车后,发生了什么:

  1. 请求抵达:请求到达 Tomcat 服务器,Tomcat 根据 web.xml 中的配置,发现所有 "/" 的请求都交给 dispatcherServlet 处理,于是请求被转发给 SpringMVC 的 DispatcherServlet

  2. 找 HandlerDispatcherServlet 拿着 /hello 这个 URL,问所有的 HandlerMapping:“谁能处理 /hello?”

  3. HandlerMapping 响应:某个 HandlerMapping(比如 RequestMappingHandlerMapping)通过扫描 @Controller 和 @RequestMapping 注解,发现 HelloController 里的 sayHello 方法上标注着 @RequestMapping("/hello")。于是它返回一个 HandlerExecutionChain(包含了找到的 sayHello 方法和拦截器)。

  4. 找适配器DispatcherServlet 拿到了要执行的方法,但它不知道怎么执行。它又去问 HandlerAdapter:“你能帮我执行这个 sayHello 方法吗?”

  5. 执行 HandlerHandlerAdapter 说“没问题”,然后它负责调用 sayHello 方法。在调用前,它会准备好方法所需的所有参数(比如这里的 Model 对象)。sayHello 方法执行,返回字符串 "success",并将数据 "Hello SpringMVC!" 放到了 Model 里。

  6. 返回 ModelAndViewHandlerAdapter 把执行结果(方法返回的字符串)和 Model 数据(里面的 message)封装成一个 ModelAndView 对象,返回给 DispatcherServlet

  7. 解析视图DispatcherServlet 拿到 ModelAndView,发现里面只有逻辑视图名 "success"。它需要找到真正的页面。于是它问 ViewResolver:“根据配置,"success" 对应的真实路径是啥?”

  8. ViewResolver 响应InternalResourceViewResolver 一看,配置了前缀 /WEB-INF/views/ 和后缀 .jsp,于是拼接出真实路径 /WEB-INF/views/success.jsp,并返回一个 View 对象(比如 InternalResourceView,它封装了对 JSP 的处理逻辑)。

  9. 渲染视图DispatcherServlet 拿着这个 View 对象,把 Model 里的数据(message)交给它。View 对象负责把数据填充到 JSP 页面中(这个过程叫渲染)。

  10. 响应结果:渲染完成后,得到一个完整的 HTML 页面。DispatcherServlet 把这个页面通过 HTTP 响应返回给浏览器,最终你看到了页面上的 "Hello SpringMVC!"。

这就是 SpringMVC 的九大组件协同工作的完整流程! 核心是 DispatcherServlet 这个大脑,协调各个组件完成分工。


阶段 3:核心用法拆解——怎么用?企业高频场景实战

掌握了基本概念和流程,我们来看看在实际开发中,最常用的功能都有哪些。这个阶段我们将按“使用场景”来分类讲解,而不是堆砌API。

3.1 请求处理:如何接收前端传过来的参数?

这是 Web 开发中最基础也最频繁的操作。SpringMVC 提供了极其灵活的参数绑定方式。

场景 1:接收普通键值对参数(GET 表单/POST 表单)

需求:前端传 username=张三&age=18,后端接收并打印。

方法1:直接在方法参数里写同名参数(最常用)

@Controller
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/register")
    @ResponseBody // 直接返回字符串,不进行页面跳转
    public String register(String username, Integer age) {
        // 参数名必须和请求参数名一致,SpringMVC 会自动类型转换(String -> Integer)
        System.out.println("username = " + username + ", age = " + age);
        return "success";
    }
}

测试请求GET /user/register?username=张三&age=18

场景分析

  • 优点:简单直接,适合参数少的场景。

  • 注意点

    • 如果请求参数名和方法参数名不一致,需要用 @RequestParam 指定。

    • 默认参数是必填的,如果没传会报 400 错误。可以通过 @RequestParam(required = false) 设置为非必填。

    • 可以通过 @RequestParam(defaultValue = "0") 设置默认值。

public String register(@RequestParam(value = "name", required = false, defaultValue = "匿名") String username) {
    // 请求参数是 name,但方法参数叫 username,需要映射
}
场景 2:接收一个复杂的对象(POJO)

需求:注册表单有十几个字段,比如用户名、密码、邮箱、电话、地址等。再写十几个方法参数就疯了。

解决方案:直接传一个 User 对象

// User.java
public class User {
    private String username;
    private Integer age;
    private String email;
    // ... getter/setter 必须提供!
}

// Controller
@RequestMapping("/register")
@ResponseBody
public String register(User user) {
    // 只要请求参数名和 User 对象的属性名一致,SpringMVC 会自动 new User() 并赋值
    System.out.println(user.getUsername() + ", " + user.getAge());
    return "success";
}

测试请求GET /user/register?username=李四&age=22&email=lisi@qq.com

场景分析

  • 原理:SpringMVC 调用 User 的无参构造函数创建对象,然后遍历请求参数,通过 setter 方法将同名的参数值注入进去。

  • 必须条件:对象必须提供无参构造方法属性的 setter 方法

场景 3:接收 JSON 数据(前后端分离的主流)

需求:现在前端都是通过 AJAX 发送 JSON 格式的数据,比如 {"username":"王五","age":25,"email":"wangwu@qq.com"}

解决方案:使用 @RequestBody 注解

@RequestMapping("/registerJson")
@ResponseBody
public String registerJson(@RequestBody User user) {
    // @RequestBody 告诉 SpringMVC,从 HTTP 请求体中读取 JSON 字符串,并转换成 User 对象
    System.out.println(user.getUsername());
    return "success";
}

测试请求

  • URLPOST /user/registerJson

  • HeadersContent-Type: application/json

  • Body{"username":"王五","age":25,"email":"wangwu@qq.com"}

场景分析

  • 前置条件:需要引入 JSON 解析库(如 Jackson 或 FastJson),SpringMVC 会自动配置。

  • @RequestBody:一个方法参数只能有一个,读取整个请求体。

  • 适用场景:所有 POST/PUT 请求,传递复杂数据结构时。

场景 4:获取 URL 中的占位符(RESTful 风格)

需求:RESTful 风格中,资源标识通常放在 URL 路径里,比如 GET /user/1 表示获取 ID 为 1 的用户。

解决方案:使用 @PathVariable 注解

@RequestMapping("/user/{id}")
@ResponseBody
public String getUserById(@PathVariable("id") Integer userId) {
    // @PathVariable 用于获取 URL 模板中的变量值
    System.out.println("userId = " + userId);
    return "user detail";
}

测试请求GET /user/123

场景分析

  • {id} 是占位符,@PathVariable("id") 将其绑定到方法参数 userId 上。

  • 如果方法参数名和占位符名一致,可以省略 value,即 @PathVariable Integer id

3.2 数据响应:如何把数据返回给客户端?

场景 1:跳转页面(传统 Web 应用)

这就是我们在 2.3 节 Demo 中演示的方式。

  • 返回字符串:默认情况下,方法返回的字符串会被视图解析器解析为具体的页面路径。

    @RequestMapping("/page")
    public String page(Model model) {
        model.addAttribute("key", "value");
        return "viewName"; // 去 /WEB-INF/views/viewName.jsp
    }
  • 使用 ModelAndView

    @RequestMapping("/page2")
    public ModelAndView page2() {
        ModelAndView mv = new ModelAndView();
        mv.addObject("key", "value"); // 添加数据
        mv.setViewName("viewName");   // 设置视图
        return mv;
    }
  • 转发与重定向

    @RequestMapping("/forwardTest")
    public String forwardTest() {
        // 转发到 /hello 这个请求,浏览器的 URL 不变
        return "forward:/hello";
    }
    
    @RequestMapping("/redirectTest")
    public String redirectTest() {
        // 重定向到 /hello,浏览器的 URL 会变成 /hello
        return "redirect:/hello";
    }
    • 转发(forward):一次请求,在服务器内部完成,可以携带 request 域的数据。

    • 重定向(redirect):两次请求,客户端重新发起请求,request 域数据会丢失。

场景 2:返回 JSON 数据(前后端分离)

这是目前最主流的模式,后端只提供数据接口,不关心页面渲染。

解决方案:@ResponseBody + @RestController

// 方式1:在方法上加 @ResponseBody
@RequestMapping("/getUser")
@ResponseBody
public User getUser() {
    User user = new User();
    user.setUsername("赵六");
    user.setAge(30);
    return user; // 直接返回对象,SpringMVC 会自动将其转为 JSON
}

// 方式2:在类上加 @RestController (组合注解 = @Controller + @ResponseBody)
@RestController // 表示这个类所有的方法都返回 JSON,不再进行页面跳转
@RequestMapping("/api/user")
public class UserApiController {
    
    @GetMapping("/{id}")
    public User getUser(@PathVariable Integer id) {
        // 查询用户...
        return user;
    }
}

场景分析

  • 返回对象或集合时,SpringMVC 会利用 HttpMessageConverter 将其自动转换为 JSON(或 XML)格式。

  • 需要确保对象有 getter 方法,因为 JSON 解析库需要通过 getter 获取属性值。

3.3 核心注解大阅兵

为了方便查阅,这里整理了最核心的注解及其作用:

注解 位置 作用
@Controller 声明此类是一个 Spring MVC 控制器
@RestController @Controller + @ResponseBody 的组合,所有方法返回 JSON
@RequestMapping 类/方法 映射 HTTP 请求到特定的方法或类。可以指定路径、方法、参数等
@GetMapping 方法 @RequestMapping(method = RequestMethod.GET) 的快捷方式
@PostMapping 方法 同上,用于 POST
@PutMapping 方法 同上,用于 PUT
@DeleteMapping 方法 同上,用于 DELETE
@PatchMapping 方法 同上,用于 PATCH
@RequestParam 方法参数 将请求参数绑定到方法参数上
@PathVariable 方法参数 将 URL 路径变量绑定到方法参数上
@RequestBody 方法参数 将 HTTP 请求体(如 JSON)绑定到方法参数上
@ResponseBody 类/方法 将方法返回值直接写入 HTTP 响应体(通常是 JSON)
@RequestHeader 方法参数 将请求头信息绑定到方法参数上
@CookieValue 方法参数 将 Cookie 值绑定到方法参数上
@ModelAttribute 方法/参数 在模型(Model)中添加属性,用于视图渲染

3.4 常见坑点与解决方案

新手在使用 SpringMVC 时,经常会遇到一些“莫名其妙”的问题。我们来排排雷。

坑点 1:静态资源(图片、CSS、JS)被拦截了

问题:你在 webapp/static/css/style.css 放了一个 CSS 文件,访问 http://localhost:8080/static/css/style.css 却返回 404。
原因web.xml 中配置的 DispatcherServlet 拦截了 "/",这意味着它会处理所有的请求,包括对静态资源的请求。但它找不到处理静态资源的 Handler,所以报 404。
解决方案

  1. 方案 A(SpringMVC 3.0+ 推荐):在 springmvc.xml 中配置静态资源放行。

    <!-- 方式1:指定某个目录为静态资源,由 SpringMVC 直接处理 -->
    <mvc:resources mapping="/static/**" location="/static/"/>
    
    <!-- 方式2:将静态资源的处理权交回给默认的 Servlet(如 Tomcat 的 DefaultServlet) -->
    <mvc:default-servlet-handler/>
  2. 方案 B(不推荐):修改 web.xml,将 DispatcherServlet 的 <url-pattern> 改为 *.do 等特定后缀,这样只有以 .do 结尾的请求才会被 SpringMVC 处理。但这不符合 RESTful 风格。

坑点 2:POST 请求中文乱码

问题:表单提交中文数据,后台接收到的是乱码。
原因:POST 请求的参数在请求体中,默认编码不是 UTF-8。
解决方案:在 web.xml 中配置 Spring 提供的 CharacterEncodingFilter 过滤器。

<!-- 在 web.xml 中,放在所有过滤器的最前面 -->
<filter>
    <filter-name>encodingFilter</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>
    <!-- 强制 request 和 response 都使用设置的编码 -->
    <init-param>
        <param-name>forceEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>encodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
坑点 3:GET 请求中文乱码

问题:GET 请求(URL 中的参数)出现乱码。
原因:Tomcat 默认对 URL 的编码是 ISO-8859-1。
解决方案

  1. 修改 Tomcat 的 server.xml(推荐,影响整个 Tomcat):

    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443"
               URIEncoding="UTF-8" /> <!-- 加上这一行 -->
  2. 手动转码(临时解决方案):

    @GetMapping("/test")
    public String test(String name) throws UnsupportedEncodingException {
        // 先按 ISO-8859-1 获取原始字节,再用 UTF-8 组装
        name = new String(name.getBytes("ISO-8859-1"), "UTF-8");
        return name;
    }
坑点 4:@Autowired 注入为 null

问题:在 Controller 中自动注入 Service,结果 Service 是 null,一调用就空指针。
原因:最常见的原因是Controller 对象不是由 Spring 管理的
排查

  1. 检查 Controller 类上是否加了 @Controller 注解。

  2. 检查 Spring 配置文件中是否开启了包扫描 (<context:component-scan base-package="..."/>),并且 Controller 所在的包确实被扫描到了。

  3. 绝对不要手动去 new 一个 Controller!必须让 Spring 容器来创建。

坑点 5:返回 JSON 时报错 406 或 HttpMediaTypeNotAcceptableException

问题:明明想返回 JSON,但报错。
原因:没有引入 JSON 解析库(Jackson 或 FastJson),或者引入的版本不兼容。
解决方案:确保引入了正确的依赖。

<!-- Jackson 核心包 -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.13.0</version>
</dependency>

阶段 4:场景融合——实战中如何与其他技术协作?

单个知识点学得再好,如果不能串联起来,也只是纸上谈兵。我们来看看在真实的企业级业务中,SpringMVC 是如何与它的兄弟们一起工作的。

4.1 业务流程串联:一个典型的“商品详情查询”请求

假设我们正在开发一个电商网站的商品详情页。用户点击一个商品,浏览器发起请求 GET /items/123。整个后端流程可能如下:

代码实现(简化版)

@RestController
@RequestMapping("/items")
public class ItemController {

    @Autowired
    private ItemService itemService; // 注入 Service

    @GetMapping("/{id}")
    public Result<Item> getItem(@PathVariable Long id) {
        // 1. Controller 只负责接收请求和返回响应
        Item item = itemService.findItemById(id);
        return Result.success(item); // 统一响应格式
    }
}

@Service
public class ItemServiceImpl implements ItemService {

    @Autowired
    private ItemMapper itemMapper; // MyBatis 的 Mapper
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate; // Redis 客户端

    @Override
    public Item findItemById(Long id) {
        String key = "item:" + id;
        
        // 2. 先从 Redis 缓存中查
        Item item = (Item) redisTemplate.opsForValue().get(key);
        if (item != null) {
            System.out.println("从缓存中获取商品:" + id);
            return item; // 缓存命中,直接返回
        }
        
        // 3. 缓存未命中,查数据库
        System.out.println("从数据库中获取商品:" + id);
        item = itemMapper.selectById(id);
        
        // 4. 如果数据库查到,放入 Redis 缓存,方便下次查询
        if (item != null) {
            redisTemplate.opsForValue().set(key, item, 1, TimeUnit.HOURS); // 设置过期时间1小时
        }
        return item;
    }
}

这个流程中,SpringMVC 扮演了什么角色?

  • Controller:作为入口和出口,负责参数解析(@PathVariable)和响应封装(@RestController)。

  • Service:业务逻辑的组装者,协调缓存(Redis)和持久层(MyBatis)。

  • 整个流程清晰、分层,SpringMVC 完美地融入了这个生态。

4.2 技术选型对比:什么时候用转发,什么时候用重定向?

虽然现在前后端分离已经是主流,但在一些内部管理系统(Admin)中,依然会用到页面跳转。理解转发和重定向的区别,对于老项目维护很重要。

特性 转发 (forward) 重定向 (redirect)
本质 服务器内部行为 客户端行为
请求次数 1 次请求 2 次请求(服务器返回 302 状态码 + Location 头,浏览器再次请求)
URL 变化 不变,依然是第一次请求的 URL 变,变成重定向后的新 URL
数据共享 可以共享 request 域中的数据 不可以,两次请求是不同的 request 对象
访问资源 只能跳转到当前应用内部的资源 可以跳转到任意外部 URL(如跳转到百度)
性能 较快 较慢(多一次网络往返)
应用场景 查询操作后跳转到展示页;需要携带数据给页面 表单提交(POST)后跳转到查询页面(防止表单重复提交);登录成功后跳转到用户中心

经典场景:POST-Redirect-GET 模式
用户提交一个表单(POST),服务器处理完成后,不要直接返回一个页面(转发),而是返回一个重定向(Redirect)到另一个 URL(GET)。这样,用户刷新页面时,刷新的是 GET 请求,而不会再次提交表单,避免了重复提交的问题。

4.3 异常处理:如何优雅地处理错误?

在一个复杂的系统中,错误不可避免。我们不能让系统直接抛出 500 错误页面给用户,而是应该返回一个友好的提示(如 JSON 格式的 {“code”:500,“message”:“服务器开小差了”})。

SpringMVC 提供了强大的全局异常处理机制。

解决方案:使用 @ControllerAdvice + @ExceptionHandler

// 1. 定义一个统一的响应格式
public class Result<T> {
    private int code;
    private String message;
    private T data;
    // 构造方法、getter/setter 省略
    
    public static <T> Result<T> success(T data) {
        return new Result<>(200, "success", data);
    }
    
    public static <T> Result<T> error(int code, String message) {
        return new Result<>(code, message, null);
    }
}

// 2. 定义业务异常
public class BusinessException extends RuntimeException {
    private int code;
    public BusinessException(int code, String message) {
        super(message);
        this.code = code;
    }
    // getter...
}

// 3. 全局异常处理器(关键)
@ControllerAdvice // 增强所有 Controller
public class GlobalExceptionHandler {

    // 处理自定义的业务异常
    @ExceptionHandler(BusinessException.class)
    @ResponseBody
    public Result<?> handleBusinessException(BusinessException e) {
        // 可以记录日志
        log.error("业务异常:{}", e.getMessage());
        return Result.error(e.getCode(), e.getMessage());
    }

    // 处理参数校验异常(比如使用 Hibernate Validator)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public Result<?> handleValidationException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getAllErrors().stream()
                .map(DefaultMessageSourceResolvable::getDefaultMessage)
                .collect(Collectors.joining(";"));
        return Result.error(400, message);
    }

    // 处理最后的未知异常(兜底)
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Result<?> handleException(Exception e) {
        log.error("系统未知异常", e);
        return Result.error(500, "系统繁忙,请稍后再试");
    }
}

效果:有了这个全局处理器,你的 Controller 就可以写得非常干净,只需要专注业务逻辑,不需要到处写 try-catch。异常会被统一拦截并转换为标准的响应格式。


阶段 5:企业级实战——从零搭建一个“商品管理系统”

理论再多,不如实战一回。我们来搭建一个符合企业开发规范的、麻雀虽小五脏俱全的“简易商品管理系统”。我们将采用 Spring Boot + Spring MVC + MyBatis Plus + Thymeleaf 的技术栈(Thymeleaf 是一种现代化的服务端模板引擎,可以替代 JSP)。

5.1 项目准备与分层规范

1. 项目结构

src/main/java/com/example/demo
├── DemoApplication.java          // Spring Boot 启动类
├── controller                    // 控制器层
│   └── ProductController.java
├── service                       // 业务逻辑层
│   ├── ProductService.java       // 接口
│   └── impl
│       └── ProductServiceImpl.java
├── mapper                        // 数据访问层 (DAO)
│   └── ProductMapper.java
├── model                         // 领域模型
│   ├── entity                    // 实体类 (与数据库对应)
│   │   └── Product.java
│   ├── dto                       // 数据传输对象 (用于接口接收/返回)
│   │   └── ProductQueryDTO.java
│   └── vo                        // 视图对象 (用于页面展示)
│       └── ProductVO.java
├── config                        // 配置类
│   ├── WebMvcConfig.java         // SpringMVC 相关配置
│   └── MybatisPlusConfig.java
└── common                        // 通用工具类
    ├── exception                 // 自定义异常
    ├── result                    // 统一响应结果
    └── constant                  // 常量定义

2. 企业开发规范

  • 分层职责

    • Controller:只做参数解析、调用 Service、返回结果。不写业务逻辑。

    • Service:写具体的业务逻辑(判断、计算、事务控制)。

    • Mapper:定义数据库操作方法。

  • 命名规范

    • 类名:大驼峰(ProductController

    • 方法名:小驼峰(getProductById

    • 常量:全大写加下划线(DEFAULT_PAGE_SIZE

  • 日志规范:使用 SLF4J 的 Logger,关键步骤打 INFO 日志,异常打 ERROR 日志,禁止 System.out.println

5.2 实战核心代码演示

Step 1: 创建实体类 (Entity)

package com.example.demo.model.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;

import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data // 使用 Lombok 简化 getter/setter
@TableName("product") // MyBatis Plus 注解,对应数据库表
public class Product {
    @TableId(type = IdType.AUTO) // 自增主键
    private Long id;
    private String name;
    private String category;
    private BigDecimal price;
    private Integer stock;
    
    @TableField(fill = FieldFill.INSERT) // 自动填充
    private LocalDateTime createTime;
    
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}

Step 2: 创建 Mapper 接口

package com.example.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.model.entity.Product;
import org.apache.ibatis.annotations.Mapper;

@Mapper // 让 MyBatis 扫描到
public interface ProductMapper extends BaseMapper<Product> {
    // BaseMapper 已经提供了基础的 CRUD 方法,不需要再写 SQL
}

Step 3: 创建 Service 层

// 接口
public interface ProductService {
    Product getProductById(Long id);
    boolean createProduct(Product product);
    boolean updateStock(Long id, Integer stock);
}

// 实现类
@Service
@Slf4j // Lombok 日志注解
@Transactional(rollbackFor = Exception.class) // 事务
public class ProductServiceImpl implements ProductService {

    @Autowired
    private ProductMapper productMapper;

    @Override
    public Product getProductById(Long id) {
        log.info("查询商品信息,商品ID:{}", id);
        // 实际项目中这里可能会有缓存逻辑
        Product product = productMapper.selectById(id);
        if (product == null) {
            throw new BusinessException(404, "商品不存在");
        }
        return product;
    }

    @Override
    public boolean createProduct(Product product) {
        log.info("创建商品:{}", product.getName());
        int rows = productMapper.insert(product);
        return rows > 0;
    }

    @Override
    public boolean updateStock(Long id, Integer stock) {
        log.info("更新商品库存,ID:{},库存:{}", id, stock);
        Product product = new Product();
        product.setId(id);
        product.setStock(stock);
        // MyBatis Plus 的动态更新,只更新非空字段
        int rows = productMapper.updateById(product);
        return rows > 0;
    }
}

Step 4: 创建 Controller 层(RESTful API)

package com.example.demo.controller;

import com.example.demo.common.result.Result;
import com.example.demo.model.entity.Product;
import com.example.demo.service.ProductService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid; // 用于参数校验

@RestController
@RequestMapping("/api/products")
@Slf4j
public class ProductController {

    @Autowired
    private ProductService productService;

    @GetMapping("/{id}")
    public Result<Product> getProduct(@PathVariable Long id) {
        Product product = productService.getProductById(id);
        return Result.success(product);
    }

    @PostMapping
    public Result<?> createProduct(@Valid @RequestBody Product product) {
        // @Valid 会触发参数校验,如果校验失败会抛出 MethodArgumentNotValidException,由全局处理器处理
        boolean success = productService.createProduct(product);
        if (success) {
            return Result.success(null);
        } else {
            return Result.error(500, "创建失败");
        }
    }

    @PutMapping("/{id}/stock")
    public Result<?> updateStock(@PathVariable Long id, @RequestParam Integer stock) {
        boolean success = productService.updateStock(id, stock);
        return success ? Result.success(null) : Result.error(500, "更新失败");
    }
}

Step 5: 配置文件 (application.yml)

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印 SQL 日志
  global-config:
    db-config:
      logic-delete-field: deleted # 全局逻辑删除字段

Step 6: 启动类

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

5.3 测试与部署

单元测试 (JUnit5 + Mockito)

@SpringBootTest
@AutoConfigureMockMvc
public class ProductControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testGetProduct() throws Exception {
        mockMvc.perform(get("/api/products/1"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(200))
                .andExpect(jsonPath("$.data.name").value("测试商品"));
    }
}

部署

  • 打包:在项目根目录执行 mvn clean package,生成一个 demo-0.0.1-SNAPSHOT.jar 文件。

  • 运行java -jar demo-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod (指定生产环境配置)

  • 容器化:可以编写 Dockerfile,将这个 JAR 包构建成 Docker 镜像,方便在 K8s 等环境中部署。


阶段 6:复盘升华——从“会用”到“用好”

经过前面的学习和实战,相信你已经对 SpringMVC 有了一定的掌握。但要做到“用好”,甚至应对面试,还需要一些提炼和升华。

6.1 最佳实践总结

  1. 分层明确,拒绝耦合

    • Controller 只做“传话筒”,不要在这里写 SQL 语句或复杂计算。

    • Service 层做好事务边界,@Transactional 注解加在实现类上,而不是接口上。

    • 参数校验用 @Valid + 校验注解(如 @NotNull@Size),不要手动在代码里写一堆 if

  2. 巧用注解,简化开发

    • 新项目一律使用 @RestController 替代 @Controller + @ResponseBody

    • 用 @GetMapping 等组合注解替代 @RequestMapping(method = ...),代码更简洁。

  3. 统一响应与异常处理

    • 定义统一的 Result 类,让前端有一套固定的解析逻辑。

    • 用 @ControllerAdvice 做全局异常处理,这是企业项目的标配,能极大提升代码整洁度。

  4. 谨慎处理数据绑定

    • 对于敏感字段(如用户ID、角色),尽量不要直接接收前端传参,而是从 Session 或 Token 中获取。

    • 使用 POJO 接收参数时,要清楚字段映射规则,避免被恶意赋值。

  5. 静态资源处理

    • 明确区分动态请求和静态资源,务必配置 <mvc:resources/> 或将静态资源放在 CDN。

6.2 技术演进:从 XML 到 Spring Boot 自动配置

回顾我们走过的路,SpringMVC 的配置方式经历了巨大的变迁:

  • Spring 2.x 时代:完全的 XML 配置。一个 springmvc.xml 几百行,全是 <bean>

  • Spring 3.x 时代:引入注解 (@Controller@RequestMapping),但开启注解仍需在 XML 中配置 <context:component-scan/> 和 <mvc:annotation-driven/>

  • Spring 4.x/5.x 时代:Java Config 全面流行。可以用一个 @Configuration 类替代 XML。

    java

    @Configuration
    @EnableWebMvc // 相当于 <mvc:annotation-driven/>
    @ComponentScan("com.example.controller")
    public class WebConfig implements WebMvcConfigurer {
        @Bean
        public InternalResourceViewResolver viewResolver() {
            // 配置视图解析器...
        }
    }
  • Spring Boot 时代:自动配置 + Starter。引入 spring-boot-starter-web,它自动配置了 DispatcherServletInternalResourceViewResolverJackson 等几乎所有东西。你只需要在 application.yml 里写几行配置即可。

Spring Boot 到底自动配置了什么?
它帮我们做了以前手动配置的所有事情:

  1. 自动配置了 DispatcherServlet

  2. 自动配置了 CharacterEncodingFilter(解决乱码)。

  3. 自动配置了 MultipartResolver(文件上传支持)。

  4. 自动配置了 RequestMappingHandlerMapping 和 RequestMappingHandlerAdapter,并自动加载了 JSON 转换器等。

  5. 如果没有自定义 ViewResolver,它会自动配置一个默认的 InternalResourceViewResolver

这就是为什么用 Spring Boot 开发可以如此丝滑的原因。

6.3 面试/工作高频问题解答

Q1:SpringMVC 的执行流程是什么?
A:这是必考题。按照阶段2的流程图,用自己的话把 10 个步骤复述一遍,重点突出 DispatcherServletHandlerMappingHandlerAdapterViewResolver 的作用即可。

Q2:@Autowired 和 @Resource 的区别?
A

  • @Autowired 是 Spring 的注解,按类型 (byType) 注入。如果找到多个类型,会按名称 (byName) 查找。

  • @Resource 是 J2EE 的注解,默认按名称 (byName) 注入,可以通过 name 属性指定。

Q3:SpringMVC 中的 Controller 是单例还是多例?线程安全吗?
A:Controller 是单例的(默认 scope=singleton)。所以它不是线程安全的。因此,绝对不能在 Controller 中定义可变的成员变量。如果必须有,可以考虑使用 @Scope("prototype") 改为多例,但会增加系统开销。最好的做法是保持 Controller 无状态,所有依赖都通过方法参数传递。

Q4:如何解决 POST 请求乱码和 GET 请求乱码?
A

  • POST:配置 CharacterEncodingFilter

  • GET:修改 Tomcat 的 server.xml 配置文件,设置 URIEncoding="UTF-8"

Q5:@RequestBody 和 @ResponseBody 的作用?
A

  • @RequestBody:将 HTTP 请求体(通常是 JSON/XML)反序列化为 Java 对象。

  • @ResponseBody:将 Java 对象序列化为指定格式(通常是 JSON/XML)并写入 HTTP 响应体。

Q6:SpringMVC 中如何实现拦截器?
A:实现 HandlerInterceptor 接口,重写 preHandle(方法执行前)、postHandle(方法执行后视图渲染前)、afterCompletion(视图渲染后)三个方法。然后在配置类中注册,并指定拦截路径。

写在最后

从最初的 Servlet 时代,到 XML 配置的 SpringMVC,再到如今 Spring Boot 的自动装配,技术的演进始终朝着 “提高开发效率、降低维护成本” 的方向前进。SpringMVC 作为 Java Web 开发领域的事实标准,其设计思想(前端控制器、责任链模式、适配器模式等)值得每一位开发者深入学习和体会。

希望通过这篇长达数万字的文章,能帮你构建起一个关于 SpringMVC 的完整知识体系。记住,学习技术不是为了背诵面试题,而是为了理解它解决了什么问题,以及如何优雅地解决问题

如果你坚持看到了这里,恭喜你,你已经在成为“高级程序员”的道路上迈出了一大步。如果在工作中遇到任何关于 SpringMVC 的问题,不妨再翻开这篇文章看看,相信每次你都会有新的收获。

最后,祝你编码愉快,永不加班!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐