【JavaEE13-后端部分】学习SpringMVC之后可能的疑问-彻底搞懂 HTTP 通信:从字节流到对象,前后端数据交换全解析【AI辅助整理】
本文针对SpringMVC初学者的HTTP通信困惑,从网络底层比特流、Content-Type作用讲起,对比前端表单与JSON数据格式,拆解SpringMVC中@RequestBody解析、数据绑定、响应处理的全流程,讲清对象传输、注解使用、请求头设置的底层逻辑,打通HTTP通信全链路。
写在前面
很多初学 SpringMVC 的同学都会被一堆问题困扰:
- 为什么前端发请求有时候用
name=张三&age=18,有时候用{"name":"张三","age":18}? - 为什么后端接收 JSON 时一定要加
@RequestBody,而接收表单格式时可以直接用一个对象接住? - 为什么有时候要手动设置
Content-Type: application/json,有时候浏览器自动就带了? - HTTP 到底传输的是什么?是字符串还是二进制?对象为什么不能直接传?
- 后端收到请求后,到底是怎么把数据变成 Java 对象的?谁在做?什么时候做?
- 后端返回数据时,
Content-Type又是谁设置的?什么时候需要手动?
这些问题如果只得到简化的答案,往往当时懂了,过两天又迷糊。所以这篇文章,我们决定用最啰嗦、最详细的方式,把整个流程从最底层讲到最上层,从物理层到应用层,从浏览器到服务器,从 HTTP 协议到 SpringMVC 源码,力求让你彻底通透。
第一章:网络传输的物理本质——比特流
1.1 计算机世界里只有 0 和 1
在任何物理网络中(无论是网线、光纤还是 Wi-Fi),传输的都是一连串的 电信号或光信号,它们代表的是 比特(bit),也就是 0 和 1。这些比特组合成 字节(byte),1 字节 = 8 比特。所以,任何数据要想在网络上传输,最终都必须被转换成 二进制字节流。
想象一下你有一根水管,你只能往里面倒水(字节),不能直接把一个冰箱(对象)扔进去。这就是网络的物理限制。
1.2 HTTP 协议的作用
HTTP(超文本传输协议)是建立在 TCP/IP 之上的一种 应用层协议。它规定了客户端(如浏览器)和服务器之间通信的格式。HTTP 报文(请求和响应)本质上是一段符合特定语法的文本,但在传输前,这段文本也会被编码成二进制字节流。
举个例子,一个最简单的 HTTP GET 请求报文可能是这样的:
GET /index.html HTTP/1.1
Host: www.example.com
这看起来是文本,但实际上在网络上传输时,每个字符都会被转换成对应的 ASCII 码(比如 G 是 71,E 是 69),形成一连串的字节。
所以,HTTP 传输的底层是二进制字节流,但我们可以按照一定的字符编码(如 UTF-8)把这些字节解读为文本。
1.3 请求体:数据的“集装箱”
对于 POST、PUT 等需要携带数据的请求,数据就放在 请求体(body) 中。请求体也是一个字节流,可以存放任何内容。关键问题是:接收方拿到这一堆字节后,怎么知道如何解读?
这就需要 请求头 来帮忙了。请求头中有一个非常重要的字段:Content-Type。
第二章:Content-Type——数据的说明书
2.1 Content-Type 是什么?
Content-Type 是 HTTP 头部的一个实体头字段,它告诉接收方:请求体中的数据是什么格式,应该用什么方式去解析。常见的值有:
| Content-Type | 说明 |
|---|---|
text/plain |
纯文本,按字符编码解析 |
text/html |
HTML 文档 |
application/json |
JSON 格式的文本 |
application/x-www-form-urlencoded |
表单格式的键值对 |
multipart/form-data |
用于文件上传的混合格式 |
image/jpeg |
JPEG 图片(二进制) |
application/octet-stream |
未知的二进制流 |
2.2 Content-Type 的作用机制
当服务器收到一个 HTTP 请求时,它会先解析请求头,取出 Content-Type 的值。然后,根据这个值,服务器会从内部维护的 解析器列表 中挑选一个合适的解析器来处理请求体。
例如:
- 如果
Content-Type是application/json,服务器就会找一个 JSON 解析器,把请求体中的字节流按照 JSON 语法解析成数据结构。 - 如果
Content-Type是application/x-www-form-urlencoded,服务器就会找一个表单解析器,按&和=拆分成键值对。 - 如果
Content-Type是image/jpeg,服务器就不会尝试把它当作文本,而是直接作为二进制数据保存或处理。
如果没有 Content-Type,或者 Content-Type 与数据实际格式不符,接收方就无法正确解析,可能导致数据丢失或乱码。
第三章:前端的数据格式——表单与 JSON
前端要发送数据给后端,必须选择一种数据格式,并在请求头中用 Content-Type 标明。目前最常用的两种格式是 表单格式 和 JSON 格式。
3.1 表单格式:application/x-www-form-urlencoded
这种格式起源于 HTML 表单。当你在网页上填写一个表单并点击提交时,浏览器默认就会用这种格式把数据发送给服务器。
3.1.1 格式特点
- 数据由 键值对 组成,键和值用
=连接,多个键值对之间用&连接。 - 键和值中的特殊字符(如中文、空格、
&、=等)会被 URL 编码(也叫百分号编码),例如中文“张三”会变成%E5%BC%A0%E4%B8%89。 - 这种格式天生是 扁平的,只能表示简单的键值对,无法直接表达嵌套结构(如对象中包含对象)或数组。
3.1.2 示例
假设一个表单有两个字段:name 和 age,值分别是“张三”和 18。那么编码后的请求体就是:
name=%E5%BC%A0%E4%B8%89&age=18
(如果页面编码是 UTF-8)
3.1.3 前端如何构造表单格式?
-
方式一:传统 HTML 表单提交
<form action="/submit" method="post"> <input name="name" value="张三"> <input name="age" value="18"> <button type="submit">提交</button> </form>当用户点击提交时,浏览器会自动收集所有带有
name属性的表单控件,按照application/x-www-form-urlencoded格式进行 URL 编码,然后设置请求头Content-Type: application/x-www-form-urlencoded,最后将编码后的字符串放入请求体发送。整个过程浏览器全包了,开发者完全不用操心。 -
方式二:JavaScript 手动构造(AJAX)
如果你用fetch或XMLHttpRequest发送请求,就需要自己构造这个字符串,并手动设置Content-Type。例如:const data = new URLSearchParams(); data.append('name', '张三'); data.append('age', 18); fetch('/submit', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: data.toString() // 输出 "name=%E5%BC%A0%E4%B8%89&age=18" });这里用了
URLSearchParams来帮助编码,也可以手动拼接字符串,但要小心转义。
3.2 JSON 格式:application/json
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,现在已经成为 Web API 的主流。
3.2.1 格式特点
- 基于文本,可读性好。
- 支持基本类型(字符串、数字、布尔值、null)、数组、对象嵌套。
- 所有键名必须用双引号括起来(严格 JSON 语法)。
- 几乎所有编程语言都有成熟的 JSON 解析库。
3.2.2 示例
同样的数据用 JSON 表示:
{"name":"张三","age":18}
如果要表示更复杂的数据,比如包含地址对象:
{
"name": "张三",
"age": 18,
"address": {
"city": "北京",
"street": "长安街"
}
}
3.2.3 前端如何构造 JSON 格式?
在 JavaScript 中,用 JSON.stringify() 把对象转成 JSON 字符串,然后手动设置 Content-Type: application/json:
const user = {
name: '张三',
age: 18,
address: {
city: '北京',
street: '长安街'
}
};
fetch('/api/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(user)
});
3.3 为什么需要 JSON?表单格式不够用吗?
表单格式虽然简单,但有明显的局限性:
- 无法表示嵌套结构
比如上面的address对象,表单格式只能通过变通的方式表达,例如address.city=北京&address.street=长安街。但这种方式没有标准,后端解析时需要特殊处理,而且如果嵌套层次更深,会变得非常混乱。 - 所有值都是字符串
表单格式中,所有的值经过 URL 编码后都变成字符串。例如age=18,后端收到的是字符串"18",需要自己转换成数字。而 JSON 原生支持数字类型,后端可以直接反序列化成整数。 - 无法直接表示数组
比如要传递一个爱好列表["读书","运动"],表单格式没有标准表示法,常见的是hobbies[0]=读书&hobbies[1]=运动,但这种方式依赖于后端框架的支持(如 SpringMVC 可以通过@RequestParam List<String> hobbies接收,但需要约定命名规则)。
JSON 则天然支持数组和嵌套,并且与 JavaScript 对象完美契合,所以成为现代 Web 开发的首选。当然,如果数据非常简单,表单格式仍然可用,甚至更轻量。
第四章:后端如何解析请求?SpringMVC 的“魔法”揭秘
现在前端把请求发出去了,请求体里是一串字节,请求头里带着 Content-Type。后端服务器(比如 Tomcat + SpringMVC)是怎么把这个字节流变成 Java 对象的呢?我们一步一步来看。
4.1 请求到达服务器:Tomcat 的初步处理
假设我们使用 Spring Boot 内嵌的 Tomcat 作为 Web 服务器。当 HTTP 请求到达 Tomcat 监听的端口时,Tomcat 会做以下事情:
- 接收 TCP 连接,读取客户端发送来的字节流。
- 解析 HTTP 报文:按照 HTTP 协议规范,将字节流解析成请求行、请求头、请求体。例如,它会找到
Content-Type头,把值取出来。 - 封装成
HttpServletRequest对象:Tomcat 将解析出的信息封装成一个实现了HttpServletRequest接口的对象,这个对象包含所有请求信息,如请求方法、URL、请求头、请求体输入流等。 - 将请求交给 Servlet 容器:对于 Spring Boot 应用,Tomcat 会将请求交给 Spring 的
DispatcherServlet。
DispatcherServlet 是 SpringMVC 的前端控制器,所有请求都会经过它。
4.2 DispatcherServlet 的流程
DispatcherServlet 接收到 HttpServletRequest 对象后,会开始一系列复杂的处理,目的是找到能够处理这个请求的 Controller 方法,并调用它。
关键步骤包括:
- 根据请求 URL 找到对应的 Handler(处理器),也就是我们写的 Controller 类中的某个方法。
- 找到合适的 HandlerAdapter(处理器适配器),用于执行该方法。
- 解析方法参数:在调用方法之前,需要将请求中的数据转换成方法参数所需的 Java 对象。
这里的关键就是 参数解析器(HandlerMethodArgumentResolver)。SpringMVC 内置了很多参数解析器,用于处理不同类型的参数,比如:
@RequestParam对应RequestParamMethodArgumentResolver@RequestBody对应RequestResponseBodyMethodProcessor@PathVariable对应PathVariableMethodArgumentResolver- 等等
4.3 @RequestBody 的工作原理
如果 Controller 方法参数上加了 @RequestBody,比如:
@PostMapping("/user")
public User createUser(@RequestBody User user) { ... }
那么 DispatcherServlet 就会使用 RequestResponseBodyMethodProcessor 这个参数解析器来处理 user 参数。
这个解析器会做以下事情:
4.3.1 获取 Content-Type
从 HttpServletRequest 中调用 getContentType() 方法,拿到请求头中的 Content-Type 值。
4.3.2 寻找合适的 HttpMessageConverter
SpringMVC 内部维护了一个 HttpMessageConverter 列表。每个 HttpMessageConverter 都实现了 canRead() 方法,用于判断自己是否能处理当前请求。判断依据包括:
- 当前请求的
Content-Type是否在它支持的媒体类型列表中。 - 目标类型(比如
User.class)是否可以被它反序列化。
常见的 HttpMessageConverter 有:
| 转换器类 | 支持的媒体类型 | 作用 |
|---|---|---|
MappingJackson2HttpMessageConverter |
application/json |
JSON 与 Java 对象互转 |
StringHttpMessageConverter |
text/plain |
字符串处理 |
FormHttpMessageConverter |
application/x-www-form-urlencoded |
表单数据与 MultiValueMap 互转 |
Jaxb2RootElementHttpMessageConverter |
application/xml、text/xml |
XML 与 Java 对象互转 |
假设请求的 Content-Type 是 application/json,那么 MappingJackson2HttpMessageConverter 的 canRead() 方法会返回 true。
4.3.3 调用 read() 方法
找到合适的转换器后,解析器会调用它的 read() 方法,传入目标类型和请求的输入流(HttpServletRequest.getInputStream())。转换器内部会:
- 从输入流中读取所有字节,得到请求体的原始字节数据。
- 根据字符编码(如 UTF-8)将字节解码成字符串。这个字符编码通常可以从请求头中的
Content-Type里获取,如果没有指定,则使用默认编码(通常是 UTF-8)。 - 按照格式解析字符串:
- 对于 JSON,使用 Jackson 库将字符串解析成 Java 对象(通过反射创建对象,并递归填充属性)。
- 对于表单格式,如果是
FormHttpMessageConverter,它会按&和=拆分成键值对,然后返回一个MultiValueMap<String, String>。但注意,@RequestBody通常不用于表单格式,因为表单格式更常用数据绑定(后面会讲)。
- 返回解析好的 Java 对象。
这个 Java 对象随后被赋值给 Controller 方法的 user 参数。
所以,@RequestBody 的本质是告诉 Spring:“请从请求体中读取数据,并且根据 Content-Type 选择合适的转换器,将请求体内容反序列化成方法参数的对象。”
4.4 不加注解的情况:表单格式的数据绑定
现在考虑另一种情况:前端发送的是表单格式(Content-Type: application/x-www-form-urlencoded),并且后端方法参数是一个 Java 对象,但没有加任何注解:
@PostMapping("/user")
public User createUser(User user) { ... }
这种情况下,Spring 会使用另一种参数解析器:ServletModelAttributeMethodProcessor。这个解析器用于处理 模型属性(即没有注解的普通对象参数)。它的工作方式是:
- 从请求中获取所有 请求参数(注意:请求参数包括 URL 查询参数和表单格式的请求体数据,Spring 把它们统一视为“参数”)。
- 创建一个目标类型的实例(比如
new User())。 - 遍历请求参数的所有键值对,对于每个键(如
name),尝试在目标对象中查找对应的 setter 方法(如setName(String)),并将值转换后通过 setter 注入。 - 这个过程称为 数据绑定,它本质上也是一种反序列化,只不过针对的是简单的键值对,而不是复杂的结构化文本。
注意:数据绑定只能用于表单格式或查询参数,不能用于 JSON。因为 JSON 是一个整体结构,需要专门的解析器;而表单格式是零散的键值对,可以直接映射到对象的属性。
4.5 为什么 JSON 必须用 @RequestBody,而表单格式可以直接用对象?
因为两种数据格式的性质不同:
- JSON 是一个 完整的结构化文本,需要整体解析成树状结构,然后映射到 Java 对象。这个过程必须有一个专门的解析器(如 Jackson)从头到尾处理整个请求体。
@RequestBody就是触发这个机制的开关。 - 表单格式 是一组 零散的键值对,每个键对应对象的一个属性,可以逐个赋值。Spring 可以分别从请求参数中取出每个键的值,然后填充到对象中。这不需要整体解析,所以可以不加注解,Spring 默认用数据绑定处理。
如果你试图用 @RequestBody 接收表单格式,会报错,因为 @RequestBody 期望的是整个请求体是一个整体格式(如 JSON、XML),而表单格式的请求体虽然也是文本,但它不是一种能被 Jackson 直接解析成对象的结构(除非你写一个自定义转换器)。反之,如果你用不加注解的对象接收 JSON,也会失败,因为数据绑定只处理请求参数,而 JSON 数据在请求体中,不是请求参数的一部分。
4.6 小结:后端解析的两种主要方式
| 请求格式 | 后端接收方式 | 解析机制 |
|---|---|---|
application/x-www-form-urlencoded |
对象不加注解,或 @RequestParam |
数据绑定(从请求参数取值) |
application/json |
@RequestBody + 对象 |
消息转换器(从请求体解析) |
第五章:响应时,Content-Type 谁来设置?
Controller 方法处理完业务逻辑后,需要返回结果给客户端。这时同样需要告诉客户端响应体的格式,也就是在响应头中设置 Content-Type。
5.1 自动设置的情况
如果 Controller 类上标注了 @RestController,或者方法上标注了 @ResponseBody,Spring 会使用 HttpMessageConverter 将返回值写入响应体。这个过程与请求解析类似,但方向相反:
- Spring 会根据 返回值的类型 和客户端的
Accept请求头(如果有)来选择合适的HttpMessageConverter。 - 调用转换器的
write()方法,将 Java 对象序列化成指定格式的数据(如 JSON 字符串),并写入响应体的输出流。 - 同时,Spring 会自动设置响应头的
Content-Type,例如对于 JSON 会设为application/json。
默认情况下,开发者不需要手动设置响应头。例如:
@RestController
public class UserController {
@GetMapping("/user")
public User getUser() {
return new User("张三", 18);
}
}
这个接口返回的响应会自动带有 Content-Type: application/json。
5.2 需要手动设置的情况
虽然自动设置能满足大多数需求,但有些场景需要自定义:
- 希望返回 XML 而不是 JSON。
- 希望返回一个自定义的媒体类型,如
application/vnd.api+json。 - 需要强制指定字符编码(如
text/plain;charset=GBK)。
手动设置的方式有几种:
方式一:在 @RequestMapping 中使用 produces 属性
@GetMapping(value = "/user", produces = "application/xml")
public User getUser() {
return user;
}
这样即使返回的是 Java 对象,Spring 也会尝试找支持 XML 的转换器(如 Jaxb2RootElementHttpMessageConverter)来序列化,并设置 Content-Type: application/xml。
方式二:返回 ResponseEntity 并手动设置头
@GetMapping("/user")
public ResponseEntity<User> getUser() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML);
return new ResponseEntity<>(user, headers, HttpStatus.OK);
}
方式三:直接操作 HttpServletResponse
@GetMapping("/user")
public void getUser(HttpServletResponse response) throws IOException {
response.setContentType("application/xml");
// 手动将 user 对象序列化成 XML 并写入 response.getWriter()
}
这种方式最底层,需要自己处理序列化,不常用。
第六章:深入理解——为什么不能直接传递对象?
这是很多初学者的终极疑问:既然编程语言里都有对象,为什么不能直接把对象通过 HTTP 传过去?为什么要转成 JSON 这种字符串?
6.1 对象的内存布局与平台相关性
在编程语言中,对象是存储在内存中的数据结构,包含:
- 实例数据:对象的属性值。
- 类型信息:指向类定义的指针,表明这个对象是什么类型。
- 其他元数据:比如垃圾回收标记、锁信息等(具体取决于语言和虚拟机实现)。
这些内存布局是 与语言和平台紧密相关 的。例如:
- Java 对象在 JVM 中包括对象头、实例数据、对齐填充。
- Python 对象是一个
PyObject结构体,包含引用计数、类型指针等。 - JavaScript 对象在 V8 引擎中可能是由隐藏类(Hidden Class)和属性表组成。
不同的语言、同一语言的不同实现、甚至同一程序的不同运行时刻,对象的内存布局都可能不同。接收方如果直接拿到发送方内存中的字节,根本无法知道如何解读——它不知道哪里是属性值,哪里是类型信息,更不用说处理指针地址了。
6.2 网络传输需要通用格式
网络通信往往是 跨语言、跨平台 的。前端可能用 JavaScript,后端可能用 Java、Python、Go 等。为了让不同语言之间能够交换数据,必须采用一种 与语言无关、与平台无关的中间格式,这种格式被称为 序列化格式。
常见的序列化格式有:
- 文本格式:JSON、XML、YAML
- 二进制格式:Protocol Buffers(protobuf)、MessagePack、Avro
这些格式定义了一套规则,如何将内存中的对象转换成字节序列,以及如何从字节序列还原成对象。发送方负责 序列化,接收方负责 反序列化。
6.3 表单格式也是一种序列化
你可能会问:那表单格式 name=张三&age=18 也是一种序列化吗?
是的!它也是一种序列化格式,只不过它只支持简单的键值对,并且将所有值编码成字符串。它同样满足“与语言无关”的要求,任何语言都能解析这种格式(比如拆分成 Map)。只不过它的表达能力有限,无法表示复杂结构。
所以,无论是 JSON 还是表单格式,都是序列化的具体实现。后端用 @RequestBody 接收 JSON,实际上是在告诉 Spring:“请用 JSON 反序列化器来还原对象”。后端用对象直接接收表单格式,实际上也是在进行反序列化(数据绑定),只不过这种反序列化是基于键值对的简单映射。
第七章:完整流程示例——从用户注册看数据流转
为了把前面所有的知识点串起来,我们来看一个完整的用户注册功能示例。假设前端需要提交包含嵌套地址的用户信息,后端保存后返回完整的用户对象(包含自动生成的 ID)。
7.1 前端代码(JavaScript)
// 构造用户对象
const user = {
name: '张三',
age: 18,
address: {
city: '北京',
street: '长安街'
}
};
// 发送 POST 请求
fetch('/api/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json' // 告诉后端:我发的是 JSON
},
body: JSON.stringify(user) // 序列化:对象 -> JSON 字符串
})
.then(response => response.json()) // 收到响应后,解析 JSON
.then(data => console.log('注册成功', data));
7.2 后端代码(Spring Boot)
@RestController
@RequestMapping("/api")
public class UserController {
@PostMapping("/register")
public User register(@RequestBody User user) {
// 这里 user 已经被反序列化成 Java 对象,包含嵌套的 address
// 假设调用 service 保存用户,并设置生成的 id
user.setId(1001);
return user; // 返回的 user 对象会自动被序列化成 JSON
}
}
// 实体类
public class User {
private Integer id;
private String name;
private Integer age;
private Address address;
// getters and setters...
}
public class Address {
private String city;
private String street;
// getters and setters...
}
7.3 发生了什么?一步步分解
步骤1:前端发送请求
- 浏览器将
user对象通过JSON.stringify()变成 JSON 字符串:{"name":"张三","age":18,"address":{"city":"北京","street":"长安街"}} - 设置请求头
Content-Type: application/json - 将 JSON 字符串按 UTF-8 编码成字节流,放入 HTTP 请求体,通过网络发送给服务器。
步骤2:Tomcat 接收请求
- Tomcat 读取网络字节流,解析 HTTP 报文,提取请求头中的
Content-Type为application/json,将请求体字节流封装成ServletInputStream。 - 将请求信息封装成
HttpServletRequest对象,交给 Spring 的DispatcherServlet。
步骤3:DispatcherServlet 寻找处理器
DispatcherServlet根据 URL/api/register和 POST 方法,找到对应的register方法。- 发现方法参数有
@RequestBody,于是使用RequestResponseBodyMethodProcessor参数解析器。
步骤4:参数解析器处理
- 解析器从请求头获取
Content-Type为application/json。 - 遍历
HttpMessageConverter列表,找到支持application/json且能读取User类型的MappingJackson2HttpMessageConverter。 - 调用该转换器的
read()方法,传入目标类型User.class和请求体的输入流。 - 转换器从输入流中读取所有字节,按 UTF-8 解码成 JSON 字符串。
- 使用 Jackson 库将 JSON 字符串反序列化成
User对象:创建User实例,递归创建Address实例,填充属性。 - 返回
User对象给解析器,解析器将其赋值给方法参数user。
步骤5:Controller 方法执行
register方法被调用,参数user已经是一个完整的 Java 对象,可以直接使用。- 业务逻辑处理后,设置
user.setId(1001),返回user对象。
步骤6:返回值处理
- 由于方法在
@RestController类中,Spring 知道要将返回值写入响应体。 - 使用
RequestResponseBodyMethodProcessor作为返回值处理器,同样会选择合适的HttpMessageConverter。 - 这里还是
MappingJackson2HttpMessageConverter,调用它的write()方法,将user对象序列化成 JSON 字符串。 - 转换器设置响应头
Content-Type: application/json,并将 JSON 字符串按 UTF-8 编码写入响应体的输出流。
步骤7:Tomcat 发送响应
- Tomcat 将响应字节流发送回客户端。
步骤8:前端接收响应
- 浏览器收到字节流,根据响应头
Content-Type: application/json知道是 JSON。 - 调用
response.json()将字节流按 UTF-8 解码成字符串,再用JSON.parse()解析成 JavaScript 对象。 - 最终
data变量得到与后端返回的对象结构一致的数据,比如{ id: 1001, name: '张三', age: 18, address: { city: '北京', street: '长安街' } }。
第八章:关于 Content-Type 的常见疑问
8.1 为什么有时候浏览器自动带 Content-Type,有时候需要手动写?
- 浏览器自动带:发生在 非 AJAX 请求,比如
<form>表单提交。浏览器知道自己在提交表单,所以会自动根据表单的enctype属性(默认application/x-www-form-urlencoded)设置Content-Type,并编码数据。这是浏览器内置的行为。 - 需要手动写:发生在 AJAX 请求(
fetch、axios、XMLHttpRequest)。浏览器不知道你要发送什么数据,它只是把你给的body原样发送,所以你必须手动设置Content-Type告诉服务器这是什么格式。不过有一个例外:如果你传给fetch的body是FormData对象,浏览器会自动设置Content-Type为multipart/form-data并生成正确的 boundary,因为FormData是专门用于表单数据的。
8.2 如果不设置 Content-Type 会怎样?
- 如果请求体为空(如 GET 请求),没有 Content-Type 是正常的。
- 如果请求体有数据,但没有 Content-Type,服务器可能无法正确解析。有些服务器会尝试根据数据内容猜测(比如看到
{猜是 JSON),但这是不安全的,不推荐依赖。最好总是显式设置。
8.3 Content-Type 中的 charset 是什么意思?
例如 Content-Type: application/json;charset=UTF-8,charset 指定了字符编码。它告诉接收方,请求体中的字节是用什么编码转换成文本的。如果没有指定,通常会使用默认编码(如 UTF-8)。但为了准确,建议明确指定。
8.4 响应头中的 Content-Type 如果不设置会怎样?
Spring 会根据返回值的类型自动设置一个默认的,比如返回字符串可能设为 text/plain,返回对象设为 application/json。如果 Spring 找不到合适的默认类型(比如返回 void),可能就不会设置,但这种情况很少见。通常不用操心。
第九章:总结与思考
通过上面啰嗦的讲解,我们终于把 HTTP 通信的整个链条打通了。现在再来回顾最初的问题,是不是都清晰了?
- HTTP 传输的本质:底层是二进制字节流,但通过
Content-Type可以约定这些字节如何被解读。 - 为什么需要 JSON:因为 JSON 能表达复杂数据结构,且与语言无关,是现代 Web API 的主流格式。
- 为什么 JSON 需要 @RequestBody:因为 JSON 是完整的结构化文本,需要专门的转换器整体解析;
@RequestBody就是触发这个机制的开关。 - 为什么表单格式可以直接用对象接收:因为表单格式是零散的键值对,可以通过数据绑定逐个填充到对象属性中,不需要整体解析。
- Content-Type 谁来设置:前端发送时,如果是 AJAX 需要手动设置;如果是表单提交,浏览器自动设置。后端响应时,Spring 会根据返回类型自动设置,也可以手动覆盖。
- 后端如何看到 Content-Type:Tomcat 解析 HTTP 报文,把请求头存入
HttpServletRequest,Spring 从中读取。 - 后端如何解析请求体:通过
HttpMessageConverter链,根据Content-Type选择合适的转换器,将字节流反序列化成 Java 对象。
希望这篇啰嗦到极致的文章能帮助你彻底打通 HTTP 通信的任督二脉。
声明
本文内容来自于询问AI,由作者整理、并优化。
更多推荐


所有评论(0)