前后端传输请求分析
请求体使用 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
后端
@PostMapping("/form-data")
public String formData(MultipartFile file,String name) {
log.info("formData=>fileName:{},name:{}", file.getOriginalFilename(),name);
return "index";
}如果使用多层可以
@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; }
@RequestPart是multipart/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 的某一段 part multipart/form-data ✅ 看 ✅ 支持 multipart + JSON + 文件
JSON怎么传
// 关键:用 “info.info” 这种路径写法
fd.append('info.info', 'this is a js object')后端:
@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 来解析它。
fd.append('info', new Blob(
[JSON.stringify({ info: 'this is a js object' })],
{ type: 'application/json' }
))后端接收方式:
@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,你要么上面的,要么单独解析:
public String formData(@ModelAttribute FormDataPojo dto, @RequestPart("info") InfoPojo info) 前端
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

如果你自己非要设置,比如加上headers: { 'Content-Type': 'multipart/form-data' },只会引起服务器报错:
the request was rejected because no multipart boundary was found有的时候读取不到文件名需要显式标识:
jsfd.append("file", mFile.files[0],"fileName.js")这可能在第二个参数纯粹的只有二进制数值的时候
可以查看到设置的文件格式:
x-www-form-urlencoded
urlencoded,表单编码
form-urlencoded=form表单使用url编码
后端
@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
fetch('http://127.0.0.1:8090/urlencoded?name=zhangsan&age=15')POST | PUT | DELETE
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/plain、application/xml
后端
@PostMapping("/raw")
public String raw(@RequestBody String rawText) {
log.info("rawMapping=>rawText:{}", rawText);
return "index";
}或者封装到一个POJO中
@Data
public class PostPojo {
String name;
Integer age;
}
@PostMapping("/raw")
public String raw(@RequestBody PostPojo rawText) {
log.info("rawMapping=>rawText:{}", rawText);
return "index";
}前端
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格式:
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
fetch('/api/upload', {
method: 'PUT',
headers: { 'Content-Type': 'application/octet-stream' },
body: file
})后端怎么收?不解析字段,直接读流!
InputStream in = request.getInputStream();后端看到的是:一整条二进制流
前端
fetch('http://127.0.0.1:8090/binary', {
method: 'POST',
// headers: { 'Content-Type': 'application/json' },
body: mFile.files[0]
})后端
原始方式:
@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 稳)
@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 更常见):
@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.
1)用 URLSearchParams 拼 query(更稳,不用手动处理编码)
const params = new URLSearchParams({ name: 'zhangsan', age: '15' })
fetch(`http://127.0.0.1:8090/urlencoded?${params.toString()}`)2)用 URL 对象操作 query(适合在已有 URL 上增删改查参数)
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 风格,适合“定位某个资源”)
fetch('http://127.0.0.1:8090/users/zhangsan')
fetch('http://127.0.0.1:8090/users/15/profile')4)放在 JSON 请求体里(最常见的 POST/PUT/PATCH)
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 请求体里(老接口/表单)
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 里(上传或模拟表单)
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)
fetch('http://127.0.0.1:8090/user', {
headers: {
Authorization: 'Bearer xxx',
'X-Request-Id': crypto.randomUUID()
}
})8)带 cookie / session(需要同源或配 CORS,且要显式带凭证)
fetch('http://127.0.0.1:8090/user', {
credentials: 'include'
})评论
评论加载中……
