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

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

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

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

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

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

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
  1. 第一部分:请求行,第一行明了是post请求,以及http1.1版本。
  2. 第二部分:请求头部,第二行至第六行。
  3. 第三部分:空行,第七行的空行。
  4. 第四部分:请求数据,第八行。

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>
  1. 第一部分:状态行,由HTTP协议版本号, 状态码, 状态消息 三部分组成。
    第一行为状态行,(HTTP/1.1)表明HTTP版本为1.1版本,状态码为200,状态消息为(ok)

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

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

  4. 第四部分:响应正文,服务器返回给客户端的文本信息。
    空行后面的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

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

    POST 方式:把 form 中的数据封装到 HTTP Body 中,然后,发送到 server。
    GET 方式:提交的数据按照 key1=value1&key2=value2 的方式进行编码(标准的编码格式)。其中, key 和 value 都进行了 URL 转码。就是 URL 里面的 QueryString
    
  2. 可直接使用 request.getParamater() 方法获取参数(request.getInputStream() 或 request.getReader() 可获取到请求内容,再解析出具体的参数)。

2.2.2 multipart/form-data

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

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

  3. 多用于 文件上传(使用表单上传文件时,必须让 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-- 标示结束。

  4. 通过 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

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

    {"title":"test","sub":[1,2,3]}
    
  2. 由于 JSON 规范的流行,各大浏览器都开始原生支持 JSON.stringfy();

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

  4. 可以方便地提交复杂的结构化数据,特别适合 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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值