前后端数据传输格式(上)
作者简介:大家好,我是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 {@Overrideprotected 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);}@Overrideprotected 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 {@Overrideprotected 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);}@Overrideprotected 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/pngPNG
简单来说,就是这样:

其中,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
进群,大家一起学习,一起进步,一起对抗互联网寒冬
相关文章:
前后端数据传输格式(上)
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO 联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬 作为后端,写…...
maven的package和install命令有什么区别以及Maven常用命令与GAV坐标与Maven依赖范围与Maven依赖传递与依赖排除与统一声明版本号
maven的package和install命令有什么区别以及Maven常用命令与GAV坐标与Maven依赖范围与Maven依赖传递与依赖排除与统一声明版本号 一: maven的package和install命令有什么区别 一般都与clean命令结合使用 mvn package 生成target目录,编译、测试代码,…...
【动手学深度学习】(六)权重衰退
文章目录 一、理论知识二、代码实现2.1从零开始实现2.2简洁实现 【相关总结】 主要解决过拟合 一、理论知识 1、使用均方范数作为硬性限制(不常用) 通过限制参数值的选择范围来控制模型容量 通常不限制偏移b 小的意味着更强的正则项 使用均方范数作为柔…...
动手学习深度学习-跟李沐学AI-自学笔记(3)
一、深度学习硬件-CPU和GPU 芯片:Intel or AMD 内存:DDR4 显卡:nVidia 芯片可以和GPU与内存通信 GPU不能和内存通信 1. CPU 能算出每一秒能运算的浮点运算数(大概0.15左右) 1.1 提升CPU利用率 1.1.1 提升缓存…...
3.2 Puppet 和 Chef 的比较与应用
Puppet 和 Chef 的比较与应用 文章目录 Puppet 和 Chef 的比较与应用Puppet 和 Chef 简介工作原理对比**模块化的重要性**: Puppet 和 Chef 简介 介绍 Puppet 和 Chef 这两个流行的配置管理工具的背景和用途。强调它们的共同目标:实现自动化的系统配置和…...
promise使用示例
下面是一个 Promise 使用示例,通过 Promise 实现异步操作的链式调用: const getUser (userId) > {return new Promise((resolve, reject) > {// 模拟异步请求setTimeout(() > {const users [{ id: 1, name: Alice },{ id: 2, name: Bob },{ …...
一起学docker系列之十四Dockerfile微服务实践
目录 1 前言2 创建微服务模块2.1 **创建项目模块**2.2 **编写业务代码** 3 编写 Dockerfile4 构建 Docker 镜像5 运行 Docker 容器6 测试微服务7 总结8 参考地址 1 前言 微服务架构已经成为现代软件开发中的一种重要方式。而 Docker 提供了一种轻量级、便携式的容器化解决方案…...
Qt Creator 11.0.3同时使用Qt6.5和Qt5.14.2
Qt Creator 11.0.3同时使用Qt6.5和Qt5.14.2 概要方法1.打开Qt Creator中的Kit,这里我直接附上几张截图,不同的版本打开位置可能有所不同,总之最终目的是要打开构建套件(Kit)2.可以看到构建套件里面有包含了“构建套件K…...
Python中字符串列表的相互转换详解
更多资料获取 📚 个人网站:ipengtao.com 在Python编程中,经常会遇到需要将字符串列表相互转换的情况。这涉及到将逗号分隔的字符串转换为列表,或者将列表中的元素连接成一个字符串。本文将深入讨论这些情景,并提供丰富…...
09、pytest多种调用方式
官方用例 # content of myivoke.py import sys import pytestclass MyPlugin:def pytest_sessionfinish(self):print("*** test run reporting finishing")if __name__ "__main__":sys.exit(pytest.main(["-qq"],plugins[MyPlugin()]))# conte…...
分布式锁常见实现方案
分布式锁常见实现方案 基于 Redis 实现分布式锁 如何基于 Redis 实现一个最简易的分布式锁? 不论是本地锁还是分布式锁,核心都在于“互斥”。 在 Redis 中, SETNX 命令是可以帮助我们实现互斥。SETNX 即 SET if Not eXists (对应 Java 中…...
26、pytest使用allure解读
官方实例 # content of pytest_quick_start_test.py import allurepytestmark [allure.epic("My first epic"), allure.feature("Quick start feature")]allure.id(1) allure.story("Simple story") allure.title("test_allure_simple_te…...
Uncle Maker: (Time)Stamping Out The Competition in Ethereum
目录 笔记后续的研究方向摘要引言贡献攻击的简要概述 Uncle Maker: (Time)Stamping Out The Competition in Ethereum CCS 2023 笔记 本文对以太坊 1 的共识机制进行了攻击,该机制允许矿工获得比诚实同行更高的挖矿奖励。这种名为“Uncle Maker”的攻击操纵区块时间…...
浅谈可重入与线程安全
文章目录 可重入与线程安全的关系 可重入 若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另一段代码,这段代码又使用了该副程序不会出错”,则称其为可重入(reentrant 或 re-entrant)的。即当该副程序正在运作时&…...
深入理解TDD(测试驱动开发):提升代码质量的利器
在日常的软件开发工作中,我们常常会遇到这样的问题:如何在繁忙的项目进度中,保证我们的代码质量?如何在不断的迭代更新中,避免引入新的错误?对此,有一种有效的开发方式能帮助我们解决这些问题&a…...
pyqt5使用pyqtgraph实现动态热力图
pyqt5使用pyqtgraph实现动态热力图 一、效果图 二、流程 1、打开Designer创建一个UI界面 2、把UI转成py 3、创建一个main.py文件 4、在main文件中渲染画布、创建初始数据、画热力图、创建更新数据线程、绑定按钮触发事件三、UI界面 其中h_map.py代码如下: # -*- coding: ut…...
【android开发-16】android中文件和sharedpreferences数据存储详解
1,文件读写方式的数据存储 下面是一个简单的示例,演示如何在Android中使用内部存储来保存和读取文件: 保存文件: try { String data "这是要保存的数据"; FileOutputStream fos openFileOutput("myFile"…...
《当代家庭教育》期刊论文投稿发表简介
《当代家庭教育》杂志是家庭的参谋和助手,社会的桥梁和纽带,人生的伴侣和知音,事业的良师益友。 国家新闻出版总署批准的正规省级教育类G4期刊,知网、维普期刊网收录。安排基础教育相关稿件,适用于评职称时的论文发表…...
【操作教程】如何将外省医保转入广州市区(医保转移接续手续办理)?
登录(可以用微信扫码采用粤省事账号登录,没有粤省事小程序账号的可以自主申请很方便)广东政务服务网https://www.gdzwfw.gov.cn/ 这里不得不吐槽官网开发者,太拉胯了,居然有undefined,多刷新几次就好了&…...
【分布式系统学习】CAP原理详解
CAP原理详解 前言CAP一张图 一、概念1.1 关键词解读1.2 关于CAP(拆分解读)1.3 CAP原理精髓 二、CAP模拟场景举例理解三、CAP原理证明为什么不能同时满足(下面举例说明)3.1 必须满足分区容错性P下的处理方式3.2 不是必须满足分区容…...
第19节 Node.js Express 框架
Express 是一个为Node.js设计的web开发框架,它基于nodejs平台。 Express 简介 Express是一个简洁而灵活的node.js Web应用框架, 提供了一系列强大特性帮助你创建各种Web应用,和丰富的HTTP工具。 使用Express可以快速地搭建一个完整功能的网站。 Expre…...
CTF show Web 红包题第六弹
提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框,很难让人不联想到SQL注入,但提示都说了不是SQL注入,所以就不往这方面想了 先查看一下网页源码,发现一段JavaScript代码,有一个关键类ctfs…...
C++:std::is_convertible
C++标志库中提供is_convertible,可以测试一种类型是否可以转换为另一只类型: template <class From, class To> struct is_convertible; 使用举例: #include <iostream> #include <string>using namespace std;struct A { }; struct B : A { };int main…...
镜像里切换为普通用户
如果你登录远程虚拟机默认就是 root 用户,但你不希望用 root 权限运行 ns-3(这是对的,ns3 工具会拒绝 root),你可以按以下方法创建一个 非 root 用户账号 并切换到它运行 ns-3。 一次性解决方案:创建非 roo…...
鱼香ros docker配置镜像报错:https://registry-1.docker.io/v2/
使用鱼香ros一件安装docker时的https://registry-1.docker.io/v2/问题 一键安装指令 wget http://fishros.com/install -O fishros && . fishros出现问题:docker pull 失败 网络不同,需要使用镜像源 按照如下步骤操作 sudo vi /etc/docker/dae…...
爬虫基础学习day2
# 爬虫设计领域 工商:企查查、天眼查短视频:抖音、快手、西瓜 ---> 飞瓜电商:京东、淘宝、聚美优品、亚马逊 ---> 分析店铺经营决策标题、排名航空:抓取所有航空公司价格 ---> 去哪儿自媒体:采集自媒体数据进…...
MySQL用户和授权
开放MySQL白名单 可以通过iptables-save命令确认对应客户端ip是否可以访问MySQL服务: test: # iptables-save | grep 3306 -A mp_srv_whitelist -s 172.16.14.102/32 -p tcp -m tcp --dport 3306 -j ACCEPT -A mp_srv_whitelist -s 172.16.4.16/32 -p tcp -m tcp -…...
Mysql8 忘记密码重置,以及问题解决
1.使用免密登录 找到配置MySQL文件,我的文件路径是/etc/mysql/my.cnf,有的人的是/etc/mysql/mysql.cnf 在里最后加入 skip-grant-tables重启MySQL服务 service mysql restartShutting down MySQL… SUCCESS! Starting MySQL… SUCCESS! 重启成功 2.登…...
AxureRP-Pro-Beta-Setup_114413.exe (6.0.0.2887)
Name:3ddown Serial:FiCGEezgdGoYILo8U/2MFyCWj0jZoJc/sziRRj2/ENvtEq7w1RH97k5MWctqVHA 注册用户名:Axure 序列号:8t3Yk/zu4cX601/seX6wBZgYRVj/lkC2PICCdO4sFKCCLx8mcCnccoylVb40lP...
渗透实战PortSwigger Labs指南:自定义标签XSS和SVG XSS利用
阻止除自定义标签之外的所有标签 先输入一些标签测试,说是全部标签都被禁了 除了自定义的 自定义<my-tag onmouseoveralert(xss)> <my-tag idx onfocusalert(document.cookie) tabindex1> onfocus 当元素获得焦点时(如通过点击或键盘导航&…...
