2.5万字长文简单总结SpringMVC请求参数接收(下)

简介: 在日常使用SpringMVC进行开发的时候,有可能遇到前端各种类型的请求参数,这里做一次相对全面的总结。

其他参数



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


请求头


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


微信截图_20220513123720.png

spmvc-p-9


控制器方法代码:


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


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对应的值。


在一个控制器(使用了@ControllerSpring组件)中,如果存在一到多个使用了@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),见全局异常处理器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){
        // 这里处理异常和返回值
    }
    @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-Typex-www-form-urlencoded,不能使用application/json的方式:


微信截图_20220513123731.png

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集合,可以有两种选择:


  1. 使用MultipartHttpServletRequest参数,直接调用getFiles方法获取MultipartFile列表。
  2. 使用@RequestParam注解修饰MultipartFile列表,参数处理器是RequestParamMethodArgumentResolver,其实就是第1种方式的封装而已。


微信截图_20220513123737.png

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;
}
复制代码


微信截图_20220513123745.png

spmvc-p-12


使用字符串接收后再转换的缺点就是模板代码太多,编码风格不够简洁,重复性工作太多,如果有代码洁癖或者类似笔者这样是一个节能主义者,一般不会选用这种方式。


二、使用注解@DateTimeFormat或者@JsonFormat


@DateTimeFormat注解配合@RequestBody的参数使用的时候,会发现抛出InvalidFormatException异常,提示转换失败,这是因为在处理此注解的时候,只支持Form表单提交(Content-Typex-www-form-urlencoded),例子如下:


微信截图_20220513123752.png

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. ?匹配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。上面的例子只是列举了SpringMVCURL匹配的典型例子,并没有深入展开。

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


AntPathMatcher antPathMatcher = new AntPathMatcher(File.separator);
复制代码


小结



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


参考资料:

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


(本文完 c-7-d e-a-20180512 r-a-20200713 旧文重发 封面图来源于日漫《神风怪盗》)

相关文章
|
前端开发 网络架构
SpringMVC -->ant风格的路径 -->占位符 -->获取请求参数 -->@RequestParam
SpringMVC -->ant风格的路径 -->占位符 -->获取请求参数 -->@RequestParam
115 0
|
前端开发 Java 应用服务中间件
[SpringMVC]请求与响应①(映射路径、请求参数)
请求与响应①(映射路径、请求参数)
[SpringMVC]请求与响应①(映射路径、请求参数)
|
JSON 前端开发 fastjson
2.5万字长文简单总结SpringMVC请求参数接收(上)
在日常使用SpringMVC进行开发的时候,有可能遇到前端各种类型的请求参数,这里做一次相对全面的总结。
279 0
2.5万字长文简单总结SpringMVC请求参数接收(上)
|
Java
springMvc源码学习之:spirngMvc获取请求参数的方法
一、      通过@PathVariabl获取路径中的参数 @RequestMapping(value="user/{id}/{name}",method=RequestMethod.
1051 0
|
前端开发 Java 网络架构
springMvc源码学习之:spirngMVC获取请求参数的方法2
  @RequestParam,你一定见过;@PathVariable,你肯定也知道;@QueryParam,你怎么会不晓得?!还有你熟悉的他 (@CookieValue)!她(@ModelAndView)!它(@ModelAttribute)!没错,仅注解这块,spring mvc就为你打开了五彩斑斓的世界。
1088 0
|
5月前
|
设计模式 前端开发 JavaScript
Spring MVC(一)【什么是Spring MVC】
Spring MVC(一)【什么是Spring MVC】
|
4月前
|
设计模式 前端开发 Java
【Spring MVC】快速学习使用Spring MVC的注解及三层架构
【Spring MVC】快速学习使用Spring MVC的注解及三层架构
55 1
|
4月前
|
前端开发 Java 应用服务中间件
Spring框架第六章(SpringMVC概括及基于JDK21与Tomcat10创建SpringMVC程序)
Spring框架第六章(SpringMVC概括及基于JDK21与Tomcat10创建SpringMVC程序)
|
4月前
|
XML Java 数据格式
SpringMVC的XML配置解析-spring18
SpringMVC的XML配置解析-spring18
|
4月前
|
应用服务中间件
从代码角度戳一下springMVC的运行过程-spring16
从代码角度戳一下springMVC的运行过程-spring16