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

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: HandlerMethodArgumentResolver(一):Controller方法入参自动封装器(将参数parameter解析为值)【享学Spring MVC】(中)
// @since 3.0 需要注意的是:它只支持标注在@RequestMapping的方法(处理器)上使用~
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PathVariable {
  @AliasFor("name")
  String value() default "";
  @AliasFor("value")
  String name() default "";
  // 注意:它并没有defaultValue哦~
  // @since 4.3.3  它也是标记为false非必须的~~~~
  boolean required() default true;
}
// @since 3.1
public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver implements UriComponentsContributor {
  private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
  // 简单一句话描述:@PathVariable是必须,不管你啥类型
  // 标注了注解,且是Map类型,
  @Override
  public boolean supportsParameter(MethodParameter parameter) {
    if (!parameter.hasParameterAnnotation(PathVariable.class)) {
      return false;
    }
    if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
      PathVariable pathVariable = parameter.getParameterAnnotation(PathVariable.class);
      return (pathVariable != null && StringUtils.hasText(pathVariable.value()));
    }
    return true;
  }
  @Override
  protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
    PathVariable ann = parameter.getParameterAnnotation(PathVariable.class);
    return new PathVariableNamedValueInfo(ann);
  }
  private static class PathVariableNamedValueInfo extends NamedValueInfo {
    public PathVariableNamedValueInfo(PathVariable annotation) {
      // 默认值使用的DEFAULT_NONE~~~
      super(annotation.name(), annotation.required(), ValueConstants.DEFAULT_NONE);
    }
  }
  // 根据name去拿值的过程非常之简单,但是它和前面的只知识是有关联的
  // 至于这个attr是什么时候放进去的,AbstractHandlerMethodMapping.handleMatch()匹配处理器方法上
  // 通过UrlPathHelper.decodePathVariables() 把参数提取出来了,然后放进request属性上暂存了~~~
  // 关于HandlerMapping内容,可来这里:https://blog.csdn.net/f641385712/article/details/89810020
  @Override
  @Nullable
  protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
    Map<String, String> uriTemplateVars = (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
    return (uriTemplateVars != null ? uriTemplateVars.get(name) : null);
  }
  // MissingPathVariableException是ServletRequestBindingException的子类
  @Override
  protected void handleMissingValue(String name, MethodParameter parameter) throws ServletRequestBindingException {
    throw new MissingPathVariableException(name, parameter);
  }
  // 值完全处理结束后,把处理好的值放进请求域,方便view里渲染时候使用~
  // 抽象父类的handleResolvedValue方法,只有它复写了~
  @Override
  @SuppressWarnings("unchecked")
  protected void handleResolvedValue(@Nullable Object arg, String name, MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest request) {
    String key = View.PATH_VARIABLES;
    int scope = RequestAttributes.SCOPE_REQUEST;
    Map<String, Object> pathVars = (Map<String, Object>) request.getAttribute(key, scope);
    if (pathVars == null) {
      pathVars = new HashMap<>();
      request.setAttribute(key, pathVars, scope);
    }
    pathVars.put(name, arg);
  }
  ...
}


关于@PathVariable的使用,不用再给例子了。


说明:因为使用路径参数需要进行复杂的匹配流程以及正则匹配,所有效率相较来说低些,若以若是那种对响应事件强要求的(比如记录点击事件…),建议用请求参数代替(当然你也可以重写RequestMappingHandlerMapping的URL匹配方法来定制化你的需求)。

GET /list/cityId/1 属于RESTful /list/cityId?cityId=1不属于RESTful。通过Apache JMeter测试:非RESTful接口的性能是RESTful接口的两倍,接口相应时间上更是达到10倍左右(是–>300ms左右 非–>20ms左右)


针对RESTful此处我提出一个思考题:若你是一个现成的系统,现对相应提出要求:接口耗时必须控制在50ms以内,怎么破?

思路一:将所有的url修改为非RESTful风格(不使用@PathVariable)

痛点:系统已存在几百个接口,若修改不仅需要修改服务端,客户端也得改,工作量太大。并且稍有不慎,容易造成404现象~


思路二:定制化AbstractHandlerMethodMapping#lookupHandlerMethod方法

此方法负责URL的匹配,我们为了提效其实就是为了避免一些正则匹配(AntPathMatcher)。


对此文答案有兴趣的可参见此文:SpringMVC RESTful 性能优化


唯一需要说一下如果类型是Map类型的情况下的使用注意事项,如下:

@PathVariable("jsonStr") Map<String,Object> map


希望把jsonStr对应的字符串解析成键值对封装进Map里。那么你必须,必须,必须注册了能处理此字符串的Converter/PropertyEditor(自定义)。使用起来相对麻烦,但技术隐蔽性高。我一般不建议这么来用~

关于@PathVariable的required=false使用注意事项


这个功能是很多人比较疑问的,如何使用???

@ResponseBody
@GetMapping("/test/{id}")
public Person test(@PathVariable(required = false) Integer id) { ... }



以为这样写通过/test这个url就能访问到了,其实这样是不行的,会404。

正确姿势:

@ResponseBody
@GetMapping({"/test/{id}", "/test"})
public Person test(@PathVariable(required = false) Integer id) { ... }


这样/test和/test/1这两个url就都能正常work了~


@PathVariable的required=false使用较少,一般用于在用URL传多个值时,但有些值是非必传的时候使用。比如这样的URL:"/user/{id}/{name}","/user/{id}","/user"


RequestParamMethodArgumentResolver


顾名思义,是解析标注有@RequestParam的方法入参解析器,这个注解比上面的注解强大很多了,它用于从请求参数(?后面的)中获取值完成封装。这是我们的绝大多数使用场景。除此之外,它还支持MultipartFile,也就是说能够从MultipartHttpServletRequest | HttpServletRequest 获取数据,并且并且并且还兜底处理没有标注任何注解的“简单类型”~

// @since 2.5
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
  @AliasFor("name")
  String value() default "";
   // @since 4.2
  @AliasFor("value")
  String name() default "";
  boolean required() default true;
  String defaultValue() default ValueConstants.DEFAULT_NONE;
}
// @since 3.1
public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver implements UriComponentsContributor {
  private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
  // 这个参数老重要了:
  // true:表示参数类型是基本类型 参考BeanUtils#isSimpleProperty(什么Enum、Number、Date、URL、包装类型、以上类型的数组类型等等)
  // 如果是基本类型,即使你不写@RequestParam注解,它也是会走进来处理的~~~(这个@PathVariable可不会哟~)
  // fasle:除上以外的。  要想它处理就必须标注注解才行哦,比如List等~
  // 默认值是false
  private final boolean useDefaultResolution;
  // 此构造只有`MvcUriComponentsBuilder`调用了  传入的false
  public RequestParamMethodArgumentResolver(boolean useDefaultResolution) {
    this.useDefaultResolution = useDefaultResolution;
  }
  // 传入了ConfigurableBeanFactory ,所以它支持处理占位符${...} 并且支持SpEL了
  // 此构造都在RequestMappingHandlerAdapter里调用,最后都会传入true来Catch-all Case  这种设计挺有意思的
  public RequestParamMethodArgumentResolver(@Nullable ConfigurableBeanFactory beanFactory, boolean useDefaultResolution) {
    super(beanFactory);
    this.useDefaultResolution = useDefaultResolution;
  }
  // 此处理器能处理如下Case:
  // 1、所有标注有@RequestParam注解的类型(非Map)/ 注解指定了value值的Map类型(自己提供转换器哦)
  // ======下面都表示没有标注@RequestParam注解了的=======
  // 1、不能标注有@RequestPart注解,否则直接不处理了
  // 2、是上传的request:isMultipartArgument() = true(MultipartFile类型或者对应的集合/数组类型  或者javax.servlet.http.Part对应结合/数组类型)
  // 3、useDefaultResolution=true情况下,"基本类型"也会处理
  @Override
  public boolean supportsParameter(MethodParameter parameter) {
    if (parameter.hasParameterAnnotation(RequestParam.class)) {
      if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
        RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
        return (requestParam != null && StringUtils.hasText(requestParam.name()));
      } else {
        return true;
      }
    } else {
      if (parameter.hasParameterAnnotation(RequestPart.class)) {
        return false;
      }
      parameter = parameter.nestedIfOptional();
      if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
        return true;
      } else if (this.useDefaultResolution) {
        return BeanUtils.isSimpleProperty(parameter.getNestedParameterType());
      } else {
        return false;
      }
    }
  }
  // 从这也可以看出:即使木有@RequestParam注解,也是可以创建出一个NamedValueInfo来的
  @Override
  protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
    RequestParam ann = parameter.getParameterAnnotation(RequestParam.class);
    return (ann != null ? new RequestParamNamedValueInfo(ann) : new RequestParamNamedValueInfo());
  }
  // 内部类
  private static class RequestParamNamedValueInfo extends NamedValueInfo {
    // 请注意这个默认值:如果你不写@RequestParam,那么就会用这个默认值
    // 注意:required = false的哟(若写了注解,required默认可是true,请务必注意区分)
    // 因为不写注解的情况下,若是简单类型参数都是交给此处理器处理的。所以这个机制需要明白
    // 复杂类型(非简单类型)默认是ModelAttributeMethodProcessor处理的
    public RequestParamNamedValueInfo() {
      super("", false, ValueConstants.DEFAULT_NONE);
    }
    public RequestParamNamedValueInfo(RequestParam annotation) {
      super(annotation.name(), annotation.required(), annotation.defaultValue());
    }
  }
  // 核心方法:根据Name 获取值(普通/文件上传)
  // 并且还有集合、数组等情况
  @Override
  @Nullable
  protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
    HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
    // 这块解析出来的是个MultipartFile或者其集合/数组
    if (servletRequest != null) {
      Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
      if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
        return mpArg;
      }
    }
    Object arg = null;
    MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class);
    if (multipartRequest != null) {
      List<MultipartFile> files = multipartRequest.getFiles(name);
      if (!files.isEmpty()) {
        arg = (files.size() == 1 ? files.get(0) : files);
      }
    }
    // 若解析出来值仍旧为null,那处理完文件上传里木有,那就去参数里取吧
    // 由此可见:文件上传的优先级是高于请求参数的
    if (arg == null) {
      //小知识点:getParameter()其实本质是getParameterNames()[0]的效果
      // 强调一遍:?ids=1,2,3 结果是["1,2,3"](兼容方式,不建议使用。注意:只能是逗号分隔)
      // ?ids=1&ids=2&ids=3  结果是[1,2,3](标准的传值方式,建议使用)
      // 但是Spring MVC这两种都能用List接收  请务必注意他们的区别~~~
      String[] paramValues = request.getParameterValues(name);
      if (paramValues != null) {
        arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
      }
    }
    return arg;
  }
  ...
}

可以看到ServletModelAttributeMethodProcessor和RequestParamMethodArgumentResolver一样,也是有兜底的效果的。


相关文章
|
20小时前
|
前端开发 Java Spring
【Spring】“请求“ 之传递单个参数、传递多个参数和传递对象
【Spring】“请求“ 之传递单个参数、传递多个参数和传递对象
10 2
|
20小时前
|
前端开发 Java 应用服务中间件
【Spring】Spring MVC的项目准备和连接建立
【Spring】Spring MVC的项目准备和连接建立
11 2
|
3天前
|
XML 前端开发 Java
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
本文阐述了Spring、Spring Boot和Spring MVC的关系与区别,指出Spring是一个轻量级、一站式、模块化的应用程序开发框架,Spring MVC是Spring的一个子框架,专注于Web应用和网络接口开发,而Spring Boot则是对Spring的封装,用于简化Spring应用的开发。
16 0
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
|
28天前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
|
16天前
|
Java 应用服务中间件 Spring
IDEA 工具 启动 spring boot 的 main 方法报错。已解决
IDEA 工具 启动 spring boot 的 main 方法报错。已解决
|
15天前
|
XML 缓存 前端开发
springMVC02,restful风格,请求转发和重定向
文章介绍了RESTful风格的基本概念和特点,并展示了如何使用SpringMVC实现RESTful风格的请求处理。同时,文章还讨论了SpringMVC中的请求转发和重定向的实现方式,并通过具体代码示例进行了说明。
springMVC02,restful风格,请求转发和重定向
|
15天前
|
Java Spring
spring boot 启动项目参数的设定
spring boot 启动项目参数的设定
|
2月前
|
Java Spring
|
2月前
|
存储 SQL Java
|
15天前
|
SQL 监控 druid
springboot-druid数据源的配置方式及配置后台监控-自定义和导入stater(推荐-简单方便使用)两种方式配置druid数据源
这篇文章介绍了如何在Spring Boot项目中配置和监控Druid数据源,包括自定义配置和使用Spring Boot Starter两种方法。

推荐镜像

更多