
Spring-Web-MVC
- 1. 什么是 Spring Web MVC?
- 2. 第一个 Spring MVC 程序
- 3. Postman:后端工程师的测试利器
- 4. 请求 —— 如何接收参数
- 5. 获取 Cookie / Session
- 6. 获取 Header
- 7. 响应
- 8. 综合案例实战
- 9. 应用分层:从「一锅粥」到三层架构
- 10. 总结与速查清单
前言:
这里是小谢同学整理的Spring-Web-MVC相关资料。资料用于自我复盘巩固,有错误欢迎大家指出,专栏还有 Java、网络、C 语言等系列笔记欢迎翻阅.同时也希望这篇文章能够帮助到你~
这篇文章怎么读?
Spring MVC 是 Java Web 开发的分水岭。在这之前你写的是本地 Java 程序;在这之后,你的代码可以被全世界通过一个 URL 访问。
全文围绕一句话展开:
学习 Spring MVC,本质上就是学习「浏览器」和「Java 程序」之间怎么交互。 拆开看只有三件事:建立连接、接收请求、返回响应。
👥 适合读者
- 大二大三同学,备战后端实习
- 适合SpringBoot初学者/复习
- Java后端面试复盘,梳理Web层全套考点
📌 需要掌握的前置知识
Java基础、Servlet基础、Maven使用、HTTP协议基础
你可以按两种姿势阅读:
- 顺序读:从头到尾,适合第一次学。
- 复习读:直接跳到文末的「速查清单」和「常见坑」,用表格自查。
1. 什么是 Spring Web MVC?
我们知道了Spring MVC 是Java Web开发的一个分水岭,那到底什么是Spring Web MVC?
Spring Web MVC is the original web framework built on the Servlet API and has been included in the Spring Framework from the very beginning.
翻译过来:Spring Web MVC 是基于 Servlet API 构建的原始 Web 框架,从一开始就包含在 Spring 框架中。正式名称来自其源模块 spring-webmvc,但大家更习惯叫它 Spring MVC。
1.1 什么是 Servlet?
很多同学同学上来就被各种各样的一个专业知识给难到了,在这给可以听听我的一个建议:先用起来,先会使用,再去了解它的底层原理,这样可以有效降低我们学习难度
很多同学一上来就被 Servlet 劝退了,其实一句话就能懂:
Servlet 是一套「Java Web 开发规范/技术标准」。
规范本身什么也干不了,必须要有人去实现它。实现了 Servlet 规范的产品,就叫做Servlet 容器(Web 服务器),开发中常见的有:
| 容器 | 说明 |
|---|---|
| Tomcat | SpringBoot 默认内嵌,我们最常用 |
| Jetty | 轻量级,常用在内嵌场景 |
| WebLogic / WebSphere / JBoss | 老牌商业级 JavaEE 容器 |
这些容器的职责就是:管理你写的 Servlet 类。
我们可以使用一句话来串起来:浏览器请求 → Tomcat(Servlet 容器)→ Spring MVC → 你写的 Controller 方法。
1.2 MVC 到底是个啥?
MVC 是 Model(模型)、View(视图)、Controller(控制器) 的缩写,是软件工程中的一种架构设计模式,把软件系统分成三个基本部分:

| 组成部分 | 职责 | 生活中的角色(饭店版) |
|---|---|---|
| View(视图) | 与浏览器交互、展示数据 | 服务员:接待顾客、帮顾客点餐、给顾客上菜 |
| Controller(控制器) | 分发器:决定请求用哪个模型处理、处理完跳回哪个视图 | 前厅:根据菜单决定给哪个后厨下达命令 |
| Model(模型) | 应用程序主体,处理数据/业务逻辑 | 后厨:真正把饭做出来 |
当然我们也可以这么进行理解:
- HR = View:接待你,最后告诉你面试结果;
- 用人部门 = Controller:根据你的岗位决定由谁来面;
- 面试官 = Model:真正执行「面试」这件事并处理数据。
注意:现实里不一定要严格走这三步。有的饭店前厅直接接待顾客(Controller 接收请求),有的公司面试官直接面你(省去 HR 交接)。设计模式是死的,人是活的。
1.3 Spring MVC 与 MVC、SpringBoot 的关系
面试容易出错的术语:
- MVC 是一种架构设计思想,Spring MVC 是这种思想的具体实现;
- Spring MVC 是一个 Web 框架;
- SpringBoot 是快速脚手架工具,它封装、自动装配了 SpringMVC;SpringBoot 只是简化 SpringMVC 使用的工具
它们的关系,用厨房来比喻:
| 概念 | 比喻 |
|---|---|
| MVC | 做饭的思路 / 流程(思想):洗菜、下锅、上菜,只是一套做事的抽象想法,不是实物厨具 |
| SpringBoot | 厨房(现成房子):容器,底层 Spring (IOC) 地基已经建好,加载不同依赖就获得不同功能 |
| Spring MVC | 燃气灶(厨具 / 框架实物):用来实现 “做饭 ” 的现成工具,本身可以独立使用,不依赖厨房 |
引入 Spring Web 依赖 | 把燃气灶搬进厨房,自动安装调试好,厨房就拥有做饭(Web 请求处理)的能力 |
时间线佐证:Spring 发布于 2004 年,SpringBoot 发布于 2014 年。2014 年之前当然也能用 Spring 实现 MVC 架构,只不过配置比较繁琐(一堆 XML)。SpringBoot 干的事是「
约定大于配置」,把引入 Spring MVC 这一步简化成了勾选一个 Spring Web 模块。
2. 第一个 Spring MVC 程序
创建一个标准的 SpringBoot 项目,创建时勾选 Spring Web 模块即可,这就相当于创建了一个 Spring MVC 项目。
早期纯使用 Spring MVC来 搭建项目需要大量 XML 配置,现在一律推荐 SpringBoot 方式,本篇不再涉及旧方式。
此处我这选择的是Maven

- Name:项目名字,本地文件夹名称,随便起,英文,不要中文空格
- Group:组织 / 公司包名,一般写域名反写,个人项目写自己名字小写,对应 Maven 的
groupId - Artifact: 模块名,Maven 的
artifactId,项目构件 id,一般和 Name 保持一致 - Package name: Java 根包名,由
Group+Artifact拼接生成,代码全部放在这个包下面

进行创建:

此时我们就创建好了第一个spring项目

2.1 第一个 Spring MVC 程序

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/userController")
public class UserController {
@RequestMapping("/sayHi")
public String sayHi() {
return "hello,Spring MVC";
}
}
启动项目,浏览器访问 http://127.0.0.1:8080/userController/sayHi,页面上就显示了 hello,Spring MVC。

📌 方法名和路径名无需一致。是
@RequestMapping里写的路径在起作用,不是方法名。
2.2 @RequestMapping 注解详解
它是 Spring MVC 中最常用的注解之一,作用是「注册接口的路由映射」。
路由映射:当用户访问一个 URL 时,把这个请求对应到程序中某个类的某个方法的过程。
- 既可以修饰类,也可以修饰方法

访问地址:http://127.0.0.1:8080/userController/sayHi
规则:
- 修饰类:设置映射路径的初始信息(前缀),通常用来做模块隔离,比如
/user、/book;- 修饰方法:设置映射路径的具体信息;
- 最终访问路径 = 类路径 + 方法路径。
- 路径前加不加
/都行
@RequestMapping("user")
@RestController
public class UserController {
@RequestMapping("sayHi")
public String sayHi() { return "hello,Spring MVC"; }
}
Spring 启动时会自动判断,前面没加
/就给你拼一个。但规范写法建议统一加/。
- 支持多层路径
@RequestMapping("/user/m1")
@RestController
public class UserController {
@RequestMapping("/say/hi")
public String sayHi() { return "hello,Spring MVC"; }
}
访问:http://127.0.0.1:8080/user/m1/say/hi
@RequestMapping默认同时支持 GET、POST 以及其他所有请求方式。
如果想限定请求方式,可以用
method属性:
@RestController
public class UserController {
// 只接收 POST 请求
@RequestMapping(value = "/getRequest", method = RequestMethod.POST)
public String sayHi() {
return "get request...";
}
}
建议:限定固定的请求方式,减少漏洞、提高安全性。
当然我们也可以使用比较简单的注释来进行限定使用方法:
例如: @PostMapping,@GetMapping,等等,大家也可以去IDEA中进行尝试,此处不过多进行讲解
2.3 为什么必须加 @RestController?
试着把 @RestController 删掉再访问——404,找不到页面。

原因:一个项目里有很多类、很多方法,Spring 凭什么知道要执行哪个?
Spring 会扫描所有的类,只有类上加了 @RestController(或 @Controller)等注解,Spring 才会把它当作控制器,进而去看它里面的方法有没有加 @RequestMapping。
这是 Spring IoC/DI 的典型体现:把对象的创建与管理交给 Spring 容器。
3.Postman:后端工程师的测试利器
写后端每测一个接口就手写一份前端 HTML?太痛苦了。
随着互联网发展,企业按职能拆分出了前端开发工程师和后端开发工程师。后端不必再掌握前端技能,那怎么测自己的接口?——用专业的接口测试工具。
Postman 是最主流的选择:
- 下载地址:https://www.postman.com/downloads
- 核心操作:创建请求 → 选请求方法 → 填 URL → 传参 → Send
Postman 四种传参方式
| 方式 | 说明 | 对应 Content-Type |
|---|---|---|
| 普通传参(Query String) | URL 中 ? 后面的部分,如 ?name=spring | - |
| form-data | 表单提交,可上传图片/文件 | multipart/form-data |
| x-www-form-urlencoded | 普通 form 表单 | application/x-www-form-urlencoded |
| raw | 任意文本:text / json / xml / html | 如 application/json |
📌 类比理解:后端就像餐厅厨师,他不关心用户是到店点、外卖点还是小程序点,只需要知道怎么接收订单、按订单做菜即可。所以我们不必死磕前端如何传参,重点是怎么接。

4. 请求 —— 如何接收参数
这是 Spring MVC 最核心也是最长的一节。下面按照「参数形态」逐个拆解。
后续程序的一个约定:以下所有 Controller 都带类注解 @RequestMapping("/param")。
4.1 传递单个参数
@RestController
@RequestMapping("/param")
public class ParamController {
@RequestMapping("/m1")
public String method1(String name) {
return "接收到参数name:" + name;
}
}
使用postman来设置参数访问:http://127.0.0.1:8080/param/m1?name=spring

结果:

Spring MVC 会根据方法参数名去请求里找同名参数并赋值。 如果名字不一致就拿不到(结果为 null)。
基本类型 vs 包装类型
@RequestMapping("/m1/int")
public Object method1GetInt(int age) {
return "接收到参数age:" + age;
}
如果我这里写int类型,不传的话会出现什么呢?

| 场景 | 结果 |
|---|---|
正常传参 ?age=18 | 200,正常返回 |
不传参 ? 后面啥也没有 | 500 错误! |
类型不匹配 ?age=abc | 400 错误! |

int 类型的参数 ‘age’,虽然为可选的,但由于被声明为基本类型而不能转换为空值。考虑将其声明为对应基本类型的包装类型。
使用基本类型接收参数时,参数必须传(boolean 除外),否则报 500;类型不匹配报 400。
对于参数可能为空的场景,一律使用包装类型(Integer/Long/Boolean…)。 包装类型不传参时值为null,不会报错。
🔧 看日志的习惯比记住结论更重要。 初学时会遇到各种各样的问题,养成「看错误日志 → 按提示改」的习惯,而不是瞎猜。
4.2 传递多个参数
直接用多个形参接即可:
@RequestMapping("/m2")
public Object method2(String name, String password) {
return "接收到参数name:" + name + ", password:" + password;
}
访问:http://127.0.0.1:8080/param/m2?name=zhangsan&password=123456
此处和传递一个参数类似,不过多进行讲解~
4.3 传递对象
参数一多,方法签名就会变成一长串,而且每新增一个参数都要改方法声明。解决办法就是:把参数封装成对象。
@Data//加上Data注解
public class Person {
private int id;
private String name;
private String password;
}
此处给大家介绍一个工具帮我们重写get和set方法
Lombok
我们需要在依赖中进行添加:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

启动程序
访问:http://127.0.0.1:8080/param/m3?id=5&name=zhangsan&password=123456

❤️ Spring 会根据参数名自动绑定到对象的同名属性上。
❤️ 某个属性没传:包装类型为null,基本类型赋默认值(int→0)。
4.4 后端参数重命名:@RequestParam
场景:前端传的 key 是 time,后端想用 createtime 来接。名字不一致,直接接收会拿到 null。这时需要「重命名」。
@RequestMapping("/m4")
public Object method4(@RequestParam("time") String createtime) {
return "接收到参数createtime:" + createtime;
}
访问:http://127.0.0.1:8080/param/m4?time=2023-09-12 → 正常拿到值。

❤️ 注意: 加了 @RequestParam 之后,参数变成「必传」
再进行访问 http://127.0.0.1:8080/param/m4?createtime=2023-09-12 → 400 错误,日志显示:请求参数 'time' 不存在。

为什么会这样? 我们去看一眼源码就明白了:

我们也可以修改为非必传:
@RequestMapping("/m4")
public Object method4(@RequestParam(value = "time", required = false) String createtime) {
return "接收到参数createtime:" + createtime;
}

📌 注解语法小贴士:只赋一个值时,默认赋给
value属性,所以@RequestParam("time")能简写;我们一旦要赋多个属性(value+required),就必须显式写value = "time"。
4.5 传递数组
同一个参数名传多次,后端用数组接:
@RequestMapping("/m5")
public String method5(String[] arrayParam) {
return Arrays.toString(arrayParam);
}
以下三种写法等价,都能正常接收:
方式一:?arrayParam=zhangsan&arrayParam=lisi&arrayParam=wangwu
方式二:?arrayParam=zhangsan%2clisi%2cwangwu (%2c 是逗号的 URL 编码)
方式三:?arrayParam=zhangsan,lisi,wangwu

4.6 传递集合
和数组类似,但必须加 @RequestParam 绑定参数关系(否则 Spring 不知道要不要塞进集合):
@RequestMapping("/m6")
public String method6(@RequestParam List<String> listParam) {
return "size:" + listParam.size() + ",listParam:" + listParam;
}
访问:http://127.0.0.1:8080/param/m6?listParam=zhangsan&listParam=lisi&listParam=wangwu

❤️ 数组 vs 集合:数组可以不加
@RequestParam;集合必须加。
4.7 传递 JSON:@RequestBody
我们先来搞清楚 JSON 是什么?
JSON(JavaScript Object Notation) 是一种轻量级的数据交互格式。
❤️ 它和 JavaScript 没有关系,只是语法相似(所以 JS 开发者上手快)。JSON 的本质是一个字符串,就像:
- 国际通用语言 → 英语
- 中国 56 个民族的通用语言 → 普通话
有自己的语法,并且其他语言也认识。
JSON 语法格式:
{
"squadName": "Super hero squad",
"homeTown": "Metro City",
"formed": 2016,
"active": true,
"members": [
{
"name": "Molecule Man",
"age": 29,
"powers": ["Radiation resistance", "Turning tiny"]
}
]
}
五条语法规则:
- 数据在键值对(Key/Value) 中;
- 数据由逗号
,分隔; - 对象用
{}表示(无序键值对集合); - 数组用
[]表示(有序值集合); - 值可以是对象,也可以是数组,二者可互相嵌套。
写 JSON 时用在线工具校验:在线 JSON 校验格式化工具
JSON 与 Java 对象互转
Spring MVC 已内置 Jackson 来完成这件事。本质上是
jackson-databind提供的功能,Spring MVC 帮我们引了依赖。如果脱离 Spring MVC 使用,需要自己引:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.5</version>
</dependency>
public class JSONUtils {
private static ObjectMapper objectMapper = new ObjectMapper();
public static void main(String[] args) throws JsonProcessingException {
Person person = new Person();
person.setId(5);
person.setName("zhangsan");
person.setPassword("123456");
// 对象 → JSON 字符串
String jsonStr = objectMapper.writeValueAsString(person);
System.out.println("JSON字符串为:" + jsonStr);
// JSON 字符串 → 对象
Person p = objectMapper.readValue(jsonStr, Person.class);
System.out.println("转换的对象id:" + p.getId() + ",name:" + p.getName());
}
}

| 方法 | 作用 |
|---|---|
writeValueAsString(obj) | 对象 → JSON 字符串 |
readValue(jsonStr, Xxx.class) | JSON 字符串 → 对象 |
后端接收 JSON
接收 JSON 必须加 @RequestBody
@RequestMapping(value = "/m7")
public Object method7(@RequestBody Person person) {
return person.toString();
}
RequestBody = 请求正文。 这个注解表示:把请求正文(body)中的数据绑定到方法参数上,也就是说参数必须写在请求体中。
❤️ 注意:如果把@RequestBody去掉,即使 Postman 发了 JSON,后端也无法给对象赋值,拿到的是空属性对象。原因:默认的参数绑定只认 query/form 参数,不认 body。

4.8 获取 URL 中的参数:@PathVariable
Path Variable = 路径变量,作用在请求 URL 路径上的数据绑定。
@RequestMapping("/m8/{id}/{name}")
public String method8(@PathVariable Integer id, @PathVariable("name") String userName) {
return "解析参数id:" + id + ",name:" + userName;
}
访问:http://127.0.0.1:8080/param/m8/5/zhangsan

规则:
- URL 中的变量用
{xxx}占位; - 方法参数名与变量名一致时,可以简写,不用给
@PathVariable赋值(如上例的id); - 不一致时,必须显式指定
@PathVariable("name")(如上例的userName)。
📌
@PathVariable是 RESTful 风格接口的基础,比如/book/1表示查询 id 为 1 的图书。
4.9 上传文件:@RequestPart
@RequestMapping("/m9")
public String getfile(@RequestPart("file") MultipartFile file) throws IOException {
// 获取文件名称
String fileName = file.getOriginalFilename();
// 文件上传到指定路径
file.transferTo(new File("D:/temp/" + file.getOriginalFilename()));
return "接收到文件名称为: " + fileName;
}
Postman 中选 form-data,key 类型选 File,选择文件上传即可,然后去 D:/temp 目录查看是否成功。

此时我电脑中对应的文件中为空的:

进行发送之后:

5. 获取 Cookie / Session
HTTP 协议本身是「无状态」协议。 客户端和服务器之间的这次通信,和下次通信之间没有直接联系。
但实际开发中「我很需要这个联系」:登录网站成功后,第二次访问服务器得知道你已经登录过了。
Cookie:客户端的令牌
拿医院挂号举例:
- 挂号时需要身份证,同时得到一张**「就诊卡」= 令牌**;
- 后续去各科检查、开药,不必再出示身份证,凭就诊卡就能识别身份;
- 看完病注销就诊卡,身份关联就销毁了(类似网站注销);
- 下次来看病办张新卡,就是新的令牌。
❤️ Cookie 是可以伪造的,也就是不安全的,所以使用 Cookie 时后端必须做校验,不能拿 Cookie 值直接用。
Session:服务器端的哈希表
先理解「会话(Session)」:
会话是客户端与服务器之间不中断的请求响应。 就像打客服电话:拨通 = 会话开始,挂断 = 会话结束,长时间不说话会话也会超时结束。
服务器同一时刻可能会收到成千上万的请求,这时候服务器就需要区分每个请求属于哪个用户。
Session 是服务器为了保存用户信息而创建的一个特殊对象。
它的本质上是一个哈希表:
- Key = SessionId(服务器生成的唯一字符串,也可称token)【就是靠这个SessionId区分不同用户】;
- Value = 用户信息(结构灵活自定义)。
工作流程:
- 登录成功,服务端生成SessionId,把SessionId通过Set‑Cookie下发浏览器
- 浏览器后续每一次请求自动在Cookie带上SessionId
- 服务器拿到本次请求Cookie里面的SessionId,作为hash表key,取出对应用户数据,完成「识别是哪个用户」
Cookie 和 Session 的区别
| 维度 | Cookie | Session |
|---|---|---|
| 存储位置 | 客户端 | 服务器端 |
| 安全性 | 较低,可伪造 | 较高 |
| 容量 | 有限(几 KB) | 理论上无限制(但占服务器内存) |
| 关联方式 | 通过 SessionId 关联,SessionId 是二者的桥梁 | 同左 |
📌 二者经常配合使用,但不是必须配合:Cookie 也可以单独存数据;SessionId 也不一定非得通过 Cookie 传递(也可以通过 URL)。
5.1 传统方式获取 Cookie
@RequestMapping("/m10")
public String method10(HttpServletRequest request, HttpServletResponse response) {
Cookie[] cookies = request.getCookies();
StringBuilder builder = new StringBuilder();
if (cookies != null) {
for (Cookie ck : cookies) {
builder.append(ck.getName() + ":" + ck.getValue());
}
}
return "Cookie信息:" + builder;
}
❤️ Spring MVC 内置对象:因为是基于 Servlet API 构建的,
HttpServletRequest/HttpServletResponse是 Spring MVC 方法的内置对象,需要时直接在方法参数里声明即可。
HttpServletRequest:封装了客户端请求的所有信息(请求行、请求头、请求正文);HttpServletResponse:封装了服务器响应的所有内容(响应状态码、响应头、响应正文)。
5.2 简洁获取 Cookie:@CookieValue
@RequestMapping("/getCookie")
public String cookie(@CookieValue("bite") String bite) {
return "bite:" + bite;
}
5.3 Session 存储与读取
// 1. 存储
@RequestMapping("/setSess")
public String setsess(HttpServletRequest request) {
HttpSession session = request.getSession(); // 无会话则自动创建
if (session != null) {
session.setAttribute("username", "java");
}
return "session 存储成功";
}
// 2. 读取
@RequestMapping("/getSess")
public String sess(HttpServletRequest request) {
HttpSession session = request.getSession(false); // 不存在则不创建,返回 null
String username = null;
if (session != null && session.getAttribute("username") != null) {
username = (String) session.getAttribute("username");
}
return "username:" + username;
}
| API | 说明 |
|---|---|
getSession() | 等价于 getSession(true):不存在会话时新建 |
getSession(false) | 不存在会话时返回 null,不新建(查询场景推荐) |
setAttribute(name, value) | 绑定一个对象到该会话 |
getAttribute(name) | 取出指定名称的对象,没有则返回 null |
❤️
getSession()内部会提取请求 Cookie 里的 SessionId,再根据 SessionId 找到对应的 Session 对象(用HttpSession描述)。所以代码里你看不到 SessionId,它在幕后。
5.4 另外两种简洁获取 Session 的方式
// 方式一:@SessionAttribute
@RequestMapping("/getSess2")
public String sess2(@SessionAttribute(value = "username", required = false) String username) {
return "username:" + username;
}
// 方式二:直接用内置对象 HttpSession
@RequestMapping("/getSess3")
public String sess3(HttpSession session) {
String username = (String) session.getAttribute("username");
return "username:" + username;
}
❤️区别:
@SessionAttribute不会自动创建 Session;而方法参数里写HttpSession,如果 Session 不存在会自动创建。
6. 获取 Header
// 传统方式
@RequestMapping("/param10")
public String param10(HttpServletRequest request) {
String userAgent = request.getHeader("User-Agent");
return "userAgent:" + userAgent;
}
// 简洁方式
@RequestMapping("/header")
public String header(@RequestHeader("User-Agent") String userAgent) {
return "userAgent:" + userAgent;
}
@RequestHeader的参数值就是 HTTP 请求报头中的 Key。
7. 响应
HTTP 响应可以是数据,也可以是静态页面,还可以设置状态码和 Header。
7.1 返回静态页面
准备 static/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Index页面</title>
</head>
<body>
Hello,Spring MVC,我是Index页面.
</body>
</html>
注意:此处的不是
RestController而是Controller
@Controller
public class IndexController {
@RequestMapping("/index")
public Object index() {
return "/index.html";
}
}

核心结论:
@RestController=@Controller+@ResponseBody
| 注解 | 含义 | 默认行为 |
|---|---|---|
@Controller | 定义一个控制器,启动时由 Spring 加载并管理该对象 | 返回视图(页面) |
@ResponseBody | 定义返回的数据格式为非视图 | 返回 text/html 数据 |
@RestController | 上面二者合体 | 返回数据 |
📌 时代背景:早期 MVC 中后端返回视图;现在流行前后端分离,Java 只做后端,View 不再返回视图,而是返回显示视图所需的数据。
7.2 返回数据:@ResponseBody
@ResponseBody 既是类注解,又是方法注解:
- 作用在类上 → 该类所有方法都返回数据;
- 作用在方法上 → 只有该方法返回数据。
混用场景(一个类里既有返回页面的又有返回数据的):
@Controller
public class IndexController {
@RequestMapping("/index")
public Object index() {
return "/index.html"; // 返回视图
}
@RequestMapping("/returnData")
@ResponseBody
public String returnData() {
return "该方法返回数据"; // 返回数据
}
}
❤️ 注意:如果去掉上面
/returnData的@ResponseBody,程序会报 404——因为 Spring 会把它当成「要返回一个视图」,按/该方法返回数据去找文件,找不到。📌 多个注解没有先后顺序,先写哪个都行。
7.3 返回 HTML 代码片段
@RequestMapping("/returnHtml")
@ResponseBody
public String returnHtml() {
return "<h1>Hello,HTML~</h1>";
}
浏览器会解析 HTML 标签,渲染出大标题。此时 Content-Type 为 text/html。

常见 Content-Type:
| Content-Type | 含义 |
|---|---|
text/html | body 是 HTML |
text/css | body 是 CSS |
application/javascript | body 是 JavaScript |
application/json | body 是 JSON |
Spring MVC 会根据请求的文件类型自动设置 Content-Type(请求 js 文件 →
application/javascript,css →text/css)。
7.4 返回 JSON
后端方法返回对象/集合时,Spring MVC 会自动序列化成 JSON。
@RequestMapping("/returnJson")
@ResponseBody
public HashMap<String, String> returnJson() {
HashMap<String, String> map = new HashMap<>();
map.put("Java", "Java Value");
map.put("MySQL", "MySQL Value");
map.put("Redis", "Redis Value");
return map;
}
浏览器结果:

7.5 设置状态码
状态码 Spring MVC 会自动设置,也可以手动指定:通过内置对象 HttpServletResponse。
@RequestMapping(value = "/setStatus")
@ResponseBody
public String setStatus(HttpServletResponse response) {
response.setStatus(401); // 401 = 未授权
return "设置状态码成功";
}
📌 状态码不影响页面展示,页面照样显示"设置状态码成功",只是 Fiddler/浏览器 Network 里看到状态码变了。
7.6 设置 Header
我们先来完整看一眼 @RequestMapping 的源码的六个属性:
先完整看一眼 @RequestMapping 的源码,六个属性:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
String name() default "";
@AliasFor("path") String[] value() default {};
@AliasFor("value") String[] path() default {};
RequestMethod[] method() default {};
String[] params() default {};
String[] headers() default {};
String[] consumes() default {};
String[] produces() default {};
}
| 属性 | 说明 |
|---|---|
value | 指定映射的 URL |
method | 指定请求类型 GET / POST 等 |
consumes | 限定请求的提交内容类型(Content-Type) |
produces | 指定返回的内容类型,可同时指定编码 |
params | 规定 request 中必须包含某些参数值才处理 |
headers | 规定 request 中必须包含某些 header 才处理 |
// 只设置类型
@RequestMapping(value = "/returnJson2", produces = "application/json")
@ResponseBody
public String returnJson2() {
return "{\"success\":true}";
}
这里不做具体演示,可以私底下进行动手试验:
8. 综合案例实战
学完上面就可以动手做小案例了。核心目标就是要:理解前后端交互过程、接口传参定义、数据返回与页面展示。
8.1 加法计算器
需求:输入两个整数,点击「点击相加」按钮,显示计算结果。
接口定义
请求路径:calc/sum
请求方式:GET/POST
接口描述:计算两个整数相加
请求参数:
| 参数名 | 类型 | 是否必须 | 备注 |
|---|---|---|---|
| num1 | Integer | 是 | 参与计算的第一个数 |
| num2 | Integer | 是 | 参与计算的第二个数 |
示例:num1=5&num2=3 → 响应:计算机计算结果: 8(Content-Type: text/html)
前端页面
<form action="calc/sum" method="post">
<h1>计算器</h1>
数字1:<input name="num1" type="text"><br>
数字2:<input name="num2" type="text"><br>
<input type="submit" value=" 点击相加">
</form>
服务端代码
@RestController
@RequestMapping("/calc")
public class CalcController {
@RequestMapping("/sum")
public String sum(Integer num1, Integer num2) {
Integer sum = num1 + num2;
return "<h1>计算机计算结果: " + sum + "</h1>";
}
}
📌 注意这里用
Integer而不是int:万一用户没填,Integer会得到 null 而不是直接 500(后续应该做参数校验)。
8.2 案例二:用户登录
需求:输入账号密码,后端校验。不正确则前端提示;正确则跳转首页并显示当前登录用户;后续再访问首页仍能拿到登录用户信息。
接口定义
① 校验接口
请求路径:/user/login
请求方式:POST
接口描述:校验账号密码是否正确
| 参数名 | 类型 | 是否必须 | 备注 |
|---|---|---|---|
| userName | String | 是 | 校验的账号 |
| password | String | 是 | 校验的密码 |
响应:true(成功)/ false(失败)
② 查询登录用户接口
请求路径:/user/getLoginUser
请求方式:GET
接口描述:查询当前登录的用户
请求参数:无。响应:当前登录用户名,未登录返回空字符串。
8.2.1 简易登录界面前端
登录页 login.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h2>用户登录</h2>
<form onsubmit="login(); return false;">
账号: <input type="text" id="userName" name="userName" /><br/><br/>
密码: <input type="password" id="password" name="password" /><br/><br/>
<button type="submit">登录</button>
</form>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<script>
function login() {
$.ajax({
type: "post",
url: "/user/login",
data: {
"userName": $("#userName").val(),
"password": $("#password").val()
},
success: function (result) {
if (result) {
location.href = "/index.html";
} else {
alert("账号或密码有误.");
}
}
});
}
</script>
</body>
</html>
页面跳转的三种方式:
window.location.href = "book_list.html"; // 常用
window.location.assign("book_list.html");
window.location.replace("book_list.html"); // 无历史记录,不能后退
通常省略
window.,直接写location.href = "book_list.html";
前端代码(首页 index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Index页面</title>
</head>
<body>
<h2>欢迎 <span id="loginUser">加载中...</span></h2>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<script>
$.ajax({
type: "get",
url: "/user/getLoginUser",
success: function (result) {
$("#loginUser").text(result);
},
error: function () {
$("#loginUser").text("未登录或接口不存在");
}
});
</script>
</body>
</html>
❤️ 重要现象:多次刷新
http://127.0.0.1:8080/index.html依然能拿到登录用户;一旦重启服务器,登录人显示为空——因为 Session 存在内存中。
8.2.2 简易登录界面后端
import jakarta.servlet.http.HttpSession;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class LoginController {
/**
* 校验用户账号和密码是否正确
*/
@RequestMapping("/login")
public boolean login(String userName, String password, HttpSession session) {
// 账号或密码为空
if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {
return false;
}
// 校验账号密码是否正确
// 理论上应该从数据库中获取账号密码校验,这里先写死
if (!"zhangsan".equals(userName) || !"123456".equals(password)) {
return false;
}
// 密码验证成功,把用户名存储在 Session 中
session.setAttribute("userName", userName);
return true;
}
/**
* 返回登录用户信息
*/
@RequestMapping("/getLoginUser")
public String getLoginUser(HttpSession session) {
String userName = (String) session.getAttribute("userName");
if (StringUtils.hasLength(userName)) {
return userName;
}
return "";
}
}
StringUtils.hasLength() 是 Spring 提供的工具方法,判断字符串是否有值:
public static boolean hasLength(@Nullable String str) {
return str != null && !str.isEmpty();
}


9. 应用分层:从「一锅粥」到三层架构
做完上面的案例你会发现:代码有点"杂乱",而这还只是"一点点功能"的开发。如果把整个项目做完,代码会更加"杂乱无章"——文件乱、代码内容也乱。
类比公司的组织架构:
公司初创时,一个人身兼数职:既做财务,又做人事,还做行政。随着公司壮大,会划分出财务部、人事部、行政部,部门内部还会再细分。
项目开发一模一样:功能简单时前后端一起写 → 项目复杂后拆成前后端团队 → 后端内部还会继续细分。
应用分层是一种软件开发设计思想,它将应用程序分成 N 个层次,各层分别负责各自的职责,多层协同提供完整的功能。
根据项目复杂度,可以分三层、四层甚至更多。常见的 MVC 设计模式,就是应用分层的一种具体体现。
为什么必须分层? 不分层时大量代码混在一起,会出现:逻辑不清晰、各模块相互依赖、代码扩展性差、改动一处而牵一发而动全身。
三层架构:
现在主流是前后端分离,后端工程师不再关注前端,于是 Java 后端有了新的分层方式——三层架构:
| 层 | 职责 | Spring 中的体现 |
|---|---|---|
| 表现层 | 展示数据结果、接收用户指令,最靠近用户的一层 | Controller(控制层):接收请求、处理请求、响应数据 |
| 业务逻辑层 | 负责处理业务逻辑,复杂业务的具体实现 | Service:处理具体的业务逻辑 |
| 数据层 | 负责存储和管理与应用程序相关的数据 | Dao(持久层):数据的增删改查 |
对应到 Spring MVC 的请求处理流程:
浏览器请求
↓
Controller(控制层) —— 接收请求、响应数据
↓
Service(业务层) —— 处理业务逻辑
↓
Dao(数据层) —— 增删改查数据
↓
数据库
分层之后:可以有效降低代码的耦合度,现在基本上不讲高内聚,而是需要
代码低耦合
| 概念 | 含义 | 类比 |
|---|---|---|
| 高内聚 | 一个模块内部各元素之间联系紧密程度越高越好 | 一个部门的员工关系要紧密,遇到问题一起解决克服(一个家庭成员生病,其他成员帮忙照顾) |
| 低耦合 | 模块与模块之间的依赖关联程度越低越好 | 各部门之间关联要尽可能小,一个部门出问题尽量不影响其他部门(楼上漏水楼下遭殃 = 耦合) |
分层好处:
- 降低层与层之间的依赖,结构更加明确,利于各层逻辑的复用;
- 开发人员可以只关注整个结构中的某一层,极大地降低维护成本和维护时间;
- 可以很容易地用新的实现来替换原有层次的实现;
- 有利于标准化。
10. 总结与速查清单
注解速查表
学 Spring MVC,本质上就是学习 Web 开发需要的各种注解:
| 注解 | 作用 | 位置 |
|---|---|---|
@RequestMapping | 路由映射,注册 URL 到方法 | 类 / 方法 |
@RequestParam | 后端参数重命名(也可用非必传、默认值) | 参数 |
@RequestBody | 接收 JSON 类型的参数(从请求正文绑定) | 参数 |
@PathVariable | 接收 URL 路径中的参数 | 参数 |
@RequestPart | 上传文件 | 参数 |
@ResponseBody | 返回数据(而非视图) | 类 / 方法 |
@CookieValue | 从 Cookie 中获取值 | 参数 |
@SessionAttribute | 从 Session 中获取值 | 参数 |
@RequestHeader | 从 Header 中获取值 | 参数 |
@Controller | 定义控制器,交给 Spring 管理,默认返回视图 | 类 |
@RestController | @ResponseBody + @Controller,返回数据 | 类 |
状态码与常见坑
| 现象 | 原因 | 解决 |
|---|---|---|
| 404 | ①类上没加 @Controller/@RestController;②返回视图时 @ResponseBody 没去掉 | 检查注解 |
| 400 | ①参数类型不匹配(?age=abc);②@RequestParam 必传参数没传 | 检查参数类型与必传性 |
| 500 | 基本类型参数没传值(int age 无参) | 改用包装类型 Integer |
@RequestBody 收不到值 | 前端没用 application/json,或 JSON key 与属性名不一致 | 用 Postman 选 raw-JSON |
| Cookie 拿到 null | 浏览器还没设置过 Cookie | 先请求 set 接口或用浏览器手动设置 |
| 重启后登录态/留言数据没了 | Session 和 List 都存在内存里 | 数据需要持久化到数据库 |
核心三句话:
- Spring MVC 是一个「实现了 MVC 模式的 Web 框架」,SpringBoot 只是实现它的一种方式(勾选 Spring Web 依赖即可)。
- 掌握 Spring MVC = 掌握三件事:建立连接(
@RequestMapping)、获取请求参数、返回响应。 - Cookie 是客户端机制,Session 是服务端机制,二者通过 SessionId 关联。 Spring MVC 内置
HttpServletRequest、HttpServletResponse两个对象,需要时在方法参数中直接声明即可;Cookie/Session 可以从 Request 中获取,也可以用 Response 设置响应状态码。
效率工具清单:
- Postman:接口测试,后端必备
- Lombok:
@Data消除样板代码 - EditStarter:图形化快速添加 SpringBoot 依赖
- Fiddler:抓包观察 HTTP 请求/响应
Spring MVC 的知识点非常"碎",但本质上就是在回答「浏览器的数据怎么进来」「程序的结果怎么出去」这两件事。

转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2502_93799892/article/details/166945785


![[JavaEE进阶] Spring Web MVC 快速入门全总结:从 MVC 思想到手写两个实战案例封面图](https://i-blog.csdnimg.cn/direct/5ca92e53cbcd4f2e886b925671c521da.png?x-oss-process=image/resize,m_fixed,h_300)

