以下内容并不保证其准确性,但具有可参考性。
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;
}
}