由一个 @RequestBody 注解引起我的深思

tech2024-11-15  16

以下内容并不保证其准确性,但具有可参考性。

章节目录

1. 背景1.1 HTTP 协议1.2 Request 请求对象1.3 响应消息 Response 2. Content-Type2.1 Content-Type 是什么?2.2 Content-Type 的常用类型2.2.1 application/x-www-form-urlencoded2.2.2 multipart/form-data2.2.3 application/json2.2.4 text/xml2.2.5 text/plain 2.3 application/x-www-form-urlencoded 与 application/json 的区别2.3.1 区别一2.3.2 区别二 3. @RequestBody、@RequestParam注解3.1 区别3.1.1 @RequestParam 注解3.1.2 @RequestBody 注解 3.2 Controller 层的方法的多个入参无法使用多个 @RequestBody注解3.3 @RequestBody 接收 JSON 字符串数组

1. 背景

1.1 HTTP 协议

​ HTTP 协议 是一个应用层协议,它是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web )服务器传输超文本到本地浏览器的传送协议。

​ HTTP 协议 规定:POST 提交的数据必须包含在消息主体中的 Entity-body 中,但是协议并没有规定数据使用什么编码方式。开发者可以自己决定消息主体的格式。

​ 数据发送出去后,需要(接收的)服务器解析,一般服务器会根据 Content-Type 字段来获取参数是如何编码的,然后,再对应去解码。

1.2 Request 请求对象

客户端发送一个HTTP请求到服务器的请求消息包括以下格式:

请求行请求头部空行请求数据

如下图: GET 请求:

GET /562f25980001b1b106000338.jpg HTTP/1.1 Host img.mukewang.com User-Agent Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36 Accept image/webp,image/*,*/*;q=0.8 Referer http://www.imooc.com/ Accept-Encoding gzip, deflate, sdch Accept-Language zh-CN,zh;q=0.8

第一部分:请求行,用来说明请求类型,要访问的资源以及所使用的HTTP版本。 GET说明请求类型为GET,[/562f25980001b1b106000338.jpg]为要访问的资源,该行的最后一部分说明使用的是HTTP1.1版本。

第二部分:请求头部,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息 从第二行起为请求头部,HOST将指出请求的目的地.User-Agent,服务器端和客户端脚本都能访问它,它是浏览器类型检测逻辑的重要基础.该信息由你的浏览器来定义,并且在每个请求中自动发送等等

第三部分:空行,请求头部后面的空行是必须的 即使第四部分的请求数据为空,也必须有空行。

第四部分:请求数据也叫主体,可以添加任意的其他数据。这个例子的请求数据为空。

POST请求

POST / HTTP1.1 Host:www.wrox.com User-Agent:Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022) Content-Type:application/x-www-form-urlencoded Content-Length:40 Connection: Keep-Alive name=Professional%20Ajax&publisher=Wiley 第一部分:请求行,第一行明了是post请求,以及http1.1版本。第二部分:请求头部,第二行至第六行。第三部分:空行,第七行的空行。第四部分:请求数据,第八行。

1.3 响应消息 Response

一般情况下,服务器接收并处理客户端发过来的请求后会返回一个HTTP的响应消息。 HTTP响应也由四个部分组成,分别是:

状态行消息报头空行响应正文

如下图:

HTTP/1.1 200 OK Date: Fri, 22 May 2009 06:07:21 GMT Content-Type: text/html; charset=UTF-8 <html> <head></head> <body> <!--body goes here--> </body> </html>

第一部分:状态行,由HTTP协议版本号, 状态码, 状态消息 三部分组成。 第一行为状态行,(HTTP/1.1)表明HTTP版本为1.1版本,状态码为200,状态消息为(ok)

第二部分:消息报头,用来说明客户端要使用的一些附加信息 第二行和第三行为消息报头, Date:生成响应的日期和时间;Content-Type:指定了MIME类型的HTML(text/html),编码类型是UTF-8

第三部分:空行,消息报头后面的空行是必须的

第四部分:响应正文,服务器返回给客户端的文本信息。 空行后面的html部分为响应正文。

2. Content-Type

2.1 Content-Type 是什么?

​ Content-Type 是 HTTP 的实体首部字段,在 request 的请求行或 response 的状态之后(在request header 和 response header 里都存在),也是首部的一部分,用于说明请求或返回的消息主体是用何种方式编码。

2.2 Content-Type 的常用类型

2.2.1 application/x-www-form-urlencoded

浏览器表单 POST 提交的默认方式;

POST 方式:把 form 中的数据封装到 HTTP Body 中,然后,发送到 server。 GET 方式:提交的数据按照 key1=value1&key2=value2 的方式进行编码(标准的编码格式)。其中, key 和 value 都进行了 URL 转码。就是 URL 里面的 QueryString

可直接使用 request.getParamater() 方法获取参数(request.getInputStream() 或 request.getReader() 可获取到请求内容,再解析出具体的参数)。

2.2.2 multipart/form-data

常见的 POST 数据提交的方式;

支持向服务器发送二进制数据;

多用于 文件上传(使用表单上传文件时,必须让 form 的 enctype 等于这个值),表单数据都保存在 HTTP 的正文部分,各个表单项之间用 boundary 分开;

<form action="/" method="post" enctype="multipart/form-data"> <input type="text" name="description" value="some text"> <input type="file" name="myFile"> <button type="submit">Submit</button> </form> POST /foo HTTP/1.1 Content-Length: 68137 Content-Type: multipart/form-data; boundary=---------------------------974767299852498929531610575 ---------------------------974767299852498929531610575 Content-Disposition: form-data; name="description" Content-Type: some text ---------------------------974767299852498929531610575 Content-Disposition: form-data; name="myFile"; filename="foo.txt" Content-Type: text/plain (content of the uploaded file foo.txt) --974767299852498929531610575--

​ 首先生成了一个 boundary(很长很复杂,为了避免与正文内容重复。服务器会根据这个边界解析数据,划分段,每一段就是一项数据)用于分割不同的字段 。然后 Content-Type 里指明了数据是以 multipart/form-data 来编码,本次请求的 boundary 是什么内容。

​ 消息主体里按照字段个数又分为多个结构类似的部分,每部分都是以 --boundary 开始,紧接着是内容描述信息,然后是回车,最后是字段具体内容(文本或二进制)。

​ 如果传输的是文件,还要包含文件名和文件类型信息。消息主体最后以 --boundary-- 标示结束。

通过 request.getInputStream() 方法获取数据(request.getParamater() 方法是无法获取数据的),但它获取的是一个 InputStream,无法直接取到指定的表单项。但可直接利用开源组件获取表单项。如: Apache 的 fileupload 组件。

ServletFileUpload upload = new ServletFileUpload(factory); List<FileItem> list = upload.parseRequest(request); // 遍历 list 来获取参数

使用 SpringBoot 上传文件:

public class User { private String username; private String password; private String touXiang; }

实体类中的属性 touXiang 对应着下面的 html 页面的 file 类型。

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Index</title> </head> <body> <h1>Hello World</h1> <form action="/user/login" method="post" enctype="multipart/form-data"> <p>账号:<input type="text" name="username" /></p> <p>密码:<input type="password" name="password" /></p> <p>头像:<input type="file" name="file" /></p> <input type="submit" value="登陆" /> </form> </body> </html>

上述的 typen = “file” 的输入框的 name 的值(file) 不要和实体类中的属性名 touXiang 一致。

@RestController @RequestMapping("/user") public class UserController { private static final String FILEPATH = "E:/sql/"; @RequestMapping("/login") public User login(String username, String password, MultipartFile file) { String touXiang = FILEPATH + file.getOriginalFilename(); User user = new User(); user.setUsername(username); user.setPassword(password); user.setTouXiang(touXiang); try { file.transferTo(new File(touXiang)); } catch (IOException e) { e.printStackTrace(); } return user; } }

入参类型为 MultipartFile 的参数名(file)要和前端的 name 的值一致。

使用 SpringBoot 下载文件:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Index</title> </head> <body> <button onclick="downLoad()">导出</button> </body> <script type="application/javascript"> function downLoad() { window.location.href = '/user/download' } </script> </html> @RestController @RequestMapping("/user") public class UserController { private static final String FILEPATH = "E:/sql/"; @RequestMapping("/download") public void downloadFile(HttpServletResponse response) throws Exception{ String path = FILEPATH + "1.jpg"; File file = new File(path); if (!file.exists()) { throw new RuntimeException("文件不存在"); } // 配置文件下载 response.setContentType("application/force-download"); // 设置文件中文名乱码 response.addHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode(file.getName(), "UTF-8")); // 实现文件下载 byte[] bytes = new byte[1024]; try(FileInputStream inputStream = new FileInputStream(file); BufferedInputStream in = new BufferedInputStream(inputStream)) { OutputStream out = response.getOutputStream(); int len = in.read(bytes); while (len != -1) { out.write(bytes, 0, len); len = in.read(bytes); } } } }

2.2.3 application/json

消息主体是序列化后的 JSON 字符串;

{"title":"test","sub":[1,2,3]}

由于 JSON 规范的流行,各大浏览器都开始原生支持 JSON.stringfy();

Spring 对它上传的数据有很好的支持,可以直接通过 @RequestBody 进行接收;

可以方便地提交复杂的结构化数据,特别适合 RESTful 的接口

2.2.4 text/xml

它是一种使用 HTTP 作为传输协议,XML 作为编码方式的远程调用规范。

Content-Type: text/xml <!--?xml version="1.0"?--> <methodcall> <methodname>examples.getStateName</methodname> <params> <param> <value><i4>41</i4></value> </param> </params> </methodcall>

2.2.5 text/plain

数据以纯文本形式(text/json/xml/html)进行编码,其中不含任何控件或格式字符。

2.3 application/x-www-form-urlencoded 与 application/json 的区别

2.3.1 区别一

application/x-www-form-urlencoded:浏览器表单提交的默认方式、JQuery 的 AJAX 请求的默认方式。键值对形式:key1=value1&key2=value2

application/json:JSON 字符串格式

2.3.2 区别二

application/x-www-form-urlencoded:对象接收、@RequestParam注解 接收

application/json(axios默认使用):只能以 @ResquestBody注解(只能接收单个参数) 接收对象

3. @RequestBody、@RequestParam注解

3.1 区别

@RequestBody、@RequestParam注解都作用于 Controller 层的接口,用来获取参数。

@RequestParam:获取参数。请求方式:GET、POST@RequestBody:将 JSON 格式的字符串转换为 JAVA对象。请求方式:POST。@RequestBody注解是用来接收请求体的参数,由于 GET 请求的参数是拼接在 url 后面(且是键值对形式,不是 json字符串),位于请求头中,并非请求体。故,无法接受 GET 请求的参数。而 POST 请求的参数正是位于请求体中。故,能接收 POST 请求的参数。

3.1.1 @RequestParam 注解

@RequestParam注解 只能接收 Content-Type=application/x-www-form-urlencoded, GET、POST请求方式

public class User { private String username; private String password; // getter()/setter() }

GET/POST 请求:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Index</title> </head> <body> <!--默认表单的Content-Type=application/x-www-form-urlencoded--> <form action="/user/login" method="get"> <p>账号:<input type="text" name="username" /></p> <p>密码:<input type="password" name="password" /></p> <input type="submit" value="登陆" /> </form> </body> </html>

后台接收:

@RestController @RequestMapping("/user") public class UserController { // 1. 直接使用参数接收 @GetMapping("/login") public User login() { user.setUsername(username); user.setPassword(password); return user; } // 2. 使用对象接收 @GetMapping("/login") public User login(User user) { return user; } // 3. 使用 @RequestParam 注解接收。其中,@RequestParam(val) 中的 val 是与前端传来的参数名一致(name对应的值)。 // 如:将 name="password" 改为 name="pwd" @GetMapping("/login") public User login(@RequestParam("username") String username, @RequestParam("pwd") String password) { return user; } }

@RequestParam注解 接收 application/json 格式编码的参数会报错。

3.1.2 @RequestBody 注解

只接收 application/json 格式编码的参数。

使用 POSTMAN 进行 POST 请求

@PostMapping(value = "/login") public User login(@RequestBody User user) { return user; }

3.2 Controller 层的方法的多个入参无法使用多个 @RequestBody注解

​ @RequestBody注解是在当前的对象中只获取一次整个 HTTP 请求的 body 里面的所有数据,因此, Spring 就不可能将这个数据强制包装成 A 参数和 B参数,也就没必要在 Controller 层的方法的形参列表中出现多个 @RequestBody 注解。

public class Address { private String province; private String city; // getter()/setter() }

后台接收:

@PostMapping(value = "/login") public User login(@RequestBody User user, @RequestBody Address address) { return user; }

请求错误。

解决方案:把 User 和 Address 封装为一个大对象 Combine

public class Combine { private User user; private Address address; }

后台接收:

@PostMapping(value = "/login") public Combine login(@RequestBody Combine combine) { return combine; }

3.3 @RequestBody 接收 JSON 字符串数组

public class User { private String id; private String username; // getter()/setter() } public class Team { private String teamName; // 或者是数组: private User [] users; private List<User> users; // getter()/setter() }

前端请求:

后端接收:

@RestController @RequestMapping("/user") public class UserController { @PostMapping("/team") public Team team(@RequestBody Team team) { return team; } }
最新回复(0)