前后端传输请求分析

请求体使用 form-data、x-www-form-urlencoded、raw、binary?

请求体使用 form-data、x-www-form-urlencoded、raw、binary?

区别不在于“前端爱用哪个”,而在于“后端怎么解析 + 数据长什么样”。

自动配置Content-Type

多数情况下 fetch 不会替你设置,除非 body 是某些“浏览器知道怎么描述”的类型。

你可以按 body 类型记:

body: JSON.stringify(obj)(字符串) 默认:通常会变成 text/plain;charset=UTF-8(有的实现甚至不带 Content-Type) 所以你写 JSON 时必须手动加application/json

body: new URLSearchParams(...) 浏览器一般会自动设:application/x-www-form-urlencoded;charset=UTF-8

body: new FormData() 浏览器会自动设:multipart/form-data; boundary=... (你不应该手动设,否则 boundary 丢了会坏)

body: Blob / File / ArrayBuffer / Uint8Array / ReadableStream

  • Blob/File:如果你创建 Blob 时给了 type,就会用那个;没给的话可能不带或是 application/octet-stream(取决于实现/对象)
  • 纯二进制(ArrayBuffer/TypedArray):很多情况下不自动设,你最好自己设成 application/octet-stream

form-data(multipart/form-data)

这是:分段的数据流

典型用途只有一个你必须记住:上传文件。只要有 File / Blob,99% 就是它。

它的本质是:一个请求体里被拆成很多段,每一段都有自己的 Content-Type 和边界(boundary)。

  • 文件上传
  • 文件 + 其他字段一起传
  • 后端使用传统表单/上传中间件(multer、Spring Multipart)

关键细节:

  • 浏览器会自动生成 boundary
  • 不要手动设置 Content-Type
  • 对象/数组要自己 JSON.stringify

后端

java
@PostMapping("/form-data")
public String formData(MultipartFile file,String name) {
	log.info("formData=>fileName:{},name:{}", file.getOriginalFilename(),name);
	return "index";
}

如果使用多层可以

java
@Data
public class FormDataPojo {
    Integer age;
    String name;
}

@PostMapping("/form-data")
public String formData(@ModelAttribute FormDataPojo formDataPojo, MultipartFile file) {
	log.info("formData=>fileName:{},formDataPojo:{}", file.getOriginalFilename(),formDataPojo);
	return "index";
}

1️⃣ @RequestBody → 整个 body,当成一坨内容(JSON / XML / text) → 只能用一次

2️⃣ @RequestParam → 一个一个的参数 → 适合零散字段

3️⃣ @ModelAttribute → 把一堆参数,拼成一个对象

@RequestBody:把 请求体 body 当成一整坨内容(最常见是 application/json),用 JSON 反序列化成对象。 @ModelAttribute:从 参数(query / 表单字段 / multipart 文本字段)里按“键值对”把值填进对象,本质是数据绑定,不是 JSON 解析。

文件属性也会被打包!

java
@Data
public class FormDataPojo {
    Integer age;
    String name;
    MultipartFile file;
}

@RequestPartmultipart/form-data 世界里的 @RequestBody

你先把 multipart 想成这样的一坨东西:

multipart/form-data
├─ part: name=meta   Content-Type: application/json
│  { "a": 1, "b": 2 }
├─ part: name=file   Content-Type: image/png
│  <binary...>
├─ part: name=title  Content-Type: text/plain
│  hello

@RequestPart 干的事就是:拿其中某一段

注解作用对象读取来源是否看 Content-Type是否支持 JSON 反序列化典型使用场景
@RequestParam单个参数query / form / multipart 的单个字段❌ 不看❌ 不支持普通表单字段、简单参数、文件(传统用法)
@RequestPartmultipart 的某一段 partmultipart/form-data✅ 看✅ 支持multipart + JSON + 文件

JSON怎么传

js
// 关键:用 “info.info” 这种路径写法
fd.append('info.info', 'this is a js object')

后端:

java
@PostMapping("/form-data")
public String formData(@ModelAttribute FormDataPojo formDataPojo){}
@Data
public class FormDataPojo {
    Integer age;
    String name;
    MultipartFile file;
    InfoPojo info;
}
@Data
public class InfoPojo {
    String info;
}

如果你希望前端真的传一个 JSON 对象作为一个 part(而不是拆成 info.info),那就用 @RequestPart 来解析它。

js
fd.append('info', new Blob(
  [JSON.stringify({ info: 'this is a js object' })],
  { type: 'application/json' }
))

后端接收方式:

java
@PostMapping("/form-data")
public String formData(@RequestParam("age") Integer age,
					   @RequestParam("name") String name,
					   @RequestPart("info") InfoPojo info,
					   @RequestPart("file") MultipartFile file) {
	log.info("name:{},age:{},info:{},fileName:{}", name, age, info,file.getOriginalFilename());
	return "index";
}

默认情况下 @ModelAttribute 不会处理(不反序列化)JSON part,你要么上面的,要么单独解析:

java
public String formData(@ModelAttribute FormDataPojo dto, @RequestPart("info") InfoPojo info) 

前端

js
let fd = new FormData()
fd.append("name", "张三")
fd.append("age", 45)
fd.append("file", mFile.files[0])
fetch("http://127.0.0.1:8090/form-data", {
  method: 'POST',
  body: fd
})

请求头会自动设置Content-Type

image-20251229010010993

如果你自己非要设置,比如加上headers: { 'Content-Type': 'multipart/form-data' },只会引起服务器报错:

 the request was rejected because no multipart boundary was found

有的时候读取不到文件名需要显式标识:

js
fd.append("file", mFile.files[0],"fileName.js")

这可能在第二个参数纯粹的只有二进制数值的时候

可以查看到设置的文件格式:

image-20251229010859196

x-www-form-urlencoded

urlencoded,表单编码

form-urlencoded=form表单使用url编码

后端

java
@GetMapping("/urlencoded")
public String urlencodedGet(String name,Integer age) {
	log.info("getMapping=>name:{},age:{}", name, age);
	return "index";
}
@PostMapping("/urlencoded")
public String urlencodedPost(String name,Integer age) {
	log.info("postMapping=>name:{},age:{}", name, age);
	return "index";
}
@PutMapping("/urlencoded")
public String urlencodedPut(String name,Integer age) {
	log.info("putMapping=>name:{},age:{}", name, age);
	return "index";
}
@DeleteMapping("/urlencoded")
public String urlencodedDelete(String name,Integer age) {
	log.info("deleteMapping=>name:{},age:{}", name, age);
	return "index";
}

前端

GET

js
fetch('http://127.0.0.1:8090/urlencoded?name=zhangsan&age=15')

POST | PUT | DELETE

js
fetch('http://127.0.0.1:8090/urlencoded', {
  method: 'POST' | 'PUT' | 'DELETE',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  //默认会设置headers为application/x-www-form-urlencoded;charset=UTF-8所以上面可以不加
  body: new URLSearchParams({ name: 'zhangsan', age: '18' })
})
//或者使用下面也是一样的:
fetch('http://127.0.0.1:8090/urlencoded?name=zhangsan&age=15', {
  method: 'POST' | 'PUT' | 'DELETE',
})

raw

raw 不是一种格式,它只是告诉工具:

我直接往 body 里塞原始数据

真正的格式由 Content-Type 决定。

最常见的 raw:application/json(现代 API 主流)、text/plainapplication/xml

后端

java
@PostMapping("/raw")
public String raw(@RequestBody String rawText) {
	log.info("rawMapping=>rawText:{}", rawText);
	return "index";
}

或者封装到一个POJO中

java
@Data
public class PostPojo {
    String name;
    Integer age;
}
@PostMapping("/raw")
public String raw(@RequestBody PostPojo rawText) {
	log.info("rawMapping=>rawText:{}", rawText);
	return "index";
}

前端

js
fetch('http://127.0.0.1:8090/raw', {
  method: 'POST',
  body: `原始文本放在这里`
})

如果是解析实体的话,需要设置Content-Type,否则会后台警告

DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'text/plain;charset=UTF-8' not supported]

设置完之后请求体需要放置标准的JSON格式:

js
fetch('http://127.0.0.1:8090/raw', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: `{ "name": "zhangsan", "age": 18 }`
})

binary

这是:整个请求体就是一坨二进制

适合:

  • 直接上传一个文件流
  • 推视频、音频、固件、镜像
  • 对象存储直传(某些 PUT 接口)

前端怎么用

body 直接放 Blob / ArrayBuffer / File

js
fetch('/api/upload', {
  method: 'PUT',
  headers: { 'Content-Type': 'application/octet-stream' },
  body: file
})

后端怎么收?不解析字段,直接读流!

js
InputStream in = request.getInputStream();

后端看到的是:一整条二进制流

前端

js
fetch('http://127.0.0.1:8090/binary', {
  method: 'POST',
  // headers: { 'Content-Type': 'application/json' },
  body: mFile.files[0]
})

后端

原始方式:

java
@PostMapping("/binary")
public String binary(HttpServletRequest request) throws IOException {
	ServletInputStream in = request.getInputStream();
	StringBuilder sb = new StringBuilder();
	try (BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
		String line;
		while ((line = br.readLine()) != null) {
			sb.append(line).append('\n');
		}
	}
	log.info("binaryMapping=>inputStream:{}", sb);
	return "index";
}

Spring MVC 直接用参数接 byte[] / InputStream / Resource

@RequestBody InputStream(有些 Spring 版本/配置可用,但不如 Resource 稳)

java
@PostMapping("/binary")
public String binary(@RequestBody byte[] data) throws IOException {
	log.info("binaryMapping=>inputStream:{}", new String(data));
	return "index";
}
@PostMapping(value="/bin", consumes="application/octet-stream")
public String bin(@RequestBody org.springframework.core.io.Resource body) throws Exception {
    try (var in = body.getInputStream()) {
        // 读流
    }
    return "ok";
}

也可以接 InputStream(有时配合 HttpEntity 更常见):

java
@PostMapping(value="/bin", consumes="application/octet-stream")
public String bin(org.springframework.http.HttpEntity<byte[]> entity) {
    byte[] data = entity.getBody();
    return "ok";
}

加参数位置

fetch 里“加参数”本质就 4 个地方:URL 路径、URL query、请求体 body、请求头 header(还有 cookie/凭证算第五种)。

注意

GET请求不可以加在请求体里面

Failed to execute 'fetch' on 'Window': Request with GET/HEAD method cannot have body.

image-20251229003011792

1)用 URLSearchParams 拼 query(更稳,不用手动处理编码)

js
const params = new URLSearchParams({ name: 'zhangsan', age: '15' })
fetch(`http://127.0.0.1:8090/urlencoded?${params.toString()}`)

2)用 URL 对象操作 query(适合在已有 URL 上增删改查参数)

js
const url = new URL('http://127.0.0.1:8090/urlencoded')
url.searchParams.set('name', 'zhangsan')
url.searchParams.set('age', '15')
fetch(url)

3)放在路径里(REST 风格,适合“定位某个资源”)

js
fetch('http://127.0.0.1:8090/users/zhangsan')
fetch('http://127.0.0.1:8090/users/15/profile')

4)放在 JSON 请求体里(最常见的 POST/PUT/PATCH)

js
fetch('http://127.0.0.1:8090/user', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'zhangsan', age: 15 })
})

5)放在 x-www-form-urlencoded 请求体里(老接口/表单)

js
fetch('http://127.0.0.1:8090/urlencoded', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({ name: 'zhangsan', age: '15' })
})

6)放在 FormData 里(上传或模拟表单)

js
const fd = new FormData()
fd.append('name', 'zhangsan')
fd.append('age', '15')

fetch('http://127.0.0.1:8090/form', {
  method: 'POST',
  body: fd
})

7)放在 header 里(常见:鉴权、语言、版本、追踪 ID)

js
fetch('http://127.0.0.1:8090/user', {
  headers: {
    Authorization: 'Bearer xxx',
    'X-Request-Id': crypto.randomUUID()
  }
})

8)带 cookie / session(需要同源或配 CORS,且要显式带凭证)

js
fetch('http://127.0.0.1:8090/user', {
  credentials: 'include'
})

评论

评论加载中……