*Java 沉淀重走长征路*之——《SpringMVC完全指南:从石器时代到云原生,一套框架的演进之路》
写在前面:这篇文章到底有多干?
各位小伙伴,你好!
如果你正在寻找一篇能让你彻底搞懂SpringMVC的文章,那么恭喜你,找对地方了。这篇文章不是简单的API罗列,也不是枯燥的源码分析,而是一套完整的认知体系。
我们将遵循一套六阶段教学法:
-
问题锚定:先告诉你这技术是解决什么祖坟冒青烟的问题的。
-
基础认知:用大白话讲清楚它到底是什么。
-
核心用法拆解:手把手教你最常用的功能。
-
场景融合:把它放到真实的业务流程里,看它怎么和其他兄弟(Redis、MyBatis)配合。
-
企业级实战:来一个完整的项目,把规范、分层、异常处理都玩明白。
-
复盘升华:总结最佳实践,聊聊面试题,看看技术演进。
全文预计阅读时间较长(但绝对值得),总字数超过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>
痛点分析:这有什么不好?
-
配置爆炸:每写一个功能,就要在
web.xml里配一个<servlet>和<servlet-mapping>。一个稍微大点的项目,web.xml文件能有几千行,维护起来简直是灾难。 -
参数获取极其繁琐:如果想获取表单提交的数据,得用
request.getParameter("username"),然后手动进行类型转换(比如把String转成Integer),还要手动做空值校验。 -
数据共享方式混乱:数据存在
request、session还是application里?全靠程序员自觉,没有统一规范。 -
代码耦合度高:Servlet 里既要接收请求参数,又要调用业务逻辑,还要负责跳转页面,违反了“单一职责原则”。
-
视图技术绑定:通常只能返回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 来展示结果。
-
生活化的类比:想象你去餐厅吃饭。
-
你(客户端):对服务员说:“我要一份红烧肉。”(发出请求)
-
服务员(控制器/Controller):收到你的点单,撕下小票递给后厨,并对厨师说:“做一份红烧肉。”(调用模型)
-
厨师(模型/Model):根据业务规则(菜谱)开始炒菜,做好了红烧肉(处理数据)。
-
服务员(控制器/Controller):把做好的红烧肉端到你的桌子上。
-
红烧肉(视图/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 并回车后,发生了什么:
-
请求抵达:请求到达 Tomcat 服务器,Tomcat 根据
web.xml中的配置,发现所有"/"的请求都交给dispatcherServlet处理,于是请求被转发给 SpringMVC 的DispatcherServlet。 -
找 Handler:
DispatcherServlet拿着/hello这个 URL,问所有的HandlerMapping:“谁能处理/hello?” -
HandlerMapping 响应:某个
HandlerMapping(比如RequestMappingHandlerMapping)通过扫描@Controller和@RequestMapping注解,发现HelloController里的sayHello方法上标注着@RequestMapping("/hello")。于是它返回一个HandlerExecutionChain(包含了找到的sayHello方法和拦截器)。 -
找适配器:
DispatcherServlet拿到了要执行的方法,但它不知道怎么执行。它又去问HandlerAdapter:“你能帮我执行这个sayHello方法吗?” -
执行 Handler:
HandlerAdapter说“没问题”,然后它负责调用sayHello方法。在调用前,它会准备好方法所需的所有参数(比如这里的Model对象)。sayHello方法执行,返回字符串"success",并将数据"Hello SpringMVC!"放到了Model里。 -
返回 ModelAndView:
HandlerAdapter把执行结果(方法返回的字符串)和Model数据(里面的 message)封装成一个ModelAndView对象,返回给DispatcherServlet。 -
解析视图:
DispatcherServlet拿到ModelAndView,发现里面只有逻辑视图名"success"。它需要找到真正的页面。于是它问ViewResolver:“根据配置,"success"对应的真实路径是啥?” -
ViewResolver 响应:
InternalResourceViewResolver一看,配置了前缀/WEB-INF/views/和后缀.jsp,于是拼接出真实路径/WEB-INF/views/success.jsp,并返回一个View对象(比如InternalResourceView,它封装了对 JSP 的处理逻辑)。 -
渲染视图:
DispatcherServlet拿着这个View对象,把Model里的数据(message)交给它。View对象负责把数据填充到 JSP 页面中(这个过程叫渲染)。 -
响应结果:渲染完成后,得到一个完整的 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";
}
测试请求:
-
URL:
POST /user/registerJson -
Headers:
Content-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。
解决方案:
-
方案 A(SpringMVC 3.0+ 推荐):在
springmvc.xml中配置静态资源放行。<!-- 方式1:指定某个目录为静态资源,由 SpringMVC 直接处理 --> <mvc:resources mapping="/static/**" location="/static/"/> <!-- 方式2:将静态资源的处理权交回给默认的 Servlet(如 Tomcat 的 DefaultServlet) --> <mvc:default-servlet-handler/>
-
方案 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。
解决方案:
-
修改 Tomcat 的 server.xml(推荐,影响整个 Tomcat):
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" /> <!-- 加上这一行 --> -
手动转码(临时解决方案):
@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 管理的。
排查:
-
检查
Controller类上是否加了@Controller注解。 -
检查 Spring 配置文件中是否开启了包扫描 (
<context:component-scan base-package="..."/>),并且Controller所在的包确实被扫描到了。 -
绝对不要手动去
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 最佳实践总结
-
分层明确,拒绝耦合
-
Controller 只做“传话筒”,不要在这里写 SQL 语句或复杂计算。
-
Service 层做好事务边界,
@Transactional注解加在实现类上,而不是接口上。 -
参数校验用
@Valid+ 校验注解(如@NotNull、@Size),不要手动在代码里写一堆if。
-
-
巧用注解,简化开发
-
新项目一律使用
@RestController替代@Controller+@ResponseBody。 -
用
@GetMapping等组合注解替代@RequestMapping(method = ...),代码更简洁。
-
-
统一响应与异常处理
-
定义统一的
Result类,让前端有一套固定的解析逻辑。 -
用
@ControllerAdvice做全局异常处理,这是企业项目的标配,能极大提升代码整洁度。
-
-
谨慎处理数据绑定
-
对于敏感字段(如用户ID、角色),尽量不要直接接收前端传参,而是从 Session 或 Token 中获取。
-
使用 POJO 接收参数时,要清楚字段映射规则,避免被恶意赋值。
-
-
静态资源处理
-
明确区分动态请求和静态资源,务必配置
<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,它自动配置了DispatcherServlet、InternalResourceViewResolver、Jackson等几乎所有东西。你只需要在application.yml里写几行配置即可。
Spring Boot 到底自动配置了什么?
它帮我们做了以前手动配置的所有事情:
-
自动配置了
DispatcherServlet。 -
自动配置了
CharacterEncodingFilter(解决乱码)。 -
自动配置了
MultipartResolver(文件上传支持)。 -
自动配置了
RequestMappingHandlerMapping和RequestMappingHandlerAdapter,并自动加载了 JSON 转换器等。 -
如果没有自定义
ViewResolver,它会自动配置一个默认的InternalResourceViewResolver。
这就是为什么用 Spring Boot 开发可以如此丝滑的原因。
6.3 面试/工作高频问题解答
Q1:SpringMVC 的执行流程是什么?
A:这是必考题。按照阶段2的流程图,用自己的话把 10 个步骤复述一遍,重点突出 DispatcherServlet、HandlerMapping、HandlerAdapter、ViewResolver 的作用即可。
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 的问题,不妨再翻开这篇文章看看,相信每次你都会有新的收获。
最后,祝你编码愉快,永不加班!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)