在日常使用 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<Contact> 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; } }
表单参数,一般对应于页面上 <form>
标签内的所有 <input>
标签的 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
实例),反射匹配属性进行值的填充。另外,请求复杂参数里面的列表属性请求参数看起来比较奇葩,实际上和在 .properties
文件中添加最终映射到 Map
类型的参数的写法是一致的。那么,能不能把整个请求参数塞在一个字段中提交呢?
直接这样做是不行的,因为实际提交的 Form
表单, key
是 user
字符串, value
实际上也是一个字符串,缺少一个 String->User
类型的转换器,实际上 RequestParamMethodArgumentResolver
依赖 WebConversionService
中 Converter
实例列表进行参数转换:
解决办法还是有的,添加一个 org.springframework.core.convert.converter.Converter
实现即可:
@Component public class StringUserConverter implements Converter<String, User> { @Autowaired private ObjectMapper objectMapper; @Override public User convert(String source) { try { return objectMapper.readValue(source, User.class); } catch (IOException e) { throw new IllegalArgumentException(e); } } }
上面这种做法属于曲线救国的做法,不推荐使用在生产环境,但是如果有些第三方接口的对接无法避免这种参数,可以选择这种实现方式。
极度不推荐使用在 application/x-www-form-urlencoded
这种媒体类型的表单提交的形式下强行使用列表或者数组类型参数,除非是为了兼容处理历史遗留系统的参数提交处理。例如提交的参数形式是:
list = ["string-1", "string-2", "string-3"]
那么表单参数的形式要写成:
name | value |
---|---|
list[0] | string-1 |
list[1] | string-2 |
list[2] | string-3 |
控制器的代码如下:
@PostMapping(path = "/list") public void list(@RequestParam(name="list") List<String> list) { log.info(list); }
一个更加复杂的例子如下,假设想要提交的报文格式如下:
user = [{"name":"doge-1","age": 21},{"name":"doge-2","age": 22}]
那么表单参数的形式要写成:
name | value |
---|---|
user[0].name | doge-1 |
user[0].age | 21 |
user[1].name | doge-2 |
user[1].age | 22 |
控制器的代码如下:
@PostMapping(path = "/user") public void saveUsers(@RequestParam(name="user") List<UserVo> users) { log.info(users); } @Data public class UserVo{ private String name; private Integer age; }
一般来说,直接在 POST
请求中的请求体提交一个JSON字符串这种方式对于 SpringMVC
来说是比较友好的,只需要把 Content-Type
设置为 application/json
,提交一个原始的JSON字符串即可,控制器方法参数使用 @RequestBody
注解处理:
后端控制器的代码也比较简单:
@PostMapping(value = "/user-2") public User saveUser2(@RequestBody User user) { log.info(user.toString()); return user; }
因为使用了 @RequestBody
注解,最终使用到的参数处理器为 RequestResponseBodyMethodProcessor
,实际上会用到 MappingJackson2HttpMessageConverter
进行参数类型的转换,底层依赖到 Jackson
相关的包。推荐使用这种方式,这是最常用也是最稳健的 JSON
参数处理方式。
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:M|F}
,代码如下:
@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
:
@Configuration public 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
用于读取请求体的最后部分(文件的字节序列)。参数处理器用到的是 RequestPartMethodArgumentResolver
(记住一点,使用了 @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 Content-Type) { return Content-Type; }
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)
中,区别下面三种情况:
@ModelAttribute
使用在方法(返回值)上,方法没有返回值( void
类型), Model(Map)
参数需要自行设置。 @ModelAttribute
使用在方法(返回值)上,方法有返回值(非 void
类型),返回值会添加到 Model(Map)
参数, key
由 @ModelAttribute
的 value
指定,否则会使用返回值类型字符串(首写字母变为小写,如返回值类型为 Integer
,则 key
为 integer
)。 @ModelAttribute
使用在方法参数中,则可以获取同一个控制器中的已经设置的 @ModelAttribute
对应的值。
在一个控制器(使用了 @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 @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){ // 这里处理异常和返回值 } }
控制器方法的参数可以是 @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
类型参数处理例子。
这种情况下参数实际上直接回调 ModelAndViewContainer
中的 ModelMap
实例,参数处理器为 MapMethodProcessor
,往 Map
参数中添加的属性将会带到页面中。
这种情况下的参数处理器为 RequestParamMapMethodArgumentResolver
,使用的请求方式需要指定 Content-Type
为 x-www-form-urlencoded
,不能使用 application/json
的方式:
控制器代码为:
@PostMapping(value = "/map") public String mapArgs(@RequestParam Map<String, Object> map) { log.info("{}", map); return map.toString(); }
这种情况下的参数处理器为 RequestHeaderMapMethodArgumentResolver
,作用是获取请求的所有请求头的 Key-Value
。
这种情况下的参数处理器为 PathVariableMapMethodArgumentResolver
,作用是获取所有路径参数封装为 Key-Value
结构。
批量文件上传的时候,我们一般需要接收一个 MultipartFile
集合,可以有两种选择:
MultipartHttpServletRequest
参数,直接调用 getFiles
方法获取 MultipartFile
列表。 @RequestParam
注解修饰 MultipartFile
列表,参数处理器是 RequestParamMethodArgumentResolver
,其实就是第1种方式的封装而已。
控制器方法代码如下:
@PostMapping(value = "/parts") public String partArgs(@RequestParam(name = "file") List<MultipartFile> parts) { log.info("{}", parts); return parts.toString(); }
日期参数处理个人认为是请求参数处理中最复杂的,因为一般日期处理的逻辑不是通用的,过多的定制化处理导致很难有一个统一的标准处理逻辑去处理和转换日期类型的参数。不过,这里介绍几个通用的方法,以应对各种奇葩的日期格式。下面介绍的例子中全部使用Jdk8中引入的日期时间API,围绕 java.util.Date
为核心的日期时间API的使用方式类同。
这种是最原始但是最奏效的方式,统一以字符串形式接收,然后自行处理类型转换,下面给个小例子:
@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; }
使用字符串接收后再转换的缺点就是模板代码太多,编码风格不够简洁,重复性工作太多。
@DateTimeFormat
注解配合 @RequestBody
的参数使用的时候,会发现抛出 InvalidFormatException
异常,提示转换失败,这是因为在处理此注解的时候,只支持 Form
表单提交( Content-Type
为 x-www-form-urlencoded
),例子如下:
@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; }
因为 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
序列化和反序列化器对全局生效。
前面基本介绍完了主流的请求参数处理,其实 SpringMVC
中还会按照 URL
的模式进行匹配,使用的是 Ant
路径风格,处理工具类为 org.springframework.util.AntPathMatcher
,从此类的注释来看,匹配规则主要包括下面四点
:
?
匹配1个字符。 *
匹配0个或者多个 字符
。 **
匹配路径中0个或者多个 目录
。 {spring:[a-z]+}
将正则表达式[a-z]+匹配到的值,赋值给名为 spring
的路径变量。 举些例子:
@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
@GetMapping(value = "/pattern*") public String pattern() { return "success"; } /pattern 200 OK /pattern/ 200 OK /patternd 200 OK /pattern/a 404 Not Found
@GetMapping(value = "/pattern/**/p") public String pattern() { return "success"; } /pattern/p 200 OK /pattern/x/p 200 OK /pattern/x/y/p 200 OK
@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
。
最后, org.springframework.util.AntPathMatcher
作为一个工具类,可以单独使用,不仅仅可以用于匹配 URL
,也可以用于匹配系统文件路径,不过需要使用其带参数构造改变内部的 pathSeparator
变量,例如:
AntPathMatcher antPathMatcher = new AntPathMatcher(File.separator);
笔者在前一段时间曾经花大量时间梳理和分析过 Spring
、 SpringMVC
的源码,但是后面一段很长的时间需要进行业务开发,对架构方面的东西有点生疏了,毕竟东西不用就会生疏,这个是常理。这篇文章基于一些 SpringMVC
的源码经验总结了请求参数的处理相关的一些知识,希望帮到自己和大家。
参考资料:
(本文完 c-7-d e-a-20180512 旧文重发)