前后端数据传输格式(上)

作者简介:大家好,我是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

进群,大家一起学习,一起进步,一起对抗互联网寒冬

相关推荐
知兀12 天前
Maven
java·笔记·maven·javaweb·黑马程序员
SunnyRivers17 天前
黑马程序员Java项目实战《苍穹外卖》Day12
javaweb·苍穹外卖
岁岁岁平安17 天前
JavaWeb学习(4)(四大域、HttpSession原理(面试)、SessionAPI、Session实现验证码功能)
java·学习·javaweb·session·httpsession
大臣不想在月亮上上热搜22 天前
黑马2024AI+JavaWeb开发入门Day05-数据库DDL、DML、DQL飞书作业
sql·mysql·javaweb
SunnyRivers23 天前
黑马程序员Java项目实战《苍穹外卖》Day05
javaweb·苍穹外卖
岁岁岁平安1 个月前
JavaWeb实战(1)(重点:分页查询、jstl标签与jsp、EL表达式、Bootstrap组件搭建页面、jdbc)
java·servlet·javaweb·jsp·el·分页查询·jstl
SunnyRivers1 个月前
黑马程序员Java项目实战《苍穹外卖》Day01
javaweb·苍穹外卖
Slow菜鸟1 个月前
JavaWeb
javaweb
killsime1 个月前
JavaWeb开发 : tomcat+Servlet+JSP
java·servlet·tomcat·javaweb
雷神乐乐1 个月前
MyBatis中的${}和#{}区别
数据库·sql·mybatis·javaweb