User-Profile-Image
hankin
  • 5
  • Java
  • Kotlin
  • Spring
  • Web
  • SQL
  • MegaData
  • More
  • Experience
  • Enamiĝu al vi
  • 分类
    • Zuul
    • Zookeeper
    • XML
    • WebSocket
    • Web Notes
    • Web
    • Vue
    • Thymeleaf
    • SQL Server
    • SQL Notes
    • SQL
    • SpringSecurity
    • SpringMVC
    • SpringJPA
    • SpringCloud
    • SpringBoot
    • Spring Notes
    • Spring
    • Servlet
    • Ribbon
    • Redis
    • RabbitMQ
    • Python
    • PostgreSQL
    • OAuth2
    • NOSQL
    • Netty
    • MySQL
    • MyBatis
    • More
    • MinIO
    • MegaData
    • Maven
    • LoadBalancer
    • Kotlin Notes
    • Kotlin
    • Kafka
    • jQuery
    • JavaScript
    • Java Notes
    • Java
    • Hystrix
    • Git
    • Gateway
    • Freemarker
    • Feign
    • Eureka
    • ElasticSearch
    • Docker
    • Consul
    • Ajax
    • ActiveMQ
  • 页面
    • 归档
    • 摘要
    • 杂图
    • 问题随笔
  • 友链
    • Spring Cloud Alibaba
    • Spring Cloud Alibaba - 指南
    • Spring Cloud
    • Nacos
    • Docker
    • ElasticSearch
    • Kotlin中文版
    • Kotlin易百
    • KotlinWeb3
    • KotlinNhooo
    • 前端开源搜索
    • Ktorm ORM
    • Ktorm-KSP
    • Ebean ORM
    • Maven
    • 江南一点雨
    • 江南国际站
    • 设计模式
    • 熊猫大佬
    • java学习
    • kotlin函数查询
    • Istio 服务网格
    • istio
    • Ktor 异步 Web 框架
    • PostGis
    • kuangstudy
    • 源码地图
    • it教程吧
    • Arthas-JVM调优
    • Electron
    • bugstack虫洞栈
    • github大佬宝典
    • Sa-Token
    • 前端技术胖
    • bennyhuo-Kt大佬
    • Rickiyang博客
    • 李大辉大佬博客
    • KOIN
    • SQLDelight
    • Exposed-Kt-ORM
    • Javalin—Web 框架
    • http4k—HTTP包
    • 爱威尔大佬
    • 小土豆
    • 小胖哥安全框架
    • 负雪明烛刷题
    • Kotlin-FP-Arrow
    • Lua参考手册
    • 美团文章
    • Java 全栈知识体系
    • 尼恩架构师学习
    • 现代 JavaScript 教程
    • GO相关文档
    • Go学习导航
    • GoCN社区
    • GO极客兔兔-案例
    • 讯飞星火GPT
    • Hollis博客
    • PostgreSQL德哥
    • 优质博客推荐
    • 半兽人大佬
    • 系列教程
    • PostgreSQL文章
    • 云原生资料库
    • 并发博客大佬
Help?

Please contact us on our email for need any support

Support
    首页   ›   Spring   ›   正文
Spring

SpringBoot—自定义参数解析器

2022-09-23 11:53:33
943  0 1
参考目录 隐藏
1) 参数提取
2) 自定义参数解析器
3) 案例
4) 参数解析器
5) 参数解析器概览
6) AbstractNamedValueMethodArgumentResolver
7) RequestParamMethodArgumentResolver

阅读完需:约 14 分钟

在一个 Web 请求中,参数我们无非就是放在地址栏或者请求体中,个别请求可能放在请求头中。

放在地址栏中,我们可以通过如下方式获取参数:

String name = request.getParameter("name ");

放在请求体中,如果是 key/value 形式,我们可以通过如下方式获取参数:

String name = request.getParameter("name ");

如果是 JSON 形式,我们则通过如果如下方式获取到输入流,然后解析成 JSON 字符串,再通过 JSON 工具转为对象:

BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
String json = reader.readLine();
reader.close();
User user = new ObjectMapper().readValue(json, User.class);

如果参数放在请求头中,我们可以通过如下方式获取:

如果用的是 Jsp/Servlet 那一套技术栈,那么参数获取无外乎这几种方式。

如果用了 SpringMVC 框架,那参数获取方式太丰富了,各种注解如 @RequestParam、@RequestBody、@RequestHeader、@PathVariable,参数可以是 key/value 形式,也可以是 JSON 形式,非常丰富!但是,无论多么丰富,最底层获取参数的方式无外乎上面几种。

参数提取

SpringMVC 到底是怎么样从 request 中把参数提取出来?

例如下面这个接口:

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello(String name) {
        return "hello "+name;
    }
}

我们都知道 name 参数是从 HttpServletRequest 中提取出来的,到底是怎么提取出来的?

自定义参数解析器

SpringBoot依赖版本

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.2</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

之前在探究MVC接口请求的时候就有过发现,自定义参数解析器需要实现 HandlerMethodArgumentResolver 接口

SpringMVC—工作机制和请求生命周期

从上面的文章上可以得到SpringMVC是如何使用这个HandlerMethodArgumentResolver的

HandlerMethodArgumentResolver的接口探究

public interface HandlerMethodArgumentResolver {
 boolean supportsParameter(MethodParameter parameter);
 @Nullable
 Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
   NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;

}

这个接口中就两个方法:

  • supportsParameter:该方法表示是否启用这个参数解析器,返回 true 表示启用,返回 false 表示不启用。
  • resolveArgument:这是具体的解析过程,就是从 request 中取出参数的过程,方法的返回值就对应了接口中参数的值。

自定义参数解析器只需要实现该接口即可。

案例

给接口上的参数赋值,原本是从HttpServletRequest中获取,但是测试为了方便直接手动赋值

可以先写一个接口做测试接口

@RestController
public class HelloController {


    /**
     * supportsParameter:如果参数类型是 String,并且参数上有 @CurrentUserName 注解,则使用该参数解析器。
     *
     * @param name
     * @return
     */
    @GetMapping("/user")
    public String hello(@CurrentUserName String name) {
        return "hello "+name;
    }
}

其次需要一个自定义注解来表明需要赋值的对象

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface CurrentUserName {
}

重头戏是我们自定义参数解析器 CurrentUserNameHandlerMethodArgumentResolver,如下:

/**
 * 自定义参数解析
 * @author xujiahui
 */
public class CurrentUserNameHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {

    /**
     * supportsParameter:该方法表示是否启用这个参数解析器,返回 true 表示启用,返回 false 表示不启用。
     * @param parameter the method parameter to check
     * @return
     */
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterType().isAssignableFrom(String.class)&&parameter.hasParameterAnnotation(CurrentUserName.class);
    }

    /**
     * resolveArgument:这是具体的解析过程,就是从 request 中取出参数的过程,方法的返回值就对应了接口中参数的值。
     *
     * @param parameter the method parameter to resolve. This parameter must
     * have previously been passed to {@link #supportsParameter} which must
     * have returned {@code true}.
     * @param mavContainer the ModelAndViewContainer for the current request
     * @param webRequest the current request
     * @param binderFactory a factory for creating {@link WebDataBinder} instances
     * @return
     * @throws Exception
     */
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        User user=new User();
        user.setName("测试数据");
        return user.getName();
    }
}

最后,我们再将自定义的参数解析器配置到 HandlerAdapter 中,配置方式如下:

/**
 * 最后将自定义的参数解析器配置到 HandlerAdapter 中
 * @author xujiahui
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new CurrentUserNameHandlerMethodArgumentResolver());
    }
}

至此,就算配置完成了。

接下来启动项目,访问 /hello 接口,就可以看到返回当前登录用户数据了。

这就是我们自定义的一个参数类型解析器。可以看到,非常 Easy。

在 SpringMVC 中,默认也有很多 HandlerMethodArgumentResolver 的实现类。


参数解析器

HandlerMethodArgumentResolver 就是我们口口声声说的参数解析器,它的实现类还是蛮多的,因为每一种类型的参数都对应了一个参数解析器:

为了理解方便,我们可以将这些参数解析器分为四大类:

  • xxxMethodArgumentResolver:这就是一个普通的参数解析器。
  • xxxMethodProcessor:不仅可以当作参数解析器,还可以处理对应类型的返回值。
  • xxxAdapter:这种不做参数解析,仅仅用来作为 WebArgumentResolver 类型的参数解析器的适配器。
  • HandlerMethodArgumentResolverComposite:这个看名字就知道是一个组合解析器,它是一个代理,具体代理其他干活的那些参数解析器。

大致上可以分为这四类,其中最重要的当然就是前两种了。

参数解析器概览

接下来我们来先来大概看看这些参数解析器分别都是用来干什么的。

MapMethodProcessor

这个用来处理 Map/ModelMap 类型的参数,解析完成后返回 model。

PathVariableMethodArgumentResolver

这个用来处理使用了 @PathVariable 注解并且参数类型不为 Map 的参数,参数类型为 Map 则使用 PathVariableMapMethodArgumentResolver 来处理。

PathVariableMapMethodArgumentResolver

见上。

ErrorsMethodArgumentResolver

这个用来处理 Error 参数,例如我们做参数校验时的 BindingResult。

AbstractNamedValueMethodArgumentResolver

这个用来处理 key/value 类型的参数,如请求头参数、使用了 @PathVariable 注解的参数以及 Cookie 等。

RequestHeaderMethodArgumentResolver

这个用来处理使用了 @RequestHeader 注解,并且参数类型不是 Map 的参数(参数类型是 Map 的使用 RequestHeaderMapMethodArgumentResolver)。

RequestHeaderMapMethodArgumentResolver

见上。

RequestAttributeMethodArgumentResolver

这个用来处理使用了 @RequestAttribute 注解的参数。

RequestParamMethodArgumentResolver

这个功能就比较广了。使用了 @RequestParam 注解的参数、文件上传的类型 MultipartFile、或者一些没有使用任何注解的基本类型(Long、Integer)以及 String 等,都使用该参数解析器处理。需要注意的是,如果 @RequestParam 注解的参数类型是 Map,则该注解必须有 name 值,否则解析将由 RequestParamMapMethodArgumentResolver 完成。

RequestParamMapMethodArgumentResolver

见上。

AbstractCookieValueMethodArgumentResolver

这个是一个父类,处理使用了 @CookieValue 注解的参数。

ServletCookieValueMethodArgumentResolver

这个处理使用了 @CookieValue 注解的参数。

MatrixVariableMethodArgumentResolver

这个处理使用了 @MatrixVariable 注解并且参数类型不是 Map 的参数,如果参数类型是 Map,则使用 MatrixVariableMapMethodArgumentResolver 来处理。

MatrixVariableMapMethodArgumentResolver

见上。

SessionAttributeMethodArgumentResolver

这个用来处理使用了 @SessionAttribute 注解的参数。

ExpressionValueMethodArgumentResolver

这个用来处理使用了 @Value 注解的参数。

ServletResponseMethodArgumentResolver

这个用来处理 ServletResponse、OutputStream 以及 Writer 类型的参数。

ModelMethodProcessor

这个用来处理 Model 类型参数,并返回 model。

ModelAttributeMethodProcessor

这个用来处理使用了 @ModelAttribute 注解的参数。

SessionStatusMethodArgumentResolver

这个用来处理 SessionStatus 类型的参数。

PrincipalMethodArgumentResolver

这个用来处理 Principal 类型参数,这是SpringSecurity框架中用来获取用户名的处理器

AbstractMessageConverterMethodArgumentResolver

这是一个父类,当使用 HttpMessageConverter 解析 requestbody 类型参数时,相关的处理类都会继承自它。

RequestPartMethodArgumentResolver

这个用来处理使用了 @RequestPart 注解、MultipartFile 以及 Part 类型的参数。

AbstractMessageConverterMethodProcessor

这是一个工具类,不承担参数解析任务。

RequestResponseBodyMethodProcessor

这个用来处理添加了 @RequestBody 注解的参数。

HttpEntityMethodProcessor

这个用来处理 HttpEntity 和 RequestEntity 类型的参数。

ContinuationHandlerMethodArgumentResolver

AbstractWebArgumentResolverAdapter

这种不做参数解析,仅仅用来作为 WebArgumentResolver 类型的参数解析器的适配器。

ServletWebArgumentResolverAdapter

这个给父类提供 request。

UriComponentsBuilderMethodArgumentResolver

这个用来处理 UriComponentsBuilder 类型的参数。

ServletRequestMethodArgumentResolver

这个用来处理 WebRequest、ServletRequest、MultipartRequest、HttpSession、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId 类型的参数。

HandlerMethodArgumentResolverComposite

这个看名字就知道是一个组合解析器,它是一个代理,具体代理其他干活的那些参数解析器。

RedirectAttributesMethodArgumentResolver

这个用来处理 RedirectAttributes 类型的参数。

各个参数解析器的大致功能就给大家介绍完了,接下来我们选择其中一种,来具体说说它的源码。


AbstractNamedValueMethodArgumentResolver

AbstractNamedValueMethodArgumentResolver 是一个抽象类,一些键值对类型的参数解析器都是通过继承它实现的,它里边定义了很多这些键值对类型参数解析器的公共操作。

AbstractNamedValueMethodArgumentResolver 中也是应用了很多模版模式,例如它没有实现 supportsParameter 方法,该方法的具体实现在不同的子类中,resolveArgument 方法它倒是实现了,我们一起来看下

@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
  NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
 NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
 MethodParameter nestedParameter = parameter.nestedIfOptional();
 Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
 if (resolvedName == null) {
  throw new IllegalArgumentException(
    "Specified name must not resolve to null: [" + namedValueInfo.name + "]");
 }
 Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
 if (arg == null) {
  if (namedValueInfo.defaultValue != null) {
   arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
  }
  else if (namedValueInfo.required && !nestedParameter.isOptional()) {
   handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
  }
  arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
 }
 else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
  arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
 }
 if (binderFactory != null) {
  WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
  try {
   arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
  }
  catch (ConversionNotSupportedException ex) {
   throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),
     namedValueInfo.name, parameter, ex.getCause());
  }
  catch (TypeMismatchException ex) {
   throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),
     namedValueInfo.name, parameter, ex.getCause());
  }
  // Check for null value after conversion of incoming argument value
  if (arg == null && namedValueInfo.defaultValue == null &&
    namedValueInfo.required && !nestedParameter.isOptional()) {
   handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
  }
 }
 handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
 return arg;
}
  • 1. 首先根据当前请求获取一个 NamedValueInfo 对象,这个对象中保存了参数的三个属性:参数名、参数是否必须以及参数默认值。具体的获取过程就是先去缓存中拿,缓存中如果有,就直接返回,缓存中如果没有,则调用 createNamedValueInfo 方法去创建,将创建结果缓存起来并返回。createNamedValueInfo 方法是一个模版方法,具体的实现在子类中。
  • 2. 接下来处理 Optional 类型参数。
  • 3. resolveEmbeddedValuesAndExpressions 方法是为了处理注解中使用了 SpEL 表达式的情况,例如如下接口:
@GetMapping("/hello2")
public void hello2(@RequestParam(value = "${aa.bb}") String name) {
    System.out.println("name = " + name);
}

参数名使用了表达式,那么 resolveEmbeddedValuesAndExpressions 方法的目的就是解析出表达式的值,如果没用到表达式,那么该方法会将原参数原封不动返回。

  • 4. 接下来调用 resolveName 方法解析出参数的具体值,这个方法也是一个模版方法,具体的实现在子类中。
  • 5. 如果获取到的参数值为 null,先去看注解中有没有默认值,然后再去看参数值是否是必须的,如果是,则抛异常出来,否则就设置为 null 即可。
  • 6. 如果解析出来的参数值为空字符串 "",则也去 resolveEmbeddedValuesAndExpressions 方法中走一遭。
  • 7. 最后则是 WebDataBinder 的处理,解决一些全局参数的问题,WebDataBinder 之前的文章中也有说过。

Spring—@ControllerAdvice等异常处理方式或统一处理数据

大致的流程就是这样。

在这个流程中,我们看到主要有如下两个方法是在子类中实现的:

  • createNamedValueInfo
  • resolveName

在加上 supportsParameter 方法,子类中一共有三个方法需要我们重点分析。

那么接下来我们就以 RequestParamMethodArgumentResolver 为例,来看下这三个方法。


RequestParamMethodArgumentResolver

supportsParameter方法

@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;
  }
 }
}
public static boolean isSimpleProperty(Class<?> type) {
 return isSimpleValueType(type) || (type.isArray() && isSimpleValueType(type.getComponentType()));
}
public static boolean isSimpleValueType(Class<?> type) {
 return (Void.class != type && void.class != type &&
   (ClassUtils.isPrimitiveOrWrapper(type) ||
   Enum.class.isAssignableFrom(type) ||
   CharSequence.class.isAssignableFrom(type) ||
   Number.class.isAssignableFrom(type) ||
   Date.class.isAssignableFrom(type) ||
   Temporal.class.isAssignableFrom(type) ||
   URI.class == type ||
   URL.class == type ||
   Locale.class == type ||
   Class.class == type));
}

从 supportsParameter 方法中可以非常方便的看出支持的参数类型:

  1. 首先参数如果有 @RequestParam 注解的话,则分两种情况:参数类型如果是 Map,则 @RequestParam 注解必须配置 name 属性,否则不支持;如果参数类型不是 Map,则直接返回 true,表示总是支持(想想自己平时使用的时候是不是这样)。
  2. 参数如果含有 @RequestPart 注解,则不支持。
  3. 检查下是不是文件上传请求,如果是,返回 true 表示支持。
  4. 如果前面都没能返回,则使用默认的解决方案,判断是不是简单类型,主要就是 Void、枚举、字符串、数字、日期等等。

这块代码其实很简单,支持谁不支持谁,一目了然。


createNamedValueInfo方法

@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 {
 public RequestParamNamedValueInfo() {
  super("", false, ValueConstants.DEFAULT_NONE);
 }
 public RequestParamNamedValueInfo(RequestParam annotation) {
  super(annotation.name(), annotation.required(), annotation.defaultValue());
 }
}

获取注解,读取注解中的属性,构造 RequestParamNamedValueInfo 对象返回。


resolveName技术

@Override
@Nullable
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
 HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
 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);
  }
 }
 if (arg == null) {
  String[] paramValues = request.getParameterValues(name);
  if (paramValues != null) {
   arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
  }
 }
 return arg;
}

这个方法思路也比较清晰:

  1. 前面两个 if 主要是为了处理文件上传请求。
  2. 如果不是文件上传请求,则调用 request.getParameterValues 方法取出参数返回即可。

如本文“对您有用”,欢迎随意打赏作者,让我们坚持创作!

1 打赏
Enamiĝu al vi
不要为明天忧虑.因为明天自有明天的忧虑.一天的难处一天当就够了。
543文章 68评论 294点赞 593816浏览

随机文章
PostgreSQL—测试工具PGbench
3年前
Java—并发编程(三)线程等待\唤醒\让步\休眠\join\守护……
4年前
Kotlin-类型进阶—密封类(二十六)
4年前
Kotlin-类型进阶—内部类(二十三)
4年前
Caffeine—缓存实战
2年前
博客统计
  • 日志总数:543 篇
  • 评论数目:68 条
  • 建站日期:2020-03-06
  • 运行天数:1927 天
  • 标签总数:23 个
  • 最后更新:2024-12-20
Copyright © 2025 网站备案号: 浙ICP备20017730号 身体没有灵魂是死的,信心没有行为也是死的。
主页
页面
  • 归档
  • 摘要
  • 杂图
  • 问题随笔
博主
Enamiĝu al vi
Enamiĝu al vi 管理员
To be, or not to be
543 文章 68 评论 593816 浏览
测试
测试
看板娘
赞赏作者

请通过微信、支付宝 APP 扫一扫

感谢您对作者的支持!

 支付宝 微信支付