本文内容
本文主要是记录一下 springboot 中对于 HTTP 请求的各种参数绑定的详情
查询参数绑定
http://localhost:8080/hello?str=sss&more=xxx
类似于这种问号后面加上参数列表的方式,可以使用 DTO 对象的方式来接收数据,也可以使用单独的参数来接收数据,controller 和 DTO 可以这样写:
@GetMapping("/hello")
public String hello(HelloDTO helloDTO, String more) {
return "aaa" + helloDTO.getStr() + more;
}
@Data
public class HelloDTO {
private String str;
private Integer number;
private List<String> strList;
private List<Integer> numberList;
private String[] strArr;
private Integer[] numberArr;
private HelloDTO innerDTO;
private HelloDTO[] innerDTOArr;
private List<HelloDTO> innerDTOList;
}
// 返回 aaasssxxx
可以看到, str 参数自动绑定到了 HelloDTO,more 参数自动绑定到了 controller 第二个 String 参数 more。
重名参数
还是以下面请求为例。
http://localhost:8080/hello?str=sss&more=xxx
@GetMapping("/hello")
public String hello(HelloDTO helloDTO, String str) {
return "aaa" + helloDTO.getStr() + str;
}
@Data
public class HelloDTO {
private String str;
private Integer number;
private List<String> strList;
private List<Integer> numberList;
private String[] strArr;
private Integer[] numberArr;
private HelloDTO innerDTO;
private HelloDTO[] innerDTOArr;
private List<HelloDTO> innerDTOList;
}
// 返回 aaassssss
注意这里,HTTP 查询参数 str 既被绑定到了 DTO 也被绑定到了第二个 controller 方法参数 str。
如果我们把第二个参数 str 改成 Integer 类型,那么绑定 DTO 的时候不会出错,绑定第二个 str 的时候会出错,就会导致绑定错误
这种行为可以理解成:springboot 的参数绑定器,将所有的请求参数,依次对每个controller方法参数进行绑定,所以处理同名参数的时候会绑定2次
我们可以修改这种行为
@GetMapping("/hello")
public String hello(HelloDTO helloDTO, String str) {
return "aaa" + helloDTO.getStr() + str;
}
@InitBinder("helloDTO")
public void initHelloDTO(WebDataBinder binder){
binder.setFieldDefaultPrefix("dto.");
}
@Data
public class HelloDTO {
private String str;
private Integer number;
private List<String> strList;
private List<Integer> numberList;
private String[] strArr;
private Integer[] numberArr;
private HelloDTO innerDTO;
private HelloDTO[] innerDTOArr;
private List<HelloDTO> innerDTOList;
}
GET http://localhost:8080/hello?dto.str=sss&more=xxx
结果:aaasssnull
在本 Controller 中添加一个 @InitBinder 注解的方法,注解的参数表示要修改绑定的参数的名称,例如这里是 helloDTO ,与 controller 方法的第一个参数 (HelloDTO helloDTO) 比对上了,这个 binder 方法的作用就是说:给 helloDTO 参数多添加一种绑定方式,这种方式就是使用前缀 dto.
注意这里是多添加一种绑定方式,也就是说 dto.str 和 str 都可以绑定到 helloDTO 的 str 参数。
另外,如果这种行为感到比较迷惑的话,可以使用 @ModelAttribute 注解
@GetMapping("/hello")
public String hello(@ModelAttribute("helloDTO") HelloDTO helloDTO,
@ModelAttribute("helloDTO2") HelloDTO another) {
return "aaa" + helloDTO.getStr() + another.getStr();
}
@InitBinder("helloDTO")
public void initHelloDTO(WebDataBinder binder) {
binder.setFieldDefaultPrefix("dto.");
}
@InitBinder("helloDTO2")
public void initHelloDTO2(WebDataBinder binder) {
binder.setFieldDefaultPrefix("dto2.");
}
这样,dto2 开头的就绑定到第二个参数上,dto 开头的就绑定到第一个参数上
GET http://localhost:8080/hello?dto.str=sss&dto2.str=xxx
结果:aaasssxxx
这种实际上就是为参数单独设置了绑定的行为
内部嵌套对象参数绑定
HelloDTO 内部有一个对象叫做 innerDTO,这也是一个 DTO,如果想要给这种内部嵌套的对象赋值,需要使用如下的方法:
@GetMapping("/hello")
public String hello(HelloDTO helloDTO) {
return "aaa" + helloDTO.getStr() + helloDTO.getInnerDTO().getStr();
}
@Data
public class HelloDTO {
private String str;
private Integer number;
private List<String> strList;
private List<Integer> numberList;
private String[] strArr;
private Integer[] numberArr;
private HelloDTO innerDTO;
private HelloDTO[] innerDTOArr;
private List<HelloDTO> innerDTOList;
}
GET http://localhost:8080/hello?str=sss&innerDTO.str=mmm
结果:aaasssmmm
可以看到,我们使用 innerDTO.str 这种嵌套方式,绑定到了内部 innerDTO 对象的 str 属性
List参数绑定
@GetMapping("/hello")
public Object hello(HelloDTO helloDTO) {
return helloDTO.getStrList();
}
@Data
public class HelloDTO {
private String str;
private Integer number;
private List<String> strList;
private List<Integer> numberList;
private String[] strArr;
private Integer[] numberArr;
private HelloDTO innerDTO;
private HelloDTO[] innerDTOArr;
private List<HelloDTO> innerDTOList;
}
GET http://localhost:8080/hello?strList=x&strList=y&strList=z
结果:["x","y","z"]
如果要绑定内部的 List
@GetMapping("/hello")
public Object hello(HelloDTO helloDTO) {
return helloDTO.getInnerDTO().getStrList();
}
@Data
public class HelloDTO {
private String str;
private Integer number;
private List<String> strList;
private List<Integer> numberList;
private String[] strArr;
private Integer[] numberArr;
private HelloDTO innerDTO;
private HelloDTO[] innerDTOArr;
private List<HelloDTO> innerDTOList;
}
GET http://localhost:8080/hello?innerDTO.strList=x&innerDTO.strList=y&innerDTO.strList=z
结果:["x","y","z"]
可以看到,使用 argNum=arg1& argNum=arg2& argNum=arg3 这种形式就可以绑定 List
Set 参数绑定
@GetMapping("/hello")
public Object hello(HelloDTO helloDTO) {
return helloDTO.getStrSet();
}
@Data
public class HelloDTO {
private String str;
private Integer number;
private List<String> strList;
private Set<String> strSet;
private List<Integer> numberList;
private String[] strArr;
private Integer[] numberArr;
private HelloDTO innerDTO;
private HelloDTO[] innerDTOArr;
private List<HelloDTO> innerDTOList;
}
GET http://localhost:8080/hello?strSet=x&strSet=y&strSet=z&strSet=z
结果:["x","y","z"]
可以看到,使用 argNum=arg1& argNum=arg2& argNum=arg3 这种形式也可以绑定 Set,会自动去重
数组参数绑定
@GetMapping("/hello")
public Object hello(HelloDTO helloDTO) {
return helloDTO.getStrArr();
}
@Data
public class HelloDTO {
private String str;
private Integer number;
private List<String> strList;
private Set<String> strSet;
private Map<String,String> strMap;
private List<Integer> numberList;
private String[] strArr;
private Integer[] numberArr;
private HelloDTO innerDTO;
private HelloDTO[] innerDTOArr;
private List<HelloDTO> innerDTOList;
}
GET http://localhost:8080/hello?strArr=1&strArr=x&strArr=k
结果:["1","x","k"]
Map参数绑定
没研究明白怎么把查询参数绑定到DTO内部的map🤣,如果只是绑定外部的 map,可以使用如下方式
@GetMapping("/hello")
public Object hello(@RequestParam Map<String, String> map) {
return map;
}
注意必须要添加 @RequestParam 注解
否则这个 map 就是被注入进来的 ModelMap 了。
(说实话这个技术我基本不怎么用,感觉也没多少人用这个特性了)
GET http://localhost:8080/hello?a=1&b=4
结果:{"a":"1","b":"4"}
RequestParam 注解
@GetMapping("/hello")
public Object hello(@RequestParam String xx) {
return xx;
}
GET http://localhost:8080/hello?xx=s
结果:s
GET http://localhost:8080/hello
结果:报错
@GetMapping("/hello")
public Object hello(@RequestParam(value = "aa", required = false) String xx) {
return xx;
}
GET http://localhost:8080/hello?xx=s
结果:无
GET http://localhost:8080/hello?aa=s
结果:s
RequestParam 主要是定义参数名称和是否必须
当我们不写 RequestParam 时,单独的参数相当于是
@RequestParam(value = 参数名, required = false) String 参数名,默认使用参数的形参名称这里只用于单体参数,用于 DTO 这种实体参数的话有问题,它会单独取这一个参数转换成实体DTO类型,会转换异常
总结
- List、Set、数组的绑定方式是一样的,查询参数都是
argNum=arg1& argNum=arg2& argNum=arg3的形式 - 重名参数默认会绑定到所有重名的地方
- 如果想避免重名参数,可以使用 @InitBinder 注解,来添加参数前缀
- 嵌套内部对象参数的绑定方式是使用参数前缀
- 单体参数省略了 RequestParam 注解,默认使用的是参数名进行绑定
请求路径PATH参数
请求路径PATH参数表示URL里的参数,在springboot中使用 pathVariable 进行绑定。
@GetMapping("/hello/{path}")
public Object hello(@PathVariable String path) {
return path;
}
GET http://localhost:8080/hello/path1
结果:path1
GET http://localhost:8080/hello/
结果:错误
PathVariable 有两个参数,一个是绑定名称一个是是否必须有
@GetMapping("/hello/{path}")
public Object hello(@PathVariable(name = "path") String path) {
return path;
}
这个跟上面的定义是一样的,@PathVariable(name = "path") 表示绑定到 @GetMapping("/hello/{path}") 的 path 变量。
另外还有一个 required 参数表示这个参数是否是必须的
总体来说这个参数的使用比较简单
请求HEADER参数
@GetMapping("/hello")
public Object hello(@RequestHeader("My-Header") String header) {
return header + header;
}
GET localhost:8080/hello
My-Header: xxx
结果:xxxxxx
使用 @RequestHeader 注解来获取 Header 参数
也可以一次性接收所有的 Header
@GetMapping("/hello")
public Object hello(@RequestHeader Map<String, String> header) {
return header;
}
GET localhost:8080/hello
My-Header: xxx
cookie: abc=jkl
结果:
{
"my-header": "xxx",
"cookie": "abc=jkl",
"host": "localhost:8080",
"connection": "Keep-Alive",
"user-agent": "Apache-HttpClient/4.5.10 (Java/11.0.5)",
"accept-encoding": "gzip,deflate"
}
这里多余的头部是我使用的HTTP客户端自动补充的
除了
@RequestHeader Map<String, String> header也可以使用 HttpHeaders 来接收所有头部
获取Cookie
@GetMapping("/hello")
public Object hello(@RequestHeader("My-Header") String header, @CookieValue("abc") String cookie) {
return header + cookie;
}
GET localhost:8080/hello
My-Header: xxx
cookie: abc=jkl
使用 @CookieValue("abc") 获取到 cookie 中关于 abc 的值
application/json 类型的 body 参数
json绑定到DTO
@PostMapping("/hello")
public Object hello(@RequestBody HelloDTO helloDTO) {
return helloDTO.getStr();
}
POST localhost:8080/hello
Content-Type: application/json
{
"str": "sttttt"
}
结果:
sttttt
使用 @RequestBody 绑定body参数到 DTO 上
json 内部嵌套对象
@PostMapping("/hello")
public Object hello(@RequestBody HelloDTO helloDTO) {
return helloDTO.getStr() + helloDTO.getInnerDTO().getStr();
}
POST localhost:8080/hello
Content-Type: application/json
{
"str": "sttttt",
"innerDTO": {
"str": "mmmm"
}
}
结果:
stttttmmmm
使用 json 的嵌套格式即可,不需要使用 前缀名.xxx 来定义嵌套对象
json 映射为 List、数组、Set、Map
@PostMapping("/hello")
public Object hello(@RequestBody HelloDTO helloDTO) {
return helloDTO;
}
@Data
public class HelloDTO {
@JsonIgnore
private String str;
@JsonIgnore
private Integer number;
private List<String> strList;
private Set<String> strSet;
private Map<String, String> strMap;
private String[] strArr;
@JsonIgnore
private HelloDTO innerDTO;
@JsonIgnore
private HelloDTO[] innerDTOArr;
@JsonIgnore
private List<HelloDTO> innerDTOList;
}
POST localhost:8080/hello
Content-Type: application/json
{
"strList": ["x","y","z","z"],
"strSet": ["x","y","z","z"],
"strMap": {
"a": "aa",
"b": "bbb"
},
"strArr": ["x","y","z","z"]
}
结果:
{"strList":["x","y","z","z"],"strSet":["x","y","z"],"strMap":{"a":"aa","b":"bbb"},"strArr":["x","y","z","z"]}
可以看到,很符合直觉
总结
- json 只能被读取一次,如果想绑定到两个实体,是不可以的
- json 直接映射成 DTO 是很方便的,但是不能映射成单体参数,例如单独的 Integer
- 可以修改 mvcConfig 来调整映射的行为,重写 configureMessageConverters() 方法来替代默认的转换器,或者 重写 extendMessageConverters() 方法来自定义默认的转换器,或者添加新的转换器
multipart/form-data 类型的 body 参数
@PostMapping("/multi")
public Object multi(HelloDTO helloDTO){
return helloDTO.getStr() + helloDTO.getInnerDTO().getStr();
}
POST http://localhost:8080/multi
Content-Type: multipart/form-data; boundary=WebAppBoundary
--WebAppBoundary
Content-Disposition: form-data; name="str"
sss
--WebAppBoundary--
Content-Disposition: form-data; name="innerDTO.str"
xxx
--WebAppBoundary--
以上的 HTTP 请求,就是使用 POST 请求,参数类型是 multipart/form-data,参数分别是 str=sss 和 innerDTO.str = xxx
最后的结果是:sssxxx
可以看出,这种类型的数据绑定特点和查询参数很相似,传递内部嵌套参数、List、Set、数组的方式与查询参数是一致的
接收文件
以上是简单的类似于使用查询参数的方式,实际上 multipart/form-data 还可以传递更复杂的参数,例如文件、JSON 等。
@PostMapping("/file")
public Object file(MultipartFile file1, String arg1) throws IOException {
return new String(file1.getBytes()) + arg1;
}
POST http://localhost:8080/file
Content-Type: multipart/form-data; boundary=WebAppBoundary
--WebAppBoundary
Content-Disposition: form-data; name="arg1"
Content-Type: text/plain
value1
--WebAppBoundary
Content-Disposition: form-data; name="file1"; filename="data.json"
Content-Type: text/plain
< ./../../pom.xml
--WebAppBoundary--
这里的 HTTP 请求,使用 multipart/form-data 传递了两个参数,分别是文本和文件。文件是当前的 pom.xml ,这里的路径是 idea 里定义的相对路径,总之最后会确定到一个文件。文本的值是 value1。
结果是:
..... 省略
</build>
</project>
value1
可以看到成功接收了这两个参数。
这种用法基本与查询参数绑定到单体参数是一致的。
RequestPart 绑定多个复杂 JSON
@PostMapping("/twoJson")
public Object twoJson(@RequestPart("h1") HelloDTO helloDTO, @RequestPart("h2") HelloDTO helloDTO2) {
return new HelloDTO[]{helloDTO, helloDTO2};
}
POST http://localhost:8080/twoJson
Content-Type: multipart/form-data; boundary=WebAppBoundary
--WebAppBoundary
Content-Disposition: form-data; name="h1"
Content-Type: application/json
{
"strList": ["x1","y1","z1","z1"],
"strSet": ["x1","y1","z1","z1"],
"strMap": {
"a": "aa1",
"b": "bbb1"
},
"strArr": ["x1","y1","z1","z1"]
}
--WebAppBoundary--
Content-Disposition: form-data; name="h2"
Content-Type: application/json
{
"strList": ["x2","y2","z2","z2"],
"strSet": ["x2","y2","z2","z2"],
"strMap": {
"a": "aa2",
"b": "bbb2"
},
"strArr": ["x2","y2","z2","z2"]
}
--WebAppBoundary--
结果是:
[
{
"strList": [
"x1",
"y1",
"z1",
"z1"
],
"strSet": [
"z1",
"y1",
"x1"
],
"strMap": {
"a": "aa1",
"b": "bbb1"
},
"strArr": [
"x1",
"y1",
"z1",
"z1"
]
},
{
"strList": [
"x2",
"y2",
"z2",
"z2"
],
"strSet": [
"z2",
"y2",
"x2"
],
"strMap": {
"a": "aa2",
"b": "bbb2"
},
"strArr": [
"x2",
"y2",
"z2",
"z2"
]
}
]
可以看到,RequestPart 使用 multipart/form-data 把每个 part 当成一个 json 来解析了。
总结
- RequestPart 所代表的 multipart/form-data 参数里每个 part 很像是查询参数的用法
- 每个 part 可以代表更复杂的数据包括文件、json 等,使用 json 的时候一个 part 就类似于 application/json 类型的整个 body 参数
- 这种方式可以适用于参数十分复杂的情况,文件、json、单个参数都可以用这种方式来传递数据。
application/x-www-form-urlencoded 类型的 body 参数
这种参数类似于查询参数的格式
@PostMapping("/urlencoded")
public Object urlencoded(HelloDTO helloDTO) {
return helloDTO.getStr() + helloDTO.getInnerDTO().getStr();
}
POST http://localhost:8080/urlencoded
Content-Type: application/x-www-form-urlencoded
str=xxx&innerDTO.str=mmm
结果:
xxxmmm
可以看到,这种形式的参数,其实就是将 URL 里的查询参数放到了 POST 请求的 BODY 里面。
springboot 对这种参数的解析,与查询参数是完全一致的,甚至会和它混合起来。
POST http://localhost:8080/urlencoded?innerDTO.str=mmm
Content-Type: application/x-www-form-urlencoded
str=aaa
结果:
aaammm
这里就看出来,Springboot 默认的参数绑定行为是将两者直接混合了,我们使用 RequestParam 注解的效果也与查询参数是完全一致的,这里就不再重复了。
body 参数总结
- 常用的三种是:application/json、multipart/form-data、application/x-www-form-urlencoded
- application/json 只能将整个 body 当成 json 绑定一次,无法绑定多个参数
- application/x-www-form-urlencoded 基本等同于查询参数,只是把它从URL里挪到了body里
- multipart/form-data 最灵活
- 简单使用时类似于查询参数的键值对
- 复杂使用时,每个 part 部分都可以是复杂参数,常用的可以是文件、json 等
- 遇到复杂参数直接使用 multipart/form-data 是最容易满足需求的,一般文件上传接口就用这个