zl程序教程

您现在的位置是:首页 >  后端

当前栏目

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

SpringMVC 简单 总结 参数 请求 接收 2.5 字长
2023-09-27 14:25:57 时间
其他参数


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


请求头


请求头的值主要通过 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 需要在注解中指定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的方式


微信截图_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集合 可以有两种选择


使用MultipartHttpServletRequest参数 直接调用getFiles方法获取MultipartFile列表。使用 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-Type为x-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个字符。*匹配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 旧文重发 封面图来源于日漫《神风怪盗》