SpringMVC请求参数接收总结(一)

在日常使用 SpringMVC 进行开发的时候,有可能遇到前端各种类型的请求参数,这里做一次相对全面的总结。 SpringMVC 中处理控制器参数的接口是 HandlerMethodArgumentResolver ,此接口有众多子类,分别处理不同(注解类型)的参数,下面只列举几个子类:

  • RequestParamMethodArgumentResolver :解析处理使用了 @RequestParam 注解的参数、 MultipartFile 类型参数和 Simple 类型(如 longint 等类型)参数。
  • 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 注解中, producesconsumes 属性就是和请求或者响应的 Content-Type 相关的:

  • consumes 属性:指定处理请求的提交内容类型( Content-Type ),例如 application/jsontext/html 等等,只有命中了对应的 Content-Type 的值才会接受该请求。
  • produces 属性:指定返回的内容类型,仅当某个请求的请求头中的( Accept )类型中包含该指定类型才返回,如果返回的是JSON数据一般考虑使用 application/json;charset=UTF-8

另外提一点, SpringMVC 中默认使用 Jackson 作为JSON的工具包,如果不是完全理解透整套源码的运作,一般不是十分建议修改默认使用的 MappingJackson2HttpMessageConverter (例如有些人喜欢使用 FastJson ,实现 HttpMessageConverter 引入 FastJson 做HTTP消息转换器,这种做法并不推荐)。

SpringMVC请求参数接收

其实一般的表单或者JSON数据的请求都是相对简单的,一些复杂的处理主要包括URL路径参数、文件上传、数组或者列表类型数据等。另外,关于参数类型中存在日期类型属性(例如 java.util.Datejava.sql.Datejava.time.LocalDatejava.time.LocalDateTimejava.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;
}

下面主要以 HTTPGET 方法和 POST 方法提交在 SpringMVC 体系中正确处理参数的例子进行分析,还会花精力整理 SpringMVC 体系中 独有的 URL 路径参数 处理的一些技巧以及最常见的 日期参数 处理的合理实践(对于 GET 方法和 POST 方法提交的参数处理,基本囊括了其他如 DELETEPUT 等方法的参数处理,随机应变即可)。

GET方法请求参数处理

HTTP(s) 协议使用 GET 方法进行请求的时候,提交的参数位于 URL 模式的 Query 部分,也就是 URL? 之后的参数,格式是 key1=value1&key2=value2GET 方法请求参数可以有多种方法获取:

@RequestParam
Query
HttpServletRequest

假设请求的 URLhttp://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-Typeapplication/x-www-form-urlencoded ,接着我们需要构造请求参数:

因为没有使用注解,最终的参数处理器为 ServletModelAttributeMethodProcessor ,主要是把 HttpServletRequest 中的表单参数封装到 MutablePropertyValues 实例中,再通过参数类型实例化(通过构造反射创建 User 实例),反射匹配属性进行值的填充。另外,请求复杂参数里面的列表属性请求参数看起来比较奇葩,实际上和在 .properties 文件中添加最终映射到 Map 类型的参数的写法是一致的。那么,能不能把整个请求参数塞在一个字段中提交呢?

直接这样做是不行的,因为实际提交的 Form 表单, keyuser 字符串, value 实际上也是一个字符串,缺少一个 String->User 类型的转换器,实际上 RequestParamMethodArgumentResolver 依赖 WebConversionServiceConverter 实例列表进行参数转换:

解决办法还是有的,添加一个 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;
}

JSON参数

一般来说,直接在 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 路径参数,或者叫请求路径参数是基于URL模板获取到的参数,例如 /user/{userId} 是一个 URL 模板( URL 模板中的参数占位符是{}),实际请求的 URL/user/1 ,那么通过匹配实际请求的 URLURL 模板就能提取到 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-dataPOST 方式进行提交:

假设我们在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-DispositionContent-TypeContent-Length ,另外, InputStream 用于读取请求体的最后部分(文件的字节序列)。参数处理器用到的是 RequestPartMethodArgumentResolver (记住一点,使用了 @RequestPartMultipartFile 一定是使用此参数处理器)。在其他情况下,使用 @RequestParamMultipartFile 或者仅仅使用 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;
}

其他参数

其他参数主要包括请求头、 CookieModelMap 等相关参数,还有一些并不是很常用或者一些相对原生的属性值获取(例如 HttpServletRequestHttpServletResponse 等)不做讨论。

请求头

请求头的值主要通过 @RequestHeader 注解的参数获取,参数处理器是 RequestHeaderMethodArgumentResolver ,需要在注解中指定请求头的 Key 。简单实用如下:

控制器方法代码:

@PostMapping(value = "/header")
public String header(@RequestHeader(name = "Content-Type") String Content-Type) {
    return Content-Type;
}

Cookie

Cookie 的值主要通过 @CookieValue 注解的参数获取,参数处理器为 ServletCookieValueMethodArgumentResolver ,需要在注解中指定 CookieKey 。控制器方法代码如下:

@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) 中,区别下面三种情况:

  1. @ModelAttribute 使用在方法(返回值)上,方法没有返回值( void 类型), Model(Map) 参数需要自行设置。
  2. @ModelAttribute 使用在方法(返回值)上,方法有返回值(非 void 类型),返回值会添加到 Model(Map) 参数, key@ModelAttributevalue 指定,否则会使用返回值类型字符串(首写字母变为小写,如返回值类型为 Integer ,则 keyinteger )。
  3. @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参数

Errors 其实是 BindingResult 的父接口, BindingResult 主要用于回调JSR参数校验异常的属性项,如果JSR校验异常,一般会抛出 MethodArgumentNotValidException 异常,并且会返回400(Bad Request),见全局异常处理器 DefaultHandlerExceptionResolverErrors 类型的参数处理器为 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参数

控制器方法的参数可以是 @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-Typex-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集合-批量文件上传

批量文件上传的时候,我们一般需要接收一个 MultipartFile 集合,可以有两种选择:

  1. 使用 MultipartHttpServletRequest 参数,直接调用 getFiles 方法获取 MultipartFile 列表。
  2. 使用 @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或者@JsonFormat

@DateTimeFormat 注解配合 @RequestBody 的参数使用的时候,会发现抛出 InvalidFormatException 异常,提示转换失败,这是因为在处理此注解的时候,只支持 Form 表单提交( Content-Typex-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;
}

三、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. ? 匹配1个字符。
  2. * 匹配0个或者多个 字符
  3. ** 匹配路径中0个或者多个 目录
  4. 正则支持,如 {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

最后, org.springframework.util.AntPathMatcher 作为一个工具类,可以单独使用,不仅仅可以用于匹配 URL ,也可以用于匹配系统文件路径,不过需要使用其带参数构造改变内部的 pathSeparator 变量,例如:

AntPathMatcher antPathMatcher = new AntPathMatcher(File.separator);

小结

笔者在前一段时间曾经花大量时间梳理和分析过 SpringSpringMVC 的源码,但是后面一段很长的时间需要进行业务开发,对架构方面的东西有点生疏了,毕竟东西不用就会生疏,这个是常理。这篇文章基于一些 SpringMVC 的源码经验总结了请求参数的处理相关的一些知识,希望帮到自己和大家。

参考资料:

  • spring-boot-web-starter:2.0.3.RELEASE源码。

(本文完 c-7-d e-a-20180512 旧文重发)

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章