做者:zhrowable
https://www.jianshu.com/p/5f6...
其实通常的表单或者JSON数据的请求都是相对简单的,一些复杂的处理主要包括URL路径参数、文件上传、数组或者列表类型数据等。java
另外,关于参数类型中存在日期类型属性(例如java.util.Date、java.sql.Date、java.time.LocalDate、java.time.LocalDateTime),解析的时候通常须要自定义实现的逻辑实现String->日期类型的转换。程序员
其实道理很简单,日期相关的类型对于每一个国家、每一个时区甚至每一个使用者来讲认知都不必定相同。在演示一些例子主要用到下面的模特类:web
@Datapublic class User { private String name; private Integer age; private List<Contact> contacts;}@Datapublic class Contact { private String name; private String phone; }
非对象类型单个参数接收:面试
这种是最经常使用的表单参数提交,ContentType指定为application/x-www-form-urlencoded,也就是会进行URL编码。正则表达式
对应的控制器以下:spring
@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。sql
对象类型参数接收:json
咱们接着写一个接口用于提交用户信息,用到的是上面提到的模特类,主要包括用户姓名、年龄和联系人信息列表,这个时候,咱们目标的控制器最终编码以下:后端
@PostMapping(value = "/user") public User saveUser(User user) { log.info(user.toString()); return user; }
咱们仍是指定ContentType为application/x-www-form-urlencoded,接着咱们须要构造请求参数:数组
由于没有使用注解,最终的参数处理器为ServletModelAttributeMethodProcessor,主要是把HttpServletRequest中的表单参数封装到MutablePropertyValues实例中,再经过参数类型实例化(经过构造反射建立User实例),反射匹配属性进行值的填充。
另外,请求复杂参数里面的列表属性请求参数看起来比较奇葩,实际上和在.properties文件中添加最终映射到Map类型的参数的写法是一致的。那么,能不能把整个请求参数塞在一个字段中提交呢?
直接这样作是不行的,由于实际提交的form表单,key是user,value其实是一个字符串,缺乏一个String->User类型的转换器,实际上RequestParamMethodArgumentResolver依赖WebConversionService中Converter列表进行参数转换:
解决办法仍是有的,添加一个org.springframework.core.convert.converter.Converter实现便可:
@Componentpublic class StringUserConverter implements Converter<String, User> { private static final ObjectMapper MAPPER = new ObjectMapper(); @Override public User convert(String source) { try { return MAPPER.readValue(source, User.class); } catch (IOException e) { throw new IllegalArgumentException(e); } } }
上面这种作法属于曲线救国的作法,不推荐使用在生产环境,可是若是有些第三方接口的对接没法避免这种参数,能够选择这种实现方式。
通常来讲,直接POST一个JSON字符串这种方式对于SpringMVC来讲是比较友好的,只须要把ContentType设置为application/json,提交一个原始的JSON字符串便可:
后端控制器的代码也比较简单:
@PostMapping(value = "/user-2") public User saveUser2(@RequestBody User user) { log.info(user.toString()); return user; }
由于使用了@RequestBody注解,最终使用到的参数处理器为RequestResponseBodyMethodProcessor,实际上会用到MappingJackson2HttpMessageConverter进行参数类型的转换,底层依赖到Jackson相关的包。
URL参数,或者叫请求路径参数是基于URL模板获取到的参数,例如/user/{userId}是一个URL模板(URL模板中的参数占位符是{}),实际请求的URL为/user/1,那么经过匹配实际请求的URL和URL模板就能提取到userId为1。
在SpringMVC中,URL模板中的路径参数叫作PathVariable,对应注解@PathVariable,对应的参数处理器为PathVariableMethodArgumentResolver。
注意一点是,@PathVariable的解析是按照value(name)属性进行匹配,和URL参数的顺序是无关的。举个简单的例子:
后台的控制器以下:
@GetMapping(value = "/user/{name}/{age}") public String findUser1(@PathVariable(value = "age") Integer age, @PathVariable(value = "name") String name) { String content = String.format("name = %s,age = %d", name, age); log.info(content); return content; }
这种用法被普遍使用于Representational State Transfer(REST)的软件架构风格,我的以为这种风格是比较灵活和清晰的(从URL和请求方法就能彻底理解接口的意义和功能)。下面再介绍两种相对特殊的使用方式。
带条件的URL参数
其实路径参数支持正则表达式,例如咱们在使用/sex/sex}接口的时候,要求sex必须是F(Female)或者M(Male),那么咱们的URL模板能够定义为/sex/{sex:MF,代码以下:
@GetMapping(value = "/sex/{sex:M|F}") public String findUser2(@PathVariable(value = "sex") String sex){ log.info(sex); return sex; }
只有/sex/F或者/sex/M的请求才会进入findUser2控制器方法,其余该路径前缀的请求都是非法的,会返回404状态码。这里仅仅是介绍了一个最简单的URL参数正则表达式的使用方式,更强大的用法能够自行摸索。
@MatrixVariable的使用
MatrixVariable也是URL参数的一种,对应注解@MatrixVariable,不过它并非URL中的一个值(这里的值指定是两个"/"之间的部分),而是值的一部分,它经过";"进行分隔,经过"="进行K-V设置。
提及来有点抽象,举个例子:假如咱们须要打电话给一个名字为doge,性别是男,分组是码畜的程序员,GET请求的URL能够表示为:/call/doge;gender=male;group=programmer
,咱们设计的控制器方法以下:
@GetMapping(value = "/call/{name}") public String find(@PathVariable(value = "name") String name, @MatrixVariable(value = "gender") String gender, @MatrixVariable(value = "group") String group) { String content = String.format("name = %s,gender = %s,group = %s", name, gender, group); log.info(content); return content; }
固然,若是你按照上面的例子写好代码,尝试请求一下该接口发现是报错的:400 Bad Request - Missing matrix variable 'gender' for method parameter of type String。
这是由于@MatrixVariable注解的使用是不安全的,在SpringMVC中默认是关闭对其支持。要开启对@MatrixVariable的支持,须要设置RequestMappingHandlerMapping#setRemoveSemicolonContent方法为false:
@Configurationpublic class CustomMvcConfiguration implements InitializingBean { @Autowired private RequestMappingHandlerMapping requestMappingHandlerMapping; @Override public void afterPropertiesSet() throws Exception { requestMappingHandlerMapping.setRemoveSemicolonContent(false); } }
除非有很特殊的须要,不然不建议使用@MatrixVariable。
文件上传在使用POSTMAN模拟请求的时候须要选择form-data,POST方式进行提交:
假设咱们在D盘有一个图片文件叫doge.jpg,如今要经过本地服务接口把文件上传,控制器的代码以下:
@PostMapping(value = "/file1") public String file1(@RequestPart(name = "file1") MultipartFile multipartFile) { String content = String.format("name = %s,originName = %s,size = %d", multipartFile.getName(), multipartFile.getOriginalFilename(), multipartFile.getSize()); log.info(content); return content; }
控制台输出是:
name = file1,originName = doge.jpg,size = 68727
可能有点疑惑,参数是怎么来的,咱们能够用Fildder抓个包看下:
可知MultipartFile实例的主要属性分别来自Content-Disposition、content-type和content-length,另外,InputStream用于读取请求体的最后部分(文件的字节序列)。参数处理器用到的是RequestPartM
ethodArgumentResolver(记住一点,使用了@RequestPart和MultipartFile必定是使用此参数处理器)。
在其余状况下,使用@RequestParam和MultipartFile或者仅仅使用MultipartFile(参数的名字必须和POST表单中的Content-Disposition描述的name一致)也能够接收上传的文件数据,主要是经过RequestParamMethodArgumentResolver进行解析处理的,它的功能比较强大,具体能够看其supportsParameter
方法,这两种状况的控制器方法代码以下:
@PostMapping(value = "/file2") public String file2(MultipartFile file1) { String content = String.format("name = %s,originName = %s,size = %d", file1.getName(), file1.getOriginalFilename(), file1.getSize()); log.info(content); return content;}@PostMapping(value = "/file3") public String file3(@RequestParam(name = "file1") MultipartFile multipartFile) { String content = String.format("name = %s,originName = %s,size = %d", multipartFile.getName(), multipartFile.getOriginalFilename(), multipartFile.getSize()); log.info(content); return content; }
其余参数主要包括请求头、Cookie、Model、Map等相关参数,还有一些并非很经常使用或者一些相对原生的属性值获取(例如HttpServletRequest、HttpServletResponse等)不作讨论。
请求头的值主要经过@RequestHeader注解的参数获取,参数处理器是RequestHeaderMethodArgumentResolver,须要在注解中指定请求头的Key。简单实用以下:
控制器方法代码:
@PostMapping(value = "/header") public String header(@RequestHeader(name = "Content-Type") String contentType) { return contentType; }
Cookie的值主要经过@CookieValue注解的参数获取,参数处理器为ServletCookieValueMethodArgumentResolver,须要在注解中指定Cookie的Key。控制器方法代码以下:
@PostMapping(value = "/cookie") public String cookie(@CookieValue(name = "JSESSIONID") String sessionId) { return sessionId; }
Model类型参数的处理器是ModelMethodProcessor,实际上处理此参数是直接返回ModelAndViewContainer实例中的Model(ModelMap类型),由于要桥接不一样的接口和类的功能,所以回调的实例是BindingAwareModelMap类型,此类型继承自ModelMap同时实现了Model接口。举个例子:
@GetMapping(value = "/model") public String model(Model model, ModelMap modelMap) { log.info("{}", model == modelMap); return "success"; }
注意调用此接口,控制台输出Info日志内容为:true。ModelMap或者Model中添加的属性项会附加到HttpRequestServlet中带到页面中进行渲染。
@ModelAttribute注解处理的参数处理器为ModelAttributeMethodProcessor,@ModelAttribute的功能源码的注释以下:
Annotation that binds a method parameter or method return value to a named model attribute, exposed to a web view.
简单来讲,就是经过key-value形式绑定方法参数或者方法返回值到Model(Map)中,区别下面三种状况:
在一个控制器(使用了@Controller)中,若是存在一到多个使用了@ModelAttribute的方法,这些方法老是在进入控制器方法以前执行,而且执行顺序是由加载顺序决定的(具体的顺序是带参数的优先,而且按照方法首字母升序排序),举个例子:
@Slf4j @RestController public class ModelAttributeController { @ModelAttribute public void before(Model model) { log.info("before.........."); model.addAttribute("before", "beforeValue"); } @ModelAttribute(value = "beforeArg") public String beforeArg() { log.info("beforeArg.........."); return "beforeArgValue"; } @GetMapping(value = "/modelAttribute") public String modelAttribute(Model model, @ModelAttribute(value = "beforeArg") String beforeArg) { log.info("modelAttribute.........."); log.info("beforeArg..........{}", beforeArg); log.info("{}", model); return "success"; } @ModelAttribute public void after(Model model) { log.info("after.........."); model.addAttribute("after", "afterValue"); } @ModelAttribute(value = "afterArg") public String afterArg() { log.info("afterArg.........."); return "afterArgValue"; } }
调用此接口,控制台输出日志以下:
after.......... before.......... afterArg.......... beforeArg.......... modelAttribute.......... beforeArg..........beforeArgValue {after=afterValue, before=beforeValue, afterArg=afterArgValue, beforeArg=beforeArgValue}
能够印证排序规则和参数设置、获取。
Errors实际上是BindingResult的父接口,BindingResult主要用于回调JSR参数校验异常的属性项,若是JSR校验异常,通常会抛出MethodArgumentNotValidException异常,而且会返回400(Bad Request),见全局异常处理器DefaultHandlerExceptionResolver。Errors类型的参数处理器为ErrorsMethodArgumentResolver。举个例子:
@PostMapping(value = "/errors") public String errors(@RequestBody @Validated ErrorsModel errors, BindingResult bindingResult) { if (bindingResult.hasErrors()) { for (ObjectError objectError : bindingResult.getAllErrors()) { log.warn("name={},message={}", objectError.getObjectName(), objectError.getDefaultMessage()); } } return errors.toString();}//ErrorsModel@Data@NoArgsConstructorpublic class ErrorsModel { @NotNull(message = "id must not be null!") private Integer id; @NotEmpty(message = "errors name must not be empty!") private String name; }
调用接口控制台Warn日志以下:
name=errors,message=errors name must not be empty!
通常状况下,不建议用这种方式处理JSR校验异常的属性项,由于会涉及到大量的重复的硬编码工做,建议直接继承ResponseEntityExceptionHandler,覆盖对应的方法。
控制器方法的参数能够是@Value注解修饰的参数,会从Environment中装配和转换属性值到对应的参数中(也就是参数的来源并非请求体),参数处理器为ExpressionValueMethodArgumentResolver。举个例子:
@GetMapping(value = "/value") public String value(@Value(value = "${spring.application.name}") String name) { log.info("spring.application.name={}", name); return name; }
Map类型参数的范围相对比较广,对应一系列的参数处理器,注意区别使用了上面提到的部分注解的Map类型和彻底不使用注解的Map类型参数,二者的处理方式不相同。下面列举几个相对典型的Map类型参数处理例子。
不使用任何注解的Map<String,Object>参数
这种状况下参数实际上直接回调ModelAndViewContainer中的ModelMap实例,参数处理器为MapMethodProcessor,往Map参数中添加的属性将会带到页面中。
使用@RequestParam注解的Map<String,Object>参数
这种状况下的参数处理器为RequestParamMapMethodArgumentResolver,使用的请求方式须要指定ContentType为x-www-form-urlencoded,不能使用application/json的方式:
控制器代码为:
@PostMapping(value = "/map") public String mapArgs(@RequestParam Map<String, Object> map) { log.info("{}", map); return map.toString(); }
使用@RequestHeader注解的Map<String,Object>参数
这种状况下的参数处理器为RequestHeaderMapMethodArgumentResolver,做用是获取请求的全部请求头的Key-Value。
使用@PathVariable注解的Map<String,Object>参数
这种状况下的参数处理器为PathVariableMapMethodArgumentResolver,做用是获取全部路径参数封装为Key-Value结构。
批量文件上传的时候,咱们通常须要接收一个MultipartFile集合,能够有两种选择:
getFiles
方法获取MultipartFile列表。控制器方法代码以下:
@PostMapping(value = "/parts") public String partArgs(@RequestParam(name = "file") List<MultipartFile> parts) { log.info("{}", parts); return parts.toString(); }
日期处理我的认为是请求参数处理中最复杂的,由于通常日期处理的逻辑不是通用的,过多的定制化处理致使很难有一个统一的标准处理逻辑去处理和转换日期类型的参数。
不过,这里介绍几个通用的方法,以应对各类奇葩的日期格式。下面介绍的例子中所有使用Jdk8中引入的日期时间API,围绕java.util.Date为核心的日期时间API的使用方式类同。另外推荐你们关注下微信公众号Java技术栈,在后台回复Java能够获取我整理的 N 篇 Java 8+ 教程,都是干货。
这种是最原始可是最奏效的方式,统一以字符串形式接收,而后自行处理类型转换,下面给个小例子:
@PostMapping(value = "/date1") public String date1(@RequestBody UserDto userDto) { UserEntity userEntity = new UserEntity(); userEntity.setUserId(userDto.getUserId()); userEntity.setBirthdayTime(LocalDateTime.parse(userDto.getBirthdayTime(), FORMATTER)); userEntity.setGraduationTime(LocalDateTime.parse(userDto.getGraduationTime(), FORMATTER)); log.info(userEntity.toString()); return "success"; } @Datapublic class UserDto { private String userId; private String birthdayTime; private String graduationTime;}@Datapublic class UserEntity { private String userId; private LocalDateTime birthdayTime; private LocalDateTime graduationTime; }
@DateTimeFormat注解配合@RequestBody的参数使用的时候,会发现抛出InvalidFormatException异常,提示转换失败,这是由于在处理此注解的时候,只支持form提交(ContentType为x-www-form-urlencoded),例子以下:
@Datapublic class UserDto2 { private String userId; @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime birthdayTime; @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime graduationTime; } @PostMapping(value = "/date2") public String date2(UserDto2 userDto2) { log.info(userDto2.toString()); return "success"; } //或者像下面这样 @PostMapping(value = "/date2") public String date2(@RequestParam("name"="userId")String userId, @RequestParam("name"="birthdayTime")LocalDateTime birthdayTime, @RequestParam("name"="graduationTime")LocalDateTime graduationTime) { return "success"; }
而@JsonFormat注解可以使用在form或者Json请求参数的场景,所以更推荐使用@JsonFormat注解,不过注意须要指定时区(timezone属性,例如在中国是东八区"GMT+8"),不然有可能致使出现"时差",举个例子:
@PostMapping(value = "/date2") public String date2(@RequestBody UserDto2 userDto2) { log.info(userDto2.toString()); return "success"; } @Data public class UserDto2 { private String userId; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime birthdayTime; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime graduationTime; }
由于SpringMVC默认使用Jackson处理@RequestBody的参数转换,所以能够经过定制序列化器和反序列化器来实现日期类型的转换,这样咱们就可使用application/json的形式提交请求参数。关于Java序列化你应该知道的一切,这篇推荐看下。
这里的例子是转换请求Json参数中的字符串为LocalDateTime类型,属于Json反序列化,所以须要定制反序列化器:
@PostMapping(value = "/date3")public String date3(@RequestBody UserDto3 userDto3) { log.info(userDto3.toString()); return "success"; } @Data public class UserDto3 { private String userId; @JsonDeserialize(using = CustomLocalDateTimeDeserializer.class) private LocalDateTime birthdayTime; @JsonDeserialize(using = CustomLocalDateTimeDeserializer.class) private LocalDateTime graduationTime; } public class CustomLocalDateTimeDeserializer extends LocalDateTimeDeserializer { public CustomLocalDateTimeDeserializer() { super(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); } }
前面三种方式都存在硬编码等问题,其实最佳实践是直接修改MappingJackson2HttpMessageConverter中的ObjectMapper对于日期类型处理默认的序列化器和反序列化器,这样就能全局生效,不须要再使用其余注解或者定制序列化方案(固然,有些时候须要特殊处理定制),或者说,在须要特殊处理的场景才使用其余注解或者定制序列化方案。使用钩子接口Jackson2ObjectMapperBuilderCustomizer能够实现ObjectMapper的属性定制:
@Bean public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer(){ return customizer->{ customizer.serializerByType(LocalDateTime.class,new LocalDateTimeSerializer( DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); customizer.deserializerByType(LocalDateTime.class,new LocalDateTimeDeserializer( DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); }; }
这样就能定制化MappingJackson2HttpMessageConverter中持有的ObjectMapper,上面的LocalDateTime序列化和反序列化器对全局生效。
前面基本介绍完了主流的请求参数处理,其实SpringMVC中还会按照URL的模式进行匹配,使用的是Ant路径风格,处理工具类为org.springframework.util.AntPathMatcher
,今后类的注释来看,匹配规则主要包括下面四点:
?
匹配1个字符。*
匹配0个或者多个字符。**
匹配路径中0个或者多个目录。{spring:[a-z]+}
将正则表达式[a-z]+匹配到的值,赋值给名为spring的路径变量。举些例子:
?形式的URL:
@GetMapping(value = "/pattern?")public String pattern() { return "success"; } /pattern 404 Not Found /patternd 200 OK /patterndd 404 Not Found /pattern/ 404 Not Found /patternd/s 404 Not Found
*形式的URL:
@GetMapping(value = "/pattern*") public String pattern() { return "success"; } /pattern 200 OK /pattern/ 200 OK /patternd 200 OK /pattern/a 404 Not Found
形式的URL:
@GetMapping(value = "/pattern/**/p")public String pattern() { return "success"; } /pattern/p 200 OK /pattern/x/p 200 OK /pattern/x/y/p 200 OK
{spring:[a-z]+}形式的URL:
@GetMapping(value = "/pattern/{key:\[a-c\]+}") public String pattern(@PathVariable(name = "key") String key) { return "success"; } /pattern/a 200 OK /pattern/ab 200 OK /pattern/abc 200 OK /pattern 404 Not Found /pattern/abcd 404 Not Found
上面的四种URL模式能够组合使用,变幻无穷。RESTful API设计技巧经验总结推荐看看。
URL匹配还遵循精确匹配原则,也就是存在两个模式对同一个URL都可以匹配成功,则选取最精确的URL匹配,进入对应的控制器方法,举个例子:
@GetMapping(value = "/pattern/**/p") public String pattern1() { return "success"; } @GetMapping(value = "/pattern/p") public String pattern2() { return "success"; }
上面两个控制器,若是请求URL为/pattern/p,最终进入的方法为pattern2
。
最后,org.springframework.util.AntPathMatcher
做为一个工具类,能够单独使用,不只仅能够用于匹配URL,也能够用于匹配系统文件路径,不过须要使用其带参数构造改变内部的pathSeparator变量,例如:
AntPathMatcher antPathMatcher = new AntPathMatcher(File.separator);
笔者在前一段时间曾经花大量时间梳理和分析过Spring、SpringMVC的源码,可是后面一段很长的时间须要进行业务开发,对架构方面的东西有点生疏了,毕竟东西不用就会生疏,这个是常理。
这篇文章基于一些SpringMVC的源码经验总结了请求参数的处理相关的一些知识,但愿帮到本身和你们。
关注公众号Java技术栈回复"面试"获取我整理的2020最全面试题及答案。
推荐去个人博客阅读更多:
2.Spring MVC、Spring Boot、Spring Cloud 系列教程
3.Maven、Git、Eclipse、Intellij IDEA 系列工具教程
以为不错,别忘了点赞+转发哦!