其他参数
其他参数主要包括请求头、Cookie
、Model
、Map
等相关参数,还有一些并不是很常用或者一些相对原生的属性值获取(例如HttpServletRequest
、HttpServletResponse
或者它们内置的实例方法等)不做讨论。
请求头
请求头的值主要通过@RequestHeader
注解的参数获取,参数处理器是RequestHeaderMethodArgumentResolver
,需要在注解中指定请求头的Key
。简单实用如下:
spmvc-p-9
控制器方法代码:
@PostMapping(value = "/header") public String header(@RequestHeader(name = "Content-Type") String contentType) { return contentType; } 复制代码
Cookie
Cookie
的值主要通过@CookieValue
注解的参数获取,参数处理器为ServletCookieValueMethodArgumentResolver
,需要在注解中指定Cookie
的Key
。控制器方法代码如下:
@PostMapping(value = "/cookie") public String cookie(@CookieValue(name = "JSESSIONID") String sessionId) { return sessionId; } 复制代码
Model类型参数
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参数
@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)
中,区别下面三种情况:
@ModelAttribute
使用在方法(返回值)上,方法没有返回值(void
类型),Model(Map)
参数需要自行设置。@ModelAttribute
使用在方法(返回值)上,方法有返回值(非void
类型),返回值会添加到Model(Map)
参数,key
由@ModelAttribute
的value
指定,否则会使用返回值类型字符串(首写字母变为小写,如返回值类型为Integer
,则key
为integer
)。@ModelAttribute
使用在方法参数中,则可以获取同一个控制器中的已经设置的@ModelAttribute
对应的值。
在一个控制器(使用了@Controller
的Spring
组件)中,如果存在一到多个使用了@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参数
Errors
其实是BindingResult
的父接口,BindingResult
主要用于回调JSR
参数校验异常的属性项,如果JSR303
校验异常,一般会抛出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 @NoArgsConstructor public 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
覆盖对应的方法或者方式二同时使用@ExceptionHandler
和@(Rest)ControllerAdvice
注解进行异常处理。例如:
@RestControllerAdvice public class ApplicationRestControllerAdvice{ @ExceptionHandler(BusinessException.class) public Response handleBusinessException(BusinessException e, HttpServletRequest request){ // 这里处理异常和返回值 } @ExceptionHandler(MethodArgumentNotValidException.class) public Response handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request){ // 这里处理异常和返回值 } } 复制代码
❝值得注意的是,SpringBoot某个版本之后,把JSR303相关的依赖抽离到spring-boot-starter-validation依赖中,如果要使用JSR303相关相关校验功能,必须独立引入此starter
❞
@Value参数
控制器方法的参数可以是@Value
注解修饰的参数,会从Environment
实例中装配和转换属性值到对应的参数中(也就是参数的来源并不是请求体,而是上下文中已经加载和处理完成的环境属性值),参数处理器为ExpressionValueMethodArgumentResolver
。举个例子:
@GetMapping(value = "/value") public String value(@Value(value = "${spring.application.name}") String name) { log.info("spring.application.name={}", name); return name; } 复制代码
spring.application.name
属性一般在配置文件中指定,在加载配置文件属性的时候添加到全局的Environment
中。
Map类型参数
Map
类型参数的范围相对比较广,对应一系列的参数处理器,注意区别使用了上面提到的部分注解的Map
类型和完全不使用注解的Map
类型参数,两者的处理方式不相同。下面列举几个相对典型的Map
类型参数处理例子。
「不使用任何注解的Map<String,Object>
参数」
这种情况下参数实际上直接回调ModelAndViewContainer
中的ModelMap
实例,参数处理器为MapMethodProcessor
,往Map
参数中添加的属性将会带到页面中。
「使用@RequestParam注解的Map<String,Object>
参数」
这种情况下的参数处理器为RequestParamMapMethodArgumentResolver
,使用的请求方式需要指定Content-Type
为x-www-form-urlencoded
,不能使用application/json
的方式:
spmvc-p-10
控制器代码为:
@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集合-批量文件上传
批量文件上传的时候,我们一般需要接收一个MultipartFile
集合,可以有两种选择:
- 使用
MultipartHttpServletRequest
参数,直接调用getFiles
方法获取MultipartFile
列表。 - 使用
@RequestParam
注解修饰MultipartFile
列表,参数处理器是RequestParamMethodArgumentResolver
,其实就是第1种方式的封装而已。
spmvc-p-11
控制器方法代码如下:
@PostMapping(value = "/parts") public String partArgs(@RequestParam(name = "file") List<MultipartFile> parts) { log.info("{}", parts); return parts.toString(); } 复制代码
日期类型参数处理
日期参数处理个人认为是请求参数处理中最复杂的,因为一般日期处理的逻辑不是通用的,过多的定制化处理导致很难有一个统一的标准处理逻辑去处理和转换日期类型的参数。不过,这里介绍几个通用的方法,以应对各种奇葩的日期格式。下面介绍的例子中全部使用JDK8
中引入的日期时间API
,围绕java.util.Date
为核心的日期时间API
的使用方式类同。
一、统一以字符串形式接收
这种是最原始但是最奏效的方式,统一以字符串形式接收,然后自行处理类型转换,下面给个小例子:
static DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @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"; } @Data public class UserDto { private String userId; private String birthdayTime; private String graduationTime; } @Data public class UserEntity { private String userId; private LocalDateTime birthdayTime; private LocalDateTime graduationTime; } 复制代码
spmvc-p-12
使用字符串接收后再转换的缺点就是模板代码太多,编码风格不够简洁,重复性工作太多,如果有代码洁癖或者类似笔者这样是一个节能主义者,一般不会选用这种方式。
二、使用注解@DateTimeFormat或者@JsonFormat
@DateTimeFormat
注解配合@RequestBody
的参数使用的时候,会发现抛出InvalidFormatException
异常,提示转换失败,这是因为在处理此注解的时候,只支持Form
表单提交(Content-Type
为x-www-form-urlencoded
),例子如下:
spmvc-p-13
@Data public 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") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime birthdayTime, @RequestParam("name"="graduationTime") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") 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; } 复制代码
❝一般选用LocalDateTime作为日期字段参数的类型,因为它的转换相对于其他JDK8的日期时间类型简单
❞
三、Jackson序列化和反序列化定制
因为SpringMVC
默认使用Jackson
处理@RequestBody
的参数转换,因此可以通过定制序列化器和反序列化器来实现日期类型的转换,这样我们就可以使用application/json
的形式提交请求参数。这里的例子是转换请求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
序列化和反序列化器对全局生效。
请求URL匹配
前面基本介绍完了主流的请求参数处理,其实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模式可以组合使用,千变万化。
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
。上面的例子只是列举了SpringMVC
中URL
匹配的典型例子,并没有深入展开。
最后,org.springframework.util.AntPathMatcher
作为一个工具类,可以单独使用,不仅仅可以用于匹配URL
,也可以用于匹配系统文件路径,不过需要使用其带参数构造改变内部的pathSeparator
变量,例如:
AntPathMatcher antPathMatcher = new AntPathMatcher(File.separator); 复制代码
小结
笔者在前一段时间曾经花大量时间梳理和分析过Spring
、SpringMVC
的源码,但是后面一段很长的时间需要进行业务开发,对架构方面的东西有点生疏了,毕竟东西不用就会生疏,这个是常理。这篇文章基于一些SpringMVC
的源码经验总结了请求参数的处理相关的一些知识,希望帮到自己和大家。
参考资料:
- spring-boot-web-starter:2.3.0.RELEASE源码。
(本文完 c-7-d e-a-20180512 r-a-20200713 旧文重发 封面图来源于日漫《神风怪盗》)