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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/213145.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

springboot足球社区管理系统

springboot足球社区管理系统 成品项目已经更新&#xff01;同学们可以打开链接查看&#xff01;需要定做的及时联系我&#xff01;专业团队定做&#xff01;全程包售后&#xff01; 2000套项目视频链接&#xff1a;https://pan.baidu.com/s/1N4L3zMQ9nNm8nvEVfIR2pg?pwdekj…

【论文阅读】Bayes’ Rays:神经辐射场的不确定性量化

【论文阅读】Bayes’ Rays&#xff1a;神经辐射场的不确定性量化 1. Introduction2. Related work3. Background3.2. Neural Laplace Approximations 4. Method4.1. Intuition4.2. Modeling perturbations4.3. Approximating H4.4. Spatial uncertainty 5. Experiments & A…

多线程(初阶七:阻塞队列和生产者消费者模型)

一、阻塞队列的简单介绍 二、生产者消费者模型 三、模拟实现阻塞队列 一、阻塞队列的简单介绍 首先&#xff0c;我们都知道&#xff0c;队列是先进先出的一种数据结构&#xff0c;而阻塞队列&#xff0c;是基于队列&#xff0c;做了一些扩展&#xff0c;在多线程有就非常有意…

Task中Wait()和Result造成死锁

在使用Task的时候&#xff0c;一不留神就会造成死锁&#xff0c;而且难以发现&#xff0c;尤其是业务繁多的情况下&#xff0c;一个Task嵌套另一个Task的时候&#xff0c;下面就演示一下&#xff0c;在什么情况下&#xff0c;会产生Wait()和Result的死锁&#xff0c;因此&#…

Java系类 之 String、StringBuffer和StringBuilder类的区别

我 | 在这里 &#x1f575;️ 读书 | 长沙 ⭐软件工程 ⭐ 本科 &#x1f3e0; 工作 | 广州 ⭐ Java 全栈开发&#xff08;软件工程师&#xff09; &#x1f383; 爱好 | 研究技术、旅游、阅读、运动、喜欢流行歌曲 ✈️已经旅游的地点 | 新疆-乌鲁木齐、新疆-吐鲁番、广东-广州…

07、基于LunarLander登陆器的强化学习案例(含PYTHON工程)

07、基于LunarLander登陆器的强化学习&#xff08;含PYTHON工程&#xff09; 开始学习机器学习啦&#xff0c;已经把吴恩达的课全部刷完了&#xff0c;现在开始熟悉一下复现代码。全部工程可从最上方链接下载。 基于TENSORFLOW2.10 0、实践背景 gym的LunarLander是一个用于…

【论文 | 联邦学习】 | Towards Personalized Federated Learning 走向个性化的联邦学习

Towards Personalized Federated Learning 标题&#xff1a;Towards Personalized Federated Learning 收录于&#xff1a;IEEE Transactions on Neural Networks and Learning Systems (Mar 28, 2022) 作者单位&#xff1a;NTU&#xff0c;Alibaba Group&#xff0c;SDU&…

【设计模式-4.1】行为型——观察者模式

说明&#xff1a;本文介绍设计模式中行为型设计模式中的&#xff0c;观察者模式&#xff1b; 商家与顾客 观察者模式属于行为型设计模式&#xff0c;关注对象的行为。以商家与顾客为例&#xff0c;商家有商品&#xff0c;顾客来购买商品&#xff0c;如果商家商品卖完了&#…

go语言学习-并发编程(并发并行、线程协程、通道channel)

1、 概念 1.1 并发和并行 并发:具有处理多个任务的能力 (是一个处理器在处理任务)&#xff0c;cpu处理不同的任务会有时间错位&#xff0c;比如有A B 两个任务&#xff0c;某一时间段内在处理A任务&#xff0c;这时A任务需要停止运行一段时间&#xff0c;那么会切换到处理B任…

DockerFile常用保留字指令及知识点合集

目录 DockerFile加深理解&#xff1a; DockerFile常用保留字指令 保留字&#xff1a; RUN&#xff1a;容器构建时需要运行的命令 COPY&#xff1a;类似ADD&#xff0c;拷贝文件和目录到镜像中。 将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 …

【动态规划】LeetCode-面试题 17.16. 按摩师

&#x1f388;算法那些事专栏说明&#xff1a;这是一个记录刷题日常的专栏&#xff0c;每个文章标题前都会写明这道题使用的算法。专栏每日计划至少更新1道题目&#xff0c;在这立下Flag&#x1f6a9; &#x1f3e0;个人主页&#xff1a;Jammingpro &#x1f4d5;专栏链接&…

vs 安装 qt qt扩展

1 安装qt 社区版 免费 Download Qt OSS: Get Qt Online Installer 2 vs安装 qt vs tools 3 vs添加 qt添加 bin/cmake.exe 路径 3.1 扩展 -> qt versions 3.2

【STM32】STM32学习笔记-新建工程(04)

00. 目录 文章目录 00. 目录01. 创建STM32工程02. STM32工程编译和下载03. LED测试04. 型号分类及缩写05. 工程结构06. 附录 01. 创建STM32工程 【STM32】STM32F103C8T6 创建工程模版详解(固件库) 02. STM32工程编译和下载 2.1 选择下载器位ST-Link Debugger 2.2 勾选上电…

04. 函数

目录 1、前言 2、Python中的函数 2.1、内置函数 2.2、自定义函数 2.3、函数调用 3、函数的参数 3.1、形参和实参 3.2、位置参数&#xff08;Positional Arguments&#xff09; 3.3、默认参数&#xff08;Default Arguments&#xff09;&#xff1a; 3.4、关键字参数&a…

如何为C#WinFrom编译的.exe添加个性化图标

1、在VS中点击菜单栏上的“项目”,找到最下面的属性&#xff0c;单击进去 2、加载自定义的.ico文件&#xff0c;如果没有此格式的文件可以使用此网站去转换&#xff1a;图标制作大师 - 轻松制作网站favicon图标 3、重新编译文件即可

【【水 MicroBlaze 最后的介绍和使用】】

水 MicroBlaze 最后的介绍和使用 我对MicroBlaze 已经有了一个普遍的理解 了 现在我将看的两个 一个是 AXI4接口的 DDR读写实验 还有一个是 AXI DMA 环路实验 虽然是 水文 但是 也许能从中 得到一些收获 第一个是 AXI DDR 读写实验 Xilinx 从 Spartan-6 和 Virtex-6 系列开始…

SSM框架(六):SpringBoot技术及整合SSM

文章目录 一、概述1.1 简介1.2 起步依赖1.3 入门案例1.4 快速启动 二、基础配置2.1 三种配置文件方式2.2 yaml文件格式2.3 yaml读取数据方式&#xff08;3种&#xff09; 三、多环境开发3.1 yml文件-多环境开发3.2 properties文件-多环境开发3.3 多环境命令行启动参数设置3.4 多…

【数值计算方法(黄明游)】函数插值与曲线拟合(一):Lagrange插值【理论到程序】

​ 文章目录 一、近似表达方式1. 插值&#xff08;Interpolation&#xff09;2. 拟合&#xff08;Fitting&#xff09;3. 投影&#xff08;Projection&#xff09; 二、Lagrange插值1. 天书1. 人话拉格朗日插值方法a. 线性插值&#xff08;n1&#xff09;基本思想线性插值与线…

解决uview中uni-popup弹出层不能设置高度问题

开发场景&#xff1a;点击条件筛选按钮&#xff0c;在弹出的popup框中让用户选择条件进行筛选 但是在iphone12/13pro展示是正常&#xff0c;但是切换至其他手机型号就填充满了整个屏幕&#xff0c;需要给这个弹窗设置一个固定的高度 iphone12/13pro与其他型号手机对比 一开始…

智能优化算法应用:基于海洋捕食者算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于海洋捕食者算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于海洋捕食者算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.海洋捕食者算法4.实验参数设定5.算法结果…