在日常使用SpringMVC
进行开发的时候,有可能遇到前端各种类型的请求参数,这里做一次相对全面的总结。SpringMVC
中处理控制器参数的接口是HandlerMethodArgumentResolver
,此接口有众多子类,分别处理不同(注解类型)的参数,下面只列举几个子类:
RequestParamMethodArgumentResolver
:解析处理使用了@RequestParam
注解的参数、MultipartFile
类型参数和Simple
类型(如long
、int
等类型)参数。RequestResponseBodyMethodProcessor
:解析处理@RequestBody
注解的参数。PathVariableMapMethodArgumentResolver
:解析处理@PathVariable
注解的参数。实际上,一般在解析一个控制器的请求参数的时候,用到的是HandlerMethodArgumentResolverComposite
,里面装载了所有启用的HandlerMethodArgumentResolver
子类。而HandlerMethodArgumentResolver
子类在解析参数的时候使用到HttpMessageConverter
(实际上也是一个列表,进行遍历匹配解析)子类进行匹配解析,常见的如MappingJackson2HttpMessageConverter
(使用Jackson
进行序列化和反序列化)。而HandlerMethodArgumentResolver
子类到底依赖什么HttpMessageConverter
实例实际上是由请求头中的Content-Type
(在SpringMVC
中统一命名为MediaType
,见org.springframework.http.MediaType
)决定的,因此我们在处理控制器的请求参数之前必须要明确外部请求的Content-Type
到底是什么。上面的逻辑可以直接看源码AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters
,思路是比较清晰的。在@RequestMapping
注解中,produces
和consumes
属性就是和请求或者响应的Content-Type
相关的:
consumes
属性:指定处理请求的提交内容类型(Content-Type
),例如application/json
、text/html
等等,只有命中了对应的Content-Type
的值才会接受该请求。produces
属性:指定返回的内容类型,仅当某个请求的请求头中的(Accept
)类型中包含该指定类型才返回,如果返回的是JSON数据一般考虑使用application/json;charset=UTF-8
。另外提一点,SpringMVC
中默认使用Jackson
作为JSON的工具包,如果不是完全理解透整套源码的运作,一般不是十分建议修改默认使用的MappingJackson2HttpMessageConverter
(例如有些人喜欢使用FastJson
,实现HttpMessageConverter
引入FastJson
做HTTP消息转换器,这种做法并不推荐)。
其实一般的表单或者JSON数据的请求都是相对简单的,一些复杂的处理主要包括URL路径参数、文件上传、数组或者列表类型数据等。另外,关于参数类型中存在日期类型属性(例如java.util.Date
、java.sql.Date
、java.time.LocalDate
、java.time.LocalDateTime
、java.time.ZonedDateTime
等等),解析的时候一般需要自定义实现的逻辑实现String-->日期类型
的转换。其实道理很简单,日期相关的类型对于每个国家、每个时区甚至每个使用者来说认知都不一定相同,所以SpringMVC
并没有对于日期时间类型的解析提供一个通用的解决方案。在演示一些例子可能用到下面的模特类:
@Data
public class User {
private String name;
private Integer age;
private List contacts;
}
@Data
public class Contact {
private String name;
private String phone;
}
下面主要以HTTP
的GET
方法和POST
方法提交在SpringMVC
体系中正确处理参数的例子进行分析,还会花精力整理SpringMVC
体系中独有的URL
路径参数处理的一些技巧以及最常见的日期参数处理的合理实践(对于GET
方法和POST
方法提交的参数处理,基本囊括了其他如DELETE
、PUT
等方法的参数处理,随机应变即可)。
HTTP(s)
协议使用GET
方法进行请求的时候,提交的参数位于URL
模式的Query
部分,也就是URL
的?
之后的参数,格式是key1=value1&key2=value2
。GET
方法请求参数可以有多种方法获取:
@RequestParam
注解处理。Query
中的参数名称一致。HttpServletRequest
实例提供的方法(不推荐,存在硬编码)。假设请求的URL
为http://localhost:8080/get?name=doge&age=26
,那么控制器如下:
@Slf4j
@RestController
public class SampleController {
@GetMapping(path = "/get1")
public void get1(@RequestParam(name = "name") String name,
@RequestParam(name = "age") Integer age) {
log.info("name:{},age:{}", name, age);
}
@GetMapping(path = "/get2")
public void get2(UserVo vo) {
log.info("name:{},age:{}", vo.getName(), vo.getAge());
}
@GetMapping(path = "/get3")
public void get3(HttpServletRequest request) {
String name = request.getParameter("name");
String age = request.getParameter("age");
log.info("name:{},age:{}", name, age);
}
@Data
public static class UserVo {
private String name;
private Integer age;
}
}
表单参数,一般对应于页面上标签内的所有
标签的
name-value
聚合而成的参数,一般Content-Type
指定为application/x-www-form-urlencoded
,也就是会进行URL
编码。下面介绍几种常见的表单参数提交的参数形式。
对应的控制器如下:
@PostMapping(value = "/post")
public String post(@RequestParam(name = "name") String name,
@RequestParam(name = "age") Integer age) {
String content = String.format("name = %s,age = %d", name, age);
log.info(content);
return content;
}
说实话,如果有毅力的话,所有的复杂参数的提交最终都可以转化为多个单参数接收,不过这样做会产生十分多冗余的代码,而且可维护性比较低。这种情况下,用到的参数处理器是RequestParamMapMethodArgumentResolver
。
我们接着写一个接口用于提交用户信息,用到的是上面提到的模特类,主要包括用户姓名、年龄和联系人信息列表,这个时候,我们目标的控制器最终编码如下:
@PostMapping(value = "/user")
public User saveUser(User user) {
log.info(user.toString());
return user;
}
我们还是指定Content-Type
为application/x-www-form-urlencoded
,接着我们需要构造请求参数:
因为没有使用注解,最终的参数处理器为ServletModelAttributeMethodProcessor
,主要是把HttpServletRequest
中的表单参数封装到MutablePropertyValues
实例中,再通过参数类型实例化(通过构造反射创建User
实例),反射匹配属性进行值的填