写在前面

很多初学 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-Typeapplication/json,服务器就会找一个 JSON 解析器,把请求体中的字节流按照 JSON 语法解析成数据结构。
  • 如果 Content-Typeapplication/x-www-form-urlencoded,服务器就会找一个表单解析器,按 &= 拆分成键值对。
  • 如果 Content-Typeimage/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 示例

假设一个表单有两个字段:nameage,值分别是“张三”和 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)
    如果你用 fetchXMLHttpRequest 发送请求,就需要自己构造这个字符串,并手动设置 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?表单格式不够用吗?

表单格式虽然简单,但有明显的局限性:

  1. 无法表示嵌套结构
    比如上面的 address 对象,表单格式只能通过变通的方式表达,例如 address.city=北京&address.street=长安街。但这种方式没有标准,后端解析时需要特殊处理,而且如果嵌套层次更深,会变得非常混乱。
  2. 所有值都是字符串
    表单格式中,所有的值经过 URL 编码后都变成字符串。例如 age=18,后端收到的是字符串 "18",需要自己转换成数字。而 JSON 原生支持数字类型,后端可以直接反序列化成整数。
  3. 无法直接表示数组
    比如要传递一个爱好列表 ["读书","运动"],表单格式没有标准表示法,常见的是 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 会做以下事情:

  1. 接收 TCP 连接,读取客户端发送来的字节流。
  2. 解析 HTTP 报文:按照 HTTP 协议规范,将字节流解析成请求行、请求头、请求体。例如,它会找到 Content-Type 头,把值取出来。
  3. 封装成 HttpServletRequest 对象:Tomcat 将解析出的信息封装成一个实现了 HttpServletRequest 接口的对象,这个对象包含所有请求信息,如请求方法、URL、请求头、请求体输入流等。
  4. 将请求交给 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/xmltext/xml XML 与 Java 对象互转

假设请求的 Content-Typeapplication/json,那么 MappingJackson2HttpMessageConvertercanRead() 方法会返回 true。

4.3.3 调用 read() 方法

找到合适的转换器后,解析器会调用它的 read() 方法,传入目标类型和请求的输入流(HttpServletRequest.getInputStream())。转换器内部会:

  1. 从输入流中读取所有字节,得到请求体的原始字节数据。
  2. 根据字符编码(如 UTF-8)将字节解码成字符串。这个字符编码通常可以从请求头中的 Content-Type 里获取,如果没有指定,则使用默认编码(通常是 UTF-8)。
  3. 按照格式解析字符串
    • 对于 JSON,使用 Jackson 库将字符串解析成 Java 对象(通过反射创建对象,并递归填充属性)。
    • 对于表单格式,如果是 FormHttpMessageConverter,它会按 &= 拆分成键值对,然后返回一个 MultiValueMap<String, String>。但注意,@RequestBody 通常不用于表单格式,因为表单格式更常用数据绑定(后面会讲)。
  4. 返回解析好的 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。这个解析器用于处理 模型属性(即没有注解的普通对象参数)。它的工作方式是:

  1. 从请求中获取所有 请求参数(注意:请求参数包括 URL 查询参数和表单格式的请求体数据,Spring 把它们统一视为“参数”)。
  2. 创建一个目标类型的实例(比如 new User())。
  3. 遍历请求参数的所有键值对,对于每个键(如 name),尝试在目标对象中查找对应的 setter 方法(如 setName(String)),并将值转换后通过 setter 注入。
  4. 这个过程称为 数据绑定,它本质上也是一种反序列化,只不过针对的是简单的键值对,而不是复杂的结构化文本。

注意:数据绑定只能用于表单格式或查询参数,不能用于 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 将返回值写入响应体。这个过程与请求解析类似,但方向相反:

  1. Spring 会根据 返回值的类型 和客户端的 Accept 请求头(如果有)来选择合适的 HttpMessageConverter
  2. 调用转换器的 write() 方法,将 Java 对象序列化成指定格式的数据(如 JSON 字符串),并写入响应体的输出流。
  3. 同时,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-Typeapplication/json,将请求体字节流封装成 ServletInputStream
  • 将请求信息封装成 HttpServletRequest 对象,交给 Spring 的 DispatcherServlet
步骤3:DispatcherServlet 寻找处理器
  • DispatcherServlet 根据 URL /api/register 和 POST 方法,找到对应的 register 方法。
  • 发现方法参数有 @RequestBody,于是使用 RequestResponseBodyMethodProcessor 参数解析器。
步骤4:参数解析器处理
  • 解析器从请求头获取 Content-Typeapplication/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 请求fetchaxiosXMLHttpRequest)。浏览器不知道你要发送什么数据,它只是把你给的 body 原样发送,所以你必须手动设置 Content-Type 告诉服务器这是什么格式。不过有一个例外:如果你传给 fetchbodyFormData 对象,浏览器会自动设置 Content-Typemultipart/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-8charset 指定了字符编码。它告诉接收方,请求体中的字节是用什么编码转换成文本的。如果没有指定,通常会使用默认编码(如 UTF-8)。但为了准确,建议明确指定。

8.4 响应头中的 Content-Type 如果不设置会怎样?

Spring 会根据返回值的类型自动设置一个默认的,比如返回字符串可能设为 text/plain,返回对象设为 application/json。如果 Spring 找不到合适的默认类型(比如返回 void),可能就不会设置,但这种情况很少见。通常不用操心。


第九章:总结与思考

通过上面啰嗦的讲解,我们终于把 HTTP 通信的整个链条打通了。现在再来回顾最初的问题,是不是都清晰了?

  1. HTTP 传输的本质:底层是二进制字节流,但通过 Content-Type 可以约定这些字节如何被解读。
  2. 为什么需要 JSON:因为 JSON 能表达复杂数据结构,且与语言无关,是现代 Web API 的主流格式。
  3. 为什么 JSON 需要 @RequestBody:因为 JSON 是完整的结构化文本,需要专门的转换器整体解析;@RequestBody 就是触发这个机制的开关。
  4. 为什么表单格式可以直接用对象接收:因为表单格式是零散的键值对,可以通过数据绑定逐个填充到对象属性中,不需要整体解析。
  5. Content-Type 谁来设置:前端发送时,如果是 AJAX 需要手动设置;如果是表单提交,浏览器自动设置。后端响应时,Spring 会根据返回类型自动设置,也可以手动覆盖。
  6. 后端如何看到 Content-Type:Tomcat 解析 HTTP 报文,把请求头存入 HttpServletRequest,Spring 从中读取。
  7. 后端如何解析请求体:通过 HttpMessageConverter 链,根据 Content-Type 选择合适的转换器,将字节流反序列化成 Java 对象。

希望这篇啰嗦到极致的文章能帮助你彻底打通 HTTP 通信的任督二脉。

声明

本文内容来自于询问AI,由作者整理、并优化。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐