前后端数据传输格式(上)
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬
作为后端,写完接口一定要自测,而Postman可以说是最常用的接口测试工具了。但是在介绍Postman的使用之前,我们需要复习一下GET/POST请求的一些细节。据我观察,其实无论前端还是后端,很多人对POST的了解都不够:
主要内容:
- 环境准备
- HTTP请求格式
- 3种POST请求
-
- 普通表单提交
- 文件表单提交
- JSON提交
- Postman测试
环境准备
pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
启动类(@ServletComponentScan扫描Servlet)
@ServletComponentScan
@SpringBootApplication
public class RequestDemoApplication {
public static void main(String[] args) {
SpringApplication.run(RequestDemoApplication.class, args);
}
}
Servlet
@WebServlet(urlPatterns = "/upload")
@Slf4j
public class UploadServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String nameParam = request.getParameter("name");
String ageParam = request.getParameter("age");
log.info("GET请求 name:{}, age:{}", nameParam, ageParam);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String nameParam = request.getParameter("name");
String ageParam = request.getParameter("age");
log.info("POST请求 name:{}, age:{}", nameParam, ageParam);
}
}
创建表单页面form.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>
HTTP请求格式
HTTP请求的格式可以笼统地分为
- 请求行
- 请求头
- 空行
- 请求体
启动程序,分别发送GET/POST请求并观察HTTP报文(当前案例中,浏览器使用firefox效果好一些)。
GET
http://localhost:8080/upload?name=周星驰&age=18
结合上面几张图分析,可以得出3个信息:
- GET请求没有请求体,但参数会附在URL后面传递
- GET请求的参数会经过UrlEncoder编码(所以URL的中文是"%E5%91...")
- Servlet可以通过request.getParameter()取到参数
POST
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>普通表单提交:默认enctype="application/x-www-form-urlencoded"</h3>
<form action="/upload" method="post">
用户名:<input type="text" name="name"/><br>
年龄:<input type="text" name="age"/><br>
<input type="submit" value="提交"/>
</form>
</body>
</html>
结合上面几张图分析,也可以得出3个信息:
- POST请求有请求体,参数在请求体里
- 请求体里的参数会经过UrlEncoder编码
- Servlet通过request.getParameter()可以取到参数
小结
无论GET请求还是POST普通表单提交,后端都可以用request.getParameter()取到参数。
不是说GET参数是跟在URL里,而POST参数在请求体里吗,为什么都可以通过request.getParameter()得到?
其实不论是GET还是POST普通表单,请求参数都是编码后以name=xxx&age=xxx&file=xxx的形式存在的,只不过GET的参数跟在URL中,而POST放在请求体里。服务器接收到请求后,会根据请求类型是GET还是POST,分别从对应位置取出name=xxx&age=xxx&file=xxx并进行解析、分割,然后存在request的一个map中。
这里之所以强调POST表单请求,是因为POST其实有很多种形式,仅仅是POST表单请求和GET比较相似,其他几种POST形式和GET区别较大,后面会介绍。
对了,有一个很有意思的现象,GET和POST普通表单真的太像了,对于Spring的Controller来说,GET请求的参数可以是散装的,也可以用POJO接收,POST表单请求也是如此(动手试一下)!
3种POST请求
复习完HTTP请求的格式后,我们着重聊聊POST请求(GET太简单了,略过)。
POST请求中,开发常见的有3种方式:
- 普通表单提交
- 文件表单提交
- JSON提交
普通表单提交
上面已经试过了,它的特点是:
- 默认的编码方式是
enctype="application/x-www-form-urlencoded"
- 会对每一个表单项进行编码("周星驰" --> "%E5%91%A8%...")
- 不能提交文件流
- 后端可以通过request.getParameter()得到每个参数
有两点需要证明一下:默认编码是什么、能否提交文件流。
我们在HTML的表单中并没有指定enctype,然后HTTP报文中却显示:
所以POST表单请求默认编码方式是enctype="application/x-www-form-urlencoded"
。我们从name=“周星驰”这个参数的乱码也可以看出,肯定经过了某种编码。
如果你观察过Postman的POST,其实就有这一个选项:
那么enctype="application/x-www-form-urlencoded"
这种编码格式能不能上传文件流呢?比如,一个用户表单,能不能同时上传头像呢?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>普通表单提交:默认enctype="application/x-www-form-urlencoded"</h3>
<form action="/upload" method="post">
用户名:<input type="text" name="name"/><br>
年龄:<input type="text" name="age"/><br>
选择文件:<input type="file" name="file"/><br>
<input type="submit" value="提交"/>
</form>
</body>
</html>
@WebServlet(urlPatterns = "/upload")
@Slf4j
public class UploadServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String nameParam = request.getParameter("name");
String ageParam = request.getParameter("age");
log.info("GET请求 name:{}, age:{}", nameParam, ageParam);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String nameParam = request.getParameter("name");
String ageParam = request.getParameter("age");
// 根据参数名获取文件
String fileParam = request.getParameter("file");
log.info("POST请求 name:{}, age:{}, file:{}", nameParam, ageParam, fileParam);
}
}
通过实验我们发现,POST普通表单是无法上传文件的,最多只能传递文件名(请求体中是file=xxxx.png)。
文件表单提交
所谓文件表单提交,指的是此时表单中含有文件项。
在上面普通表单提交的实验中,我们已经发现,光是设置input输入框为type="file"是不够的,这样只是提交文件名,而不是文件流。
归根到底,造成这种现象的原因在于当前表单的编码格式不对。POST请求默认的编码格式是:enctype="application/x-www-form-urlencoded"
,也就是对各个表单项的参数值进行URL编码后以name=xxx&age=xxx&file=xxx的形式放入请求体。
最终file仅仅提交了文件名,而不是文件流。
那么怎样才能把整个文件提交上去呢?
只要把POST请求的编码改为enctype="multipart/form-data"
即可。
form-data形式提交
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>文件表单提交:必须指定enctype="multipart/form-data"</h3>
<form action="/upload" method="post" enctype="multipart/form-data">
用户名:<input type="text" name="name"/><br>
年龄:<input type="text" name="age"/><br>
选择文件:<input type="file" name="file"/><br>
<input type="submit" value="提交"/>
</form>
</body>
</html>
看起来好像稳了,请求体有点不一样了!
但是回到SpringBoot一看,坏了:
之前普通表单提交虽然无法上传文件流,但好歹传了文件名呢,而这次干脆啥都没提交上去。
但是,真的什么都没提交上去吗?
其实前端已经提交成功,只是后端没按正确方式接收。
失败原因分析
我们观察一下设置enctype="multipart/form-data"
后,请求体的变化:
application/x-www-form-urlencoded编码的请求体参数
multipart/form-data编码的请求体参数
浏览器会在发起POST请求时,自动带上Content-Type请求头:
- 普通表单的Content-Type是:application/x-www-form-urlencoded(图一)
- 文件表单的Content-Type是:multipart/form-data(图二)
Content-Type的作用是告诉服务器本次提交的参数编码类型,而它的值取决于enctype。
为什么multipart/form-data能实现文件上传?这要从multipart/form-data独特的格式说起。
multipart/form-data的意思是多部件传输请求,即把表单的每一项都作为一个部件,单独发送,每一个部件其实都包含:
- 请求头
- 空行
- 请求体
-----------------------------5708493371455355810326359340
Content-Disposition: form-data; name="name"
å¨æé©°
-----------------------------5708493371455355810326359340
Content-Disposition: form-data; name="age"
18
-----------------------------5708493371455355810326359340
Content-Disposition: form-data; name="file"; filename="avatar.png"
Content-Type: image/png
PNG
简单来说,就是这样:
其中,file项显示乱码恰恰代表本次上传的是文件(你本地直接用文本编辑器打开一张照片也是如此)。
所以,此时所有POST参数(包括整个文件)都已经发送到后端了。
在application/x-www-form-urlencoded格式下,服务器会帮我们分割参数并存入Map中,所以我们才能通过request.getParameter()获取参数。但由于这次ContentType是multipart/form-data,服务器将不再对参数进行解析,而是直接放入了request的inputStream中,如果想要获取参数,开发人员必须自己处理。
SpringMVC接收File
要想得到请求参数,大致的处理流程是:
- 识别分隔符"-----------5708493371455355810326359340"
- 根据分隔符把参数切成三份
- 从请求头中得到参数名
- 从请求体中得到参数值。如果Content-Type:image/png,还要把字符转为图片
是不是觉得很烦?
所幸,已经有人替我们做了。如果你用的是Servlet,那么可以引入Apache的commons-fileupload依赖 ,它的原理就是上面说的那样。
如果你用的是SpringMVC,那就更方便了,人家已经集成了commons-fileupload依赖,在请求到达Controller方法之前,已经替我们把文件封装到MultipartFile组件中:
@Slf4j
@RestController
public class UploadController {
@PostMapping("/springUpload")
public void upload(String name, Integer age, MultipartFile file) {
log.info("name:{}, age:{}, fileName:{}", name, age, file.getOriginalFilename());
}
}
此时参数file是整个文件,我们可以通过file.getInputStream()获取文件流。
整个过程大致是这样的:
POST表单请求 --> name=xxx&age=xxx,服务器能处理,存入Map --> request.getParameter()
POST文件上传 --> 服务器无法处理,全部塞入Request的InputStream --> 第三方组件分割多个参数,File参数会被单独包装成MultipartFile(包含文件流、文件名等)
上面是通过HTM的form表单实现的(enctype指定form-data),我们也可以使用Postman发送表单请求:
JSON提交
引入JQuery啥的比较麻烦,我们直接用Postman模拟。总之,HTML+Ajax能做的,都可以通过Postman模拟。
Postman测试
GET请求
POST请求:none
none就是没有请求体,但编码还是默认的enctype="application/x-www-form-urlencoded"
POST请求:x-www-form-urlencoded
有请求体,编码是默认的enctype="application/x-www-form-urlencoded"
POST请求:multipart/form-data
POST请求:raw+Json
虽然上面说了那么多种POST请求形式,但实际上前后端分离后已经很少使用其他几种POST形式了,基本都是JSON。
它们的使用频率大概是:
none:几乎为0%
x-www-form-urlencoded:10%
multipart/form-data:20%(比如OSS文件上传,有可能会配合MultipartFile单独写一个)
raw+json:70%(绝大多数POST都是JSON,后端需要用@RequestBody接收)
所以,这里我们不再演示文件提交,而是演示JSON请求(以POST新增User为例)
POJO
@Data
public class UploadUser {
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 地址
*/
private String address;
}
Controller
@RestController
@Slf4j
public class UserController {
/**
* 新增用户
* @param user
* @return
*/
@PostMapping("addUser")
public UploadUser addUser(UploadUser user) {
log.info("user:{}", user);
return user;
}
}
Postman发起POST请求(raw+JSON)
后端接口压根没取到参数:
因为现在发起的请求是JSON,所以后面必须明确以JSON格式解析:
请求成功:
什么时候加@RequestBody?
- POST请求是JSON时,必须加@RequestBody
- POST请求时普通表单时,不用加,加了反而错
- POST请求是文件上传时,不用加,用MultipartFile
规则这么多,说了等于没说。关键还是要明白@RequestBody到底意味着什么?
@RequestBody的意思是:SpringMVC将会用jackson解析请求体中参数,其他非JSON格式的参数会直接抛异常
SpringBoot默认使用jackson解析JSON
打开Postman的请求控制台:
分别发送x-www-form-urlencoded、form-data、raw+JSON三种请求:
当前端指定Content-Type为json,也就是发送JSON请求时,后端就要加@RequestBody,其他GET/POST普通表单/POST文件表单都不需要,加了反而错。
示意图
三种POST形式,参数其实都是在请求体中,但格式有点不同。
如果把GET请求的格式也加上,你会发现:
可怜的服务器其实只会处理x-www-form-urlencoded这种简单的格式,做一些参数切割工作,然后封装到Map中,方便Servlet通过request.getParameter()获取。
其他的无论multipart/form-data还是application/json,都需要第三方框架在后面补充完成。
最后,我相信部分同学可能对@RequestBody/@ResponseBody如何起作用比较感兴趣,如果后面有多余篇幅的话,可以一起探究一下。其实不是特别重要。
提问
1.GET和POST的请求参数分别在哪?默认是什么编码?
2.文章把POST请求分为哪几种?请求参数存放的位置一样吗?
3.普通表单请求和文件表单请求的区别是?
4.文件表单提交后,Servlet的request.getPatameter()方法就废了。如果不借助file-upload,你怎么获取参数?
5.后端接口方法什么时候加@RequestBody?
6.怎么用Postman分别发送文件表单、普通表单、JSON请求?
请在评论区尝试作答!
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
进群,大家一起学习,一起进步,一起对抗互联网寒冬