影子的知识库

影子的知识库

  • 知识库
  • GitHub

›Java系列

JVM系列

  • JVM内存区域
  • 对象创建-布局-访问
  • 内存溢出实战
  • 内存区域回收
  • 四大引用
  • 垃圾回收算法
  • HotSpot回收算法细节

Java系列

  • java注解
  • springboot请求参数绑定
  • springboot请求参数校验框架
  • YAML语法
  • 动态代理
  • classpath和java命令
  • springboot-aop编程
  • springboot统一异常处理
  • springboot数据库和事务
  • springboot拦截器
  • springboot中的web配置
  • docker的简单开发
  • springboot自动配置
  • 数据库的隔离级别
  • springboot监控
  • java类加载
  • java-agent的相关内容
  • 类加载器详解
  • java的SecurityManager
  • maven学习

Node

    JS 基础

    • 语法基础和数据类型
    • 数据类型转换
    • 语句 表达式 运算符
    • 变量与对象
    • 函数
    • 数据处理
    • 常用 API
    • 重点知识

    ES6

    • 块级作用域
    • 字符串和正则表达式
    • 函数
    • 对象
    • Symbol
    • Set和Map
    • 迭代器和生成器
    • 类
    • 数组
    • Promise

    Node 基础

    • 模块系统
    • package.json
    • 内置对象
    • npm脚本的使用
    • Buffer
    • Stream
    • 事件循环机制
    • 示例代码

    stream系列

    • 流的缓冲
    • 可读流
    • 可写流
    • 双工流和转换流
    • 自定义流

后期计划

  • 学习计划
  • 专题研究计划
Edit

本文内容

本文主要是记录一下 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 是最容易满足需求的,一般文件上传接口就用这个
Last updated on 11/8/2020
← java注解springboot请求参数校验框架 →
  • 查询参数绑定
    • 重名参数
    • 内部嵌套对象参数绑定
    • List参数绑定
    • Set 参数绑定
    • 数组参数绑定
    • Map参数绑定
    • RequestParam 注解
    • 总结
  • 请求路径PATH参数
  • 请求HEADER参数
    • 获取Cookie
  • application/json 类型的 body 参数
    • json绑定到DTO
    • json 内部嵌套对象
    • json 映射为 List、数组、Set、Map
    • 总结
  • multipart/form-data 类型的 body 参数
    • 接收文件
    • RequestPart 绑定多个复杂 JSON
    • 总结
  • application/x-www-form-urlencoded 类型的 body 参数
  • body 参数总结
影子的知识库
Docs
Getting Started (or other categories)Guides (or other categories)API Reference (or other categories)
Community
User ShowcaseStack OverflowProject ChatTwitter
More
BlogGitHub
Copyright © 2020 Cen ZhiPeng