HandlerMethodArgumentResolver(一):Controller方法入参自动封装器(将参数parameter解析为值)【享学Spring MVC】(上)

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: HandlerMethodArgumentResolver(一):Controller方法入参自动封装器(将参数parameter解析为值)【享学Spring MVC】(上)

前言


在享受Spring MVC带给你便捷的时候,你是否曾经这样疑问过:Controller的handler方法参数能够自动完成参数封装(有时即使没有@PathVariable、@RequestParam、@RequestBody等注解都可),甚至在方法参数任意位置写HttpServletRequest、HttpSession、Writer…等类型的参数,它自动就有值了便可直接使用。

对此你是否想问一句:Spring MVC它是怎么办到的?那么本文就揭开它的神秘面纱,还你一片"清白"。


Spring MVC作为一个最为流行的web框架,早早已经成为了实际意义上的标准化(框架),特别是随着Struts2的突然崩塌,Spring MVC几乎一骑绝尘,因此深入了解它有着深远的意义


Spring MVC它只需要区区几个注解就能够让一个普通的java方法成为一个Handler处理器,并且还能有自动参数封装、返回值视图处理/渲染等一系列强大功能,让coder的精力更加的聚焦在自己的业务。


像JSF、Google Web Toolkit、Grails Framework等web框架至少我是没有用过的。

这里有个轻量级的web框架:Play Framework设计上我个人觉得还挺有意思,有兴趣的可以玩玩


HandlerMethodArgumentResolver


策略接口:用于在给定请求的上下文中将方法参数解析为参数值。简单的理解为:它负责处理你Handler方法里的所有入参:包括自动封装、自动赋值、校验等等。有了它才能会让Spring MVC处理入参显得那么高级、那么自动化。

Spring MVC内置了非常非常多的实现,当然若还不能满足你的需求,你依旧可以自定义和自己注册,后面我会给出自定义的示例。


有个形象的公式:HandlerMethodArgumentResolver = HandlerMethod + Argument(参数) + Resolver(解析器)。

解释为:它是HandlerMethod方法的解析器,将HttpServletRequest(header + body 中的内容)解析为HandlerMethod方法的参数(method parameters)


// @since 3.1   HandlerMethod 方法中 参数解析器
public interface HandlerMethodArgumentResolver {
  // 判断 HandlerMethodArgumentResolver 是否支持 MethodParameter
  // (PS: 一般都是通过 参数上面的注解|参数的类型)
  boolean supportsParameter(MethodParameter parameter);
  // 从NativeWebRequest中获取数据,ModelAndViewContainer用来提供访问Model
  // MethodParameter parameter:请求参数
  // WebDataBinderFactory用于创建一个WebDataBinder用于数据绑定、校验
  @Nullable
  Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}


基于这个接口的处理器实现类不可谓不丰富,非常之多。我截图如下:


image.png

因为子类众多,所以我分类进行说明。我把它分为四类进行描述:


  1. 基于Name
  2. 数据类型是Map的
  3. 固定参数类型
  4. 基于ContentType的消息转换器


第一类:基于Name

从URI(路径变量)、HttpServletRequest、HttpSession、Header、Cookie…等中根据名称key来获取值


这类处理器所有的都是基于抽象类AbstractNamedValueMethodArgumentResolver来实现,它是最为重要的分支(分类)。


// @since 3.1  负责从路径变量、请求、头等中拿到值。(都可以指定name、required、默认值等属性)
// 子类需要做如下事:获取方法参数的命名值信息、将名称解析为参数值
// 当需要参数值时处理缺少的参数值、可选地处理解析值
//特别注意的是:默认值可以使用${}占位符,或者SpEL语句#{}是木有问题的
public abstract class AbstractNamedValueMethodArgumentResolver implements HandlerMethodArgumentResolver {
  @Nullable
  private final ConfigurableBeanFactory configurableBeanFactory;
  @Nullable
  private final BeanExpressionContext expressionContext;
  private final Map<MethodParameter, NamedValueInfo> namedValueInfoCache = new ConcurrentHashMap<>(256);
  public AbstractNamedValueMethodArgumentResolver() {
    this.configurableBeanFactory = null;
    this.expressionContext = null;
  }
  public AbstractNamedValueMethodArgumentResolver(@Nullable ConfigurableBeanFactory beanFactory) {
    this.configurableBeanFactory = beanFactory;
    // 默认是RequestScope
    this.expressionContext = (beanFactory != null ? new BeanExpressionContext(beanFactory, new RequestScope()) : null);
  }
  // protected的内部类  所以所有子类(注解)都是用友这三个属性值的
  protected static class NamedValueInfo {
    private final String name;
    private final boolean required;
    @Nullable
    private final String defaultValue;
    public NamedValueInfo(String name, boolean required, @Nullable String defaultValue) {
      this.name = name;
      this.required = required;
      this.defaultValue = defaultValue;
    }
  }
  // 核心方法  注意此方法是final的,并不希望子类覆盖掉他~
  @Override
  @Nullable
  public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    // 创建 MethodParameter 对应的 NamedValueInfo
    NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
    // 支持到了Java 8 中支持的 java.util.Optional
    MethodParameter nestedParameter = parameter.nestedIfOptional();
    // name属性(也就是注解标注的value/name属性)这里既会解析占位符,还会解析SpEL表达式,非常强大
    // 因为此时的 name 可能还是被 ${} 符号包裹, 则通过 BeanExpressionResolver 来进行解析
    Object resolvedName = resolveStringValue(namedValueInfo.name);
    if (resolvedName == null) {
      throw new IllegalArgumentException("Specified name must not resolve to null: [" + namedValueInfo.name + "]");
    }
    // 模版抽象方法:将给定的参数类型和值名称解析为参数值。  由子类去实现
    // @PathVariable     --> 通过对uri解析后得到的decodedUriVariables值(常用)
    // @RequestParam     --> 通过 HttpServletRequest.getParameterValues(name) 获取(常用)
    // @RequestAttribute --> 通过 HttpServletRequest.getAttribute(name) 获取   <-- 这里的 scope 是 request
    // @SessionAttribute --> 略
    // @RequestHeader    --> 通过 HttpServletRequest.getHeaderValues(name) 获取
    // @CookieValue      --> 通过 HttpServletRequest.getCookies() 获取
    Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
    // 若解析出来值仍旧为null,那就走defaultValue (若指定了的话)
    if (arg == null) {
      // 可以发现:defaultValue也是支持占位符和SpEL的~~~
      if (namedValueInfo.defaultValue != null) {
        arg = resolveStringValue(namedValueInfo.defaultValue);
      // 若 arg == null && defaultValue == null && 非 optional 类型的参数 则通过 handleMissingValue 来进行处理, 一般是报异常
      } else if (namedValueInfo.required && !nestedParameter.isOptional()) {
        // 它是个protected方法,默认抛出ServletRequestBindingException异常
        // 各子类都复写了此方法,转而抛出自己的异常(但都是ServletRequestBindingException的异常子类)
        handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
      }
      // handleNullValue是private方法,来处理null值
      // 针对Bool类型有这个判断:Boolean.TYPE.equals(paramType) 就return Boolean.FALSE;
      // 此处注意:Boolean.TYPE = Class.getPrimitiveClass("boolean") 它指的基本类型的boolean,而不是Boolean类型哦~~~
      // 如果到了这一步(value是null),但你还是基本类型,那就抛出异常了(只有boolean类型不会抛异常哦~)
      // 这里多嘴一句,即使请求传值为&bool=1,效果同bool=true的(1:true 0:false) 并且不区分大小写哦(TrUe效果同true)
      arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
    }
    // 兼容空串,若传入的是空串,依旧还是使用默认值(默认值支持占位符和SpEL)
    else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
      arg = resolveStringValue(namedValueInfo.defaultValue);
    }
    // 完成自动化的数据绑定~~~
    if (binderFactory != null) {
      WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
      try {
        // 通过数据绑定器里的Converter转换器把arg转换为指定类型的数值
        arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
      } catch (ConversionNotSupportedException ex) { // 注意这个异常:MethodArgumentConversionNotSupportedException  类型不匹配的异常
        throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),
            namedValueInfo.name, parameter, ex.getCause());
      } catch (TypeMismatchException ex) { //MethodArgumentTypeMismatchException是TypeMismatchException 的子类
        throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),
            namedValueInfo.name, parameter, ex.getCause());
      }
    }
    // protected的方法,本类为空实现,交给子类去复写(并不是必须的)
    // 唯独只有PathVariableMethodArgumentResolver把解析处理啊的值存储一下数据到 
    // HttpServletRequest.setAttribute中(若key已经存在也不会存储了)
    handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
    return arg;
  }
  // 此处有缓存,记录下每一个MethodParameter对象   value是NamedValueInfo值
  private NamedValueInfo getNamedValueInfo(MethodParameter parameter) {
    NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter);
    if (namedValueInfo == null) {
      // createNamedValueInfo是抽象方法,子类必须实现
      namedValueInfo = createNamedValueInfo(parameter);
      // updateNamedValueInfo:这一步就是我们之前说过的为何Spring MVC可以根据参数名封装的方法
      // 如果info.name.isEmpty()的话(注解里没指定名称),就通过`parameter.getParameterName()`去获取参数名~
      // 它还会处理注解指定的defaultValue:`\n\t\.....`等等都会被当作null处理
      // 都处理好后:new NamedValueInfo(name, info.required, defaultValue);(相当于吧注解解析成了此对象嘛~~)
      namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo);
      this.namedValueInfoCache.put(parameter, namedValueInfo);
    }
    return namedValueInfo;
  }
  // 抽象方法 
  protected abstract NamedValueInfo createNamedValueInfo(MethodParameter parameter);
  // 由子类根据名称,去把值拿出来
  protected abstract Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception;
}


该抽象类中定义了解析参数的主逻辑(模版逻辑),子类只需要实现对应的抽象模版方法即可。

对此部分的处理步骤,我把它简述如下:


  1. 基于MethodParameter构建NameValueInfo <-- 主要有name, defaultValue, required(其实主要是解析方法参数上标注的注解~)
  2. 通过BeanExpressionResolver(${}占位符以及SpEL) 解析name
  3. 通过模版方法resolveName从 HttpServletRequest, Http Headers, URI template variables 等等中获取对应的属性值(具体由子类去实现)
  4. 对 arg==null这种情况的处理, 要么使用默认值, 若 required = true && arg == null, 则一般报出异常(boolean类型除外~)
  5. 通过WebDataBinder将arg转换成Methodparameter.getParameterType()类型(注意:这里仅仅只是用了数据转换而已,并没有用bind()方法)


该抽象类继承树如下:


image.png


从上源码可以看出,抽象类已经定死了处理模版(方法为final的),留给子类需要做的事就不多了,大体还有如下三件事:


  1. 根据MethodParameter创建NameValueInfo(子类的实现可继承自NameValueInfo,就是对应注解的属性们)
  2. 根据方法参数名称name从HttpServletRequest, Http Headers, URI template variables等等中获取属性值
  3. 对arg == null这种情况的处理(非必须)


PathVariableMethodArgumentResolver


它帮助Spring MVC实现restful风格的URL。它用于处理标注有@PathVariable注解的方法参数,用于从URL中获取值(并不是?后面的参数哦)。

并且,并且,并且它还可以解析@PathVariable注解的value值不为空的Map(使用较少,个人不太建议使用)~


UriComponentsContributor接口:通过查看方法参数和参数值并决定应更新目标URL的哪个部分,为构建UriComponents的策略接口。


// @since 4.0 出现得还是比较晚的
public interface UriComponentsContributor {
  // 此方法完全同HandlerMethodArgumentResolver的这个方法~~~
  boolean supportsParameter(MethodParameter parameter);
  // 处理给定的方法参数,然后更新UriComponentsbuilder,或者使用uri变量添加到映射中,以便在处理完所有参数后用于扩展uri~~~
  void contributeMethodArgument(MethodParameter parameter, Object value, UriComponentsBuilder builder,
      Map<String, Object> uriVariables, ConversionService conversionService);
}


它的三个实现类:


image.png


关于此接口的使用,后面再重点介绍,此处建议自动选择性忽略。


相关文章
|
1天前
|
数据处理 Python
Python 高级技巧:深入解析读取 Excel 文件的多种方法
在数据分析中,从 Excel 文件读取数据是常见需求。本文介绍了使用 Python 的三个库:`pandas`、`openpyxl` 和 `xlrd` 来高效处理 Excel 文件的方法。`pandas` 提供了简洁的接口,而 `openpyxl` 和 `xlrd` 则针对不同版本的 Excel 文件格式提供了详细的数据读取和处理功能。此外,还介绍了如何处理复杂格式(如合并单元格)和进行性能优化(如分块读取)。通过这些技巧,可以轻松应对各种 Excel 数据处理任务。
27 16
|
19天前
|
存储 关系型数据库 MySQL
技术解析:MySQL中取最新一条重复数据的方法
以上提供的两种方法都可以有效地从MySQL数据库中提取每个类别最新的重复数据。选择哪种方法取决于具体的使用场景和MySQL版本。子查询加分组的方法兼容性更好,适用于所有版本的MySQL;而窗口函数方法代码更简洁,执行效率可能更高,但需要MySQL 8.0及以上版本。在实际应用中,应根据数据量大小、查询性能需求以及MySQL版本等因素综合考虑,选择最合适的实现方案。
92 6
|
10天前
|
Java Spring
spring boot 启动项目参数的设定
spring boot 启动项目参数的设定
|
2月前
|
项目管理 敏捷开发 开发框架
敏捷与瀑布的对决:解析Xamarin项目管理中如何运用敏捷方法提升开发效率并应对市场变化
【8月更文挑战第31天】在数字化时代,项目管理对软件开发至关重要,尤其是在跨平台框架 Xamarin 中。本文《Xamarin 项目管理:敏捷方法的应用》通过对比传统瀑布方法与敏捷方法,揭示敏捷在 Xamarin 项目中的优势。瀑布方法按线性顺序推进,适用于需求固定的小型项目;而敏捷方法如 Scrum 则强调迭代和增量开发,更适合需求多变、竞争激烈的环境。通过详细分析两种方法在 Xamarin 项目中的实际应用,本文展示了敏捷方法如何提高灵活性、适应性和开发效率,使其成为 Xamarin 项目成功的利器。
40 1
|
2月前
|
安全 数据安全/隐私保护 架构师
用Vaadin打造坚不可摧的企业级应用:安全性考虑全解析
【8月更文挑战第31天】韩林是某金融科技公司的架构师,负责构建安全的企业级应用。在众多Web框架中,他选择了简化UI设计并内置多项安全特性的Vaadin。韩林在其技术博客中分享了使用Vaadin时的安全考虑与实现方法,包括数据加密、SSL/TLS保护、结合Spring Security的用户认证、XSS防护、CSRF防御及事务性UI更新机制。他强调,虽然Vaadin提供了丰富的安全功能,但还需根据具体需求进行调整和增强。通过合理设计,可以构建高效且安全的企业级Web应用。
35 0
|
2月前
|
测试技术 数据库
确保数据访问层的可靠性:详细解析使用Entity Framework Core进行隔离的单元测试方法
【8月更文挑战第31天】在软件开发中,单元测试是确保代码质量的关键。本文通过一个在线商店的商品查询功能案例,介绍了如何使用EF Core和Moq框架实现数据访问层的隔离测试。通过模拟`ApplicationDbContext`,我们能够在不访问真实数据库的情况下对`ProductService`进行单元测试,提高测试效率并保证测试稳定性。这种方法是实现高效、可靠单元测试的重要手段。
36 0
|
2月前
|
前端开发 JavaScript 开发者
React生命周期方法完全指南:深入理解并高效应用每个阶段的钩子——从初始化到卸载的全方位解析
【8月更文挑战第31天】本文详细介绍了React组件生命周期方法,包括初始化、挂载、更新和卸载四个阶段的关键钩子。通过探讨每个阶段的方法,如`componentDidMount`和`componentWillUnmount`,帮助开发者在正确时机执行所需操作,提升应用性能。文章还提供了最佳实践,指导如何避免常见错误并充分利用最新钩子。
52 0
|
2月前
|
监控 网络协议 Java
Tomcat源码解析】整体架构组成及核心组件
Tomcat,原名Catalina,是一款优雅轻盈的Web服务器,自4.x版本起扩展了JSP、EL等功能,超越了单纯的Servlet容器范畴。Servlet是Sun公司为Java编程Web应用制定的规范,Tomcat作为Servlet容器,负责构建Request与Response对象,并执行业务逻辑。
Tomcat源码解析】整体架构组成及核心组件
|
2月前
|
存储 NoSQL Redis
redis 6源码解析之 object
redis 6源码解析之 object
58 6
|
22天前
|
存储 缓存 Java
什么是线程池?从底层源码入手,深度解析线程池的工作原理
本文从底层源码入手,深度解析ThreadPoolExecutor底层源码,包括其核心字段、内部类和重要方法,另外对Executors工具类下的四种自带线程池源码进行解释。 阅读本文后,可以对线程池的工作原理、七大参数、生命周期、拒绝策略等内容拥有更深入的认识。
什么是线程池?从底层源码入手,深度解析线程池的工作原理

推荐镜像

更多
下一篇
无影云桌面