SpringBoot 中如何自定义参数解析器?

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

[[387869]]

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

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

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

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

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

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

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

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

  1. String javaboy = request.getHeader("name"); 

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

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

那有小伙伴要问了,SpringMVC 到底是怎么样从 request 中把参数提取出来直接给我们用的呢?例如下面这个接口:

  1. @RestController 
  2. public class HelloController { 
  3.     @GetMapping("/hello"
  4.     public String hello(String name) { 
  5.         return "hello "+name
  6.     } 

我们都知道 name 参数是从 HttpServletRequest 中提取出来的,到底是怎么提取出来的?这就是松哥今天要和大家分享的话题。

1.自定义参数解析器

为了搞清楚这个问题,我们先来自定义一个参数解析器看看。

自定义参数解析器需要实现 HandlerMethodArgumentResolver 接口,我们先来看看该接口:

  1. public interface HandlerMethodArgumentResolver { 
  2.  boolean supportsParameter(MethodParameter parameter); 
  3.  @Nullable 
  4.  Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, 
  5.    NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception; 
  6.  

这个接口中就两个方法:

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

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

假设我现在有这样一个需求(实际上在 Spring Security 中获取当前登录用户名非常方便,这里只是为了该案例而做,勿抬杠):

假设我现在系统安全框架使用了 Spring Security(对 Spring Security 不熟悉的小伙伴,可以在公众号江南一点雨后台回复 ss,有教程),如果我在接口的参数上添加了 @CurrentUserName 注解,那么该参数的值就是当前登录的用户名,像下面这样:

  1. @RestController 
  2. public class HelloController { 
  3.     @GetMapping("/hello"
  4.     public String hello(@CurrentUserName String name) { 
  5.         return "hello "+name
  6.     } 

要实现这个功能,非常 easy,首先我们自定义一个 @CurrentUserName 注解,如下:

  1. @Retention(RetentionPolicy.RUNTIME) 
  2. @Target(ElementType.PARAMETER) 
  3. public @interface CurrentUserName { 

这个注解没啥好解释的。

接下来我们自定义参数解析器 CurrentUserNameHandlerMethodArgumentResolver,如下:

  1. public class CurrentUserNameHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { 
  2.     @Override 
  3.     public boolean supportsParameter(MethodParameter parameter) { 
  4.         return parameter.getParameterType().isAssignableFrom(String.class)&&parameter.hasParameterAnnotation(CurrentUserName.class); 
  5.     } 
  6.  
  7.     @Override 
  8.     public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { 
  9.         User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 
  10.         return user.getUsername(); 
  11.     } 

supportsParameter:如果参数类型是 String,并且参数上有 @CurrentUserName 注解,则使用该参数解析器。

resolveArgument:该方法的返回值就是参数的具体值,当前登录用户名从 SecurityContextHolder 中获取即可(具体参数松哥的 Spring Security 教程,公号后台回复 ss)。

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

  1. @Configuration 
  2. public class WebConfig implements WebMvcConfigurer { 
  3.     @Override 
  4.     public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { 
  5.         resolvers.add(new CurrentUserNameHandlerMethodArgumentResolver()); 
  6.     } 

至此,就算配置完成了。

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

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

在 SpringMVC 中,默认也有很多 HandlerMethodArgumentResolver 的实现类,他们处理的问题也都类似,松哥再给大家举个例子。

2.PrincipalMethodArgumentResolver

如果我们在项目中使用了 Spring Security,我们可以通过如下方式获取当前登录用户信息:

  1. @GetMapping("/hello2"
  2. public String hello2(Principal principal) { 
  3.     return "hello " + principal.getName(); 

即直接在当前接口的参数中添加 Principal 类型的参数即可,该参数描述了当前登录用户信息,这个用过 Spring Security 的小伙伴应该都知道(不熟悉 Spring Security 的小伙伴可以在公众号【江南一点雨】后台回复 ss)。

那么这个功能是怎么实现的呢?当然就是 PrincipalMethodArgumentResolver 在起作用了!

我们一起来看下这个参数解析器:

  1. public class PrincipalMethodArgumentResolver implements HandlerMethodArgumentResolver { 
  2.  
  3.  @Override 
  4.  public boolean supportsParameter(MethodParameter parameter) { 
  5.   return Principal.class.isAssignableFrom(parameter.getParameterType()); 
  6.  } 
  7.  
  8.  @Override 
  9.  public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, 
  10.    NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { 
  11.  
  12.   HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); 
  13.   if (request == null) { 
  14.    throw new IllegalStateException("Current request is not of type HttpServletRequest: " + webRequest); 
  15.   } 
  16.  
  17.   Principal principal = request.getUserPrincipal(); 
  18.   if (principal != null && !parameter.getParameterType().isInstance(principal)) { 
  19.    throw new IllegalStateException("Current user principal is not of type [" + 
  20.      parameter.getParameterType().getName() + "]: " + principal); 
  21.   } 
  22.  
  23.   return principal; 
  24.  } 
  25.  

supportsParameter:这个方法主要是判断参数类型是不是 Principal,如果参数类型是 Principal,就支持。

resolveArgument:这个方法的逻辑很简单,首先获取原生的请求,再从请求中获取 Principal 对象返回即可。

是不是很简单,有了这个,我们就可以随时加载到当前登录用户信息了。

3.RequestParamMapMethodArgumentResolver

松哥再给大家举个例子:

  1. @RestController 
  2. public class HelloController { 
  3.     @PostMapping("/hello"
  4.     public void hello(@RequestParam MultiValueMap map) throws IOException { 
  5.         //省略... 
  6.     } 

这个接口很多小伙伴可能都写过,使用 Map 去接收前端传来的参数,那么这里用到的参数解析器就是 RequestParamMapMethodArgumentResolver。

  1. public class RequestParamMapMethodArgumentResolver implements HandlerMethodArgumentResolver { 
  2.  
  3.  @Override 
  4.  public boolean supportsParameter(MethodParameter parameter) { 
  5.   RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class); 
  6.   return (requestParam != null && Map.class.isAssignableFrom(parameter.getParameterType()) && 
  7.     !StringUtils.hasText(requestParam.name())); 
  8.  } 
  9.  
  10.  @Override 
  11.  public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, 
  12.    NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { 
  13.  
  14.   ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter); 
  15.  
  16.   if (MultiValueMap.class.isAssignableFrom(parameter.getParameterType())) { 
  17.    // MultiValueMap 
  18.    Class<?> valueType = resolvableType.as(MultiValueMap.class).getGeneric(1).resolve(); 
  19.    if (valueType == MultipartFile.class) { 
  20.     MultipartRequest multipartRequest = MultipartResolutionDelegate.resolveMultipartRequest(webRequest); 
  21.     return (multipartRequest != null ? multipartRequest.getMultiFileMap() : new LinkedMultiValueMap<>(0)); 
  22.    } 
  23.    else if (valueType == Part.class) { 
  24.     HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); 
  25.     if (servletRequest != null && MultipartResolutionDelegate.isMultipartRequest(servletRequest)) { 
  26.      Collection<Part> parts = servletRequest.getParts(); 
  27.      LinkedMultiValueMap<String, Part> result = new LinkedMultiValueMap<>(parts.size()); 
  28.      for (Part part : parts) { 
  29.       result.add(part.getName(), part); 
  30.      } 
  31.      return result; 
  32.     } 
  33.     return new LinkedMultiValueMap<>(0); 
  34.    } 
  35.    else { 
  36.     Map<String, String[]> parameterMap = webRequest.getParameterMap(); 
  37.     MultiValueMap<String, String> result = new LinkedMultiValueMap<>(parameterMap.size()); 
  38.     parameterMap.forEach((keyvalues) -> { 
  39.      for (String value : values) { 
  40.       result.add(key, value); 
  41.      } 
  42.     }); 
  43.     return result; 
  44.    } 
  45.   } 
  46.  
  47.   else { 
  48.    // Regular Map 
  49.    Class<?> valueType = resolvableType.asMap().getGeneric(1).resolve(); 
  50.    if (valueType == MultipartFile.class) { 
  51.     MultipartRequest multipartRequest = MultipartResolutionDelegate.resolveMultipartRequest(webRequest); 
  52.     return (multipartRequest != null ? multipartRequest.getFileMap() : new LinkedHashMap<>(0)); 
  53.    } 
  54.    else if (valueType == Part.class) { 
  55.     HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); 
  56.     if (servletRequest != null && MultipartResolutionDelegate.isMultipartRequest(servletRequest)) { 
  57.      Collection<Part> parts = servletRequest.getParts(); 
  58.      LinkedHashMap<String, Part> result = CollectionUtils.newLinkedHashMap(parts.size()); 
  59.      for (Part part : parts) { 
  60.       if (!result.containsKey(part.getName())) { 
  61.        result.put(part.getName(), part); 
  62.       } 
  63.      } 
  64.      return result; 
  65.     } 
  66.     return new LinkedHashMap<>(0); 
  67.    } 
  68.    else { 
  69.     Map<String, String[]> parameterMap = webRequest.getParameterMap(); 
  70.     Map<String, String> result = CollectionUtils.newLinkedHashMap(parameterMap.size()); 
  71.     parameterMap.forEach((keyvalues) -> { 
  72.      if (values.length > 0) { 
  73.       result.put(keyvalues[0]); 
  74.      } 
  75.     }); 
  76.     return result; 
  77.    } 
  78.   } 
  79.  } 
  80.  

supportsParameter:参数类型是 Map,并且使用了 @RequestParam 注解,并且 @RequestParam 注解中没有配置 name 属性,就可以使用该参数解析器。

resolveArgument:具体解析分为两种情况:MultiValueMap 和其他 Map,前者中又分三种情况:MultipartFile、Part 或者其他普通请求,前两者可以处理文件上传,第三个就是普通参数。如果是普通 Map,则直接获取到原始请求参数放到一个 Map 集合中返回即可。

4.小结

前面和大家聊的都是几种简单的情况,还有复杂的如 PathVariableMethodArgumentResolver 和 RequestParamMethodArgumentResolver 松哥以后再和大家详细聊。同时还有一个问题就是这些参数解析器具体是在哪里调用的,这个也会在松哥近期的 SpringMVC 源码解析系列文章中和大家分享,好啦,今天周末,就这点简单的小知识祝大家周末愉快~

本文转载自微信公众号「江南一点雨」,可以通过以下二维码关注。转载本文请联系江南一点雨公众号。

 

责任编辑:武晓燕 来源: 江南一点雨
相关推荐

2022-07-11 10:37:41

MapPart集合

2022-05-11 10:45:21

SpringMVC框架Map

2024-02-22 08:06:45

JSON策略解析器

2023-11-01 08:01:04

SpringWeb容器

2022-01-06 06:23:49

Swagger参数解析器

2013-01-14 11:40:50

IBMdW

2021-03-18 10:56:59

SpringMVC参数解析器

2022-09-23 07:33:04

Springzookeeper配置

2023-10-11 07:57:23

springboot微服务

2011-08-09 17:16:56

CoreAnimati动画

2021-11-23 15:06:42

Kubernetes 运维开源

2013-06-27 11:10:01

iOS开发自定义UISlider

2023-06-06 08:01:18

自定义接口响应

2009-09-13 18:58:07

自定义LINQ提供器

2010-07-22 09:25:21

Symbian开发

2023-07-03 08:29:11

BannerSpringBoot

2014-12-10 10:37:45

Android自定义布局

2023-12-29 08:01:52

自定义指标模板

2019-12-02 21:29:45

Keras神经网络TensorFlow

2021-07-01 11:07:49

Swift 自定义操作符
点赞
收藏

51CTO技术栈公众号