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

目录
  • 前言
  • 1.自定义参数解析器
  • 2.PrincipalMethodArgumentResolver
  • 3.RequestParamMapMethodArgumentResolver
  • 4.小结

前言

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

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

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

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

String javaboy = 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);

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

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

如果你用的是 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 中提取出来的,到底是怎么提取出来的?这就是松哥今天要和大家分享的话题。

1.自定义参数解析器

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

自定义参数解析器需要实现 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 中取出参数的过程,方法的返回值就对应了接口中参数的值。

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

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

假设我现在系统安全框架使用了 Spring Security,如果我在接口的参数上添加了 @CurrentUserName 注解,那么该参数的值就是当前登录的用户名,像下面这样:

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

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

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

这个注解没啥好解释的。

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

public class CurrentUserNameHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterType().isAssignableFrom(String.class)&&parameter.hasParameterAnnotation(CurrentUserName.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return user.getUsername();
    }
}
  • supportsParameter:如果参数类型是 String,并且参数上有 @CurrentUserName 注解,则使用该参数解析器。
  • resolveArgument:该方法的返回值就是参数的具体值,当前登录用户名从 SecurityContextHolder 中获取即可

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

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

至此,就算配置完成了。

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

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

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

2.PrincipalMethodArgumentResolver

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

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

即直接在当前接口的参数中添加 Principal 类型的参数即可,该参数描述了当前登录用户信息,这个用过 Spring Security 的小伙伴应该都知道

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

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

public class PrincipalMethodArgumentResolver implements HandlerMethodArgumentResolver {

 @Override
 public boolean supportsParameter(MethodParameter parameter) {
  return Principal.class.isAssignableFrom(parameter.getParameterType());
 }

 @Override
 public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
   NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

  HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
  if (request == null) {
   throw new IllegalStateException("Current request is not of type HttpServletRequest: " + webRequest);
  }

  Principal principal = request.getUserPrincipal();
  if (principal != null && !parameter.getParameterType().isInstance(principal)) {
   throw new IllegalStateException("Current user principal is not of type [" +
     parameter.getParameterType().getName() + "]: " + principal);
  }

  return principal;
 }

}
  • supportsParameter:这个方法主要是判断参数类型是不是 Principal,如果参数类型是 Principal,就支持。
  • resolveArgument:这个方法的逻辑很简单,首先获取原生的请求,再从请求中获取 Principal 对象返回即可。

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

3.RequestParamMapMethodArgumentResolver

松哥再给大家举个例子:

@RestController
public class HelloController {
    @PostMapping("/hello")
    public void hello(@RequestParam MultiValueMap map) throws IOException {
        //省略...
    }
}

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

public class RequestParamMapMethodArgumentResolver implements HandlerMethodArgumentResolver {

 @Override
 public boolean supportsParameter(MethodParameter parameter) {
  RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
  return (requestParam != null && Map.class.isAssignableFrom(parameter.getParameterType()) &&
    !StringUtils.hasText(requestParam.name()));
 }

 @Override
 public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
   NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

  ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);

  if (MultiValueMap.class.isAssignableFrom(parameter.getParameterType())) {
   // MultiValueMap
   Class<?> valueType = resolvableType.as(MultiValueMap.class).getGeneric(1).resolve();
   if (valueType == MultipartFile.class) {
    MultipartRequest multipartRequest = MultipartResolutionDelegate.resolveMultipartRequest(webRequest);
    return (multipartRequest != null ? multipartRequest.getMultiFileMap() : new LinkedMultiValueMap<>(0));
   }
   else if (valueType == Part.class) {
    HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
    if (servletRequest != null && MultipartResolutionDelegate.isMultipartRequest(servletRequest)) {
     Collection<Part> parts = servletRequest.getParts();
     LinkedMultiValueMap<String, Part> result = new LinkedMultiValueMap<>(parts.size());
     for (Part part : parts) {
      result.add(part.getName(), part);
     }
     return result;
    }
    return new LinkedMultiValueMap<>(0);
   }
   else {
    Map<String, String[]> parameterMap = webRequest.getParameterMap();
    MultiValueMap<String, String> result = new LinkedMultiValueMap<>(parameterMap.size());
    parameterMap.forEach((key, values) -> {
     for (String value : values) {
      result.add(key, value);
     }
    });
    return result;
   }
  }

  else {
   // Regular Map
   Class<?> valueType = resolvableType.asMap().getGeneric(1).resolve();
   if (valueType == MultipartFile.class) {
    MultipartRequest multipartRequest = MultipartResolutionDelegate.resolveMultipartRequest(webRequest);
    return (multipartRequest != null ? multipartRequest.getFileMap() : new LinkedHashMap<>(0));
   }
   else if (valueType == Part.class) {
    HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
    if (servletRequest != null && MultipartResolutionDelegate.isMultipartRequest(servletRequest)) {
     Collection<Part> parts = servletRequest.getParts();
     LinkedHashMap<String, Part> result = CollectionUtils.newLinkedHashMap(parts.size());
     for (Part part : parts) {
      if (!result.containsKey(part.getName())) {
       result.put(part.getName(), part);
      }
     }
     return result;
    }
    return new LinkedHashMap<>(0);
   }
   else {
    Map<String, String[]> parameterMap = webRequest.getParameterMap();
    Map<String, String> result = CollectionUtils.newLinkedHashMap(parameterMap.size());
    parameterMap.forEach((key, values) -> {
     if (values.length > 0) {
      result.put(key, values[0]);
     }
    });
    return result;
   }
  }
 }

}
  • supportsParameter:参数类型是 Map,并且使用了 @RequestParam 注解,并且 @RequestParam 注解中没有配置 name 属性,就可以使用该参数解析器。
  • resolveArgument:具体解析分为两种情况:MultiValueMap 和其他 Map,前者中又分三种情况:MultipartFile、Part 或者其他普通请求,前两者可以处理文件上传,第三个就是普通参数。如果是普通 Map,则直接获取到原始请求参数放到一个 Map 集合中返回即可。

4.小结

前面和大家聊的都是几种简单的情况,还有复杂的如 PathVariableMethodArgumentResolver 和 RequestParamMethodArgumentResolver 松哥以后再和大家详细聊。

以上就是详解如何在SpringBoot中自定义参数解析器的详细内容,更多关于SpringBoot自定义参数解析器的资料请关注我们其它相关文章!

(0)

相关推荐

  • SpringBoot如何解析参数的深入理解

    前言 前几天笔者在写Rest接口的时候,看到了一种传值方式是以前没有写过的,就萌生了一探究竟的想法.在此之前,有篇文章曾涉及到这个话题,但那篇文章着重于处理流程的分析,并未深入. 本文重点来看几种传参方式,看看它们都是如何被解析并应用到方法参数上的. 一.HTTP请求处理流程 不论在SpringBoot还是SpringMVC中,一个HTTP请求会被DispatcherServlet类接收,它本质是一个Servlet,因为它继承自HttpServlet.在这里,Spring负责解析请求,匹配到Co

  • SpringBoot项目实用功能之实现自定义参数解析器

    核心点 1.实现接口 org.springframework.web.method.support.HandlerMethodArgumentResolver supportsParameter 方法根据当前方法参数决定是否需要应用当前这个参数解析器 resolveArgument 执行具体的解析过程 2.将自实现的参数解析器类 添加到Spring容器中 3.实现 org.springframework.web.servlet.config.annotation.WebMvcConfigurer

  • SpringBoot接口接收json参数解析

    目录 SpringBoot接口接收json参数 前言 前提 一.GET 二.DELETE 三.POST/PUT/PATCH Springboot restFul 参数检验 概述 常用注解 简单应用举例 自定义校验 抛出BindException而非MethodArgumentNotValidException SpringBoot接口接收json参数 前言 通常来讲,HTTP 方法会映射为 CRUD 动作,但这并不是严格的限制,有时候 PUT 也可以用来创建新的资源,POST 也可以用来更新资源

  • SpringBoot与SpringMVC中参数传递的原理解析

    目录 一:普通参数与基本注解 二:复杂参数 一:普通参数与基本注解 HandlerMapping中找到能处理请求的Handler(Controller,method()) 为当前Handler找一个适配器HandlerAdapter:RequestMappingHandlerAdapter 1.HandlerAdapter 0-支持方法上标注@RequestMapping 1-支持函数式编程的 xxxx 2.执行目标方法 3.参数解析器:确定要执行的目标方法每一个参数的值是什么 boolean

  • SpringBoot定时任务参数运行代码实例解析

    @Scheduled注解各参数详解  cron 该参数接收一个cron表达式,cron表达式是一个字符串,字符串以5或6个空格隔开,分开共6或7个域,每一个域代表一个含义. cron表达式语法 [秒] [分] [小时] [日] [月] [周] [年] 注:[年]不是必须的域,可以省略[年],则一共6个域 序号 说明 必填 允许填写的值 允许的通配符 1 秒 是 0-59 , - * / 2 分 是 0-59 , - * / 3 时 是 0-23 , - * / 4 日 是 1-31 , - *

  • 详解如何在SpringBoot中自定义参数解析器

    目录 前言 1.自定义参数解析器 2.PrincipalMethodArgumentResolver 3.RequestParamMapMethodArgumentResolver 4.小结 前言 在一个 Web 请求中,参数我们无非就是放在地址栏或者请求体中,个别请求可能放在请求头中. 放在地址栏中,我们可以通过如下方式获取参数: String javaboy = request.getParameter("name "); 放在请求体中,如果是 key/value 形式,我们可以通

  • 详解如何在SpringBoot项目中使用全局异常处理

    目录 1. 创建自定义异常 2.创建全局异常处理器 3.创建测试控制器 在完整的项目开发中,异常的出现几乎是无法避免的:如果凡是有可能出现异常的地方,我们都手动的使用try-catch将其捕获的话,虽然也能达到处理异常的效果,但是这样做会使得代码显得十分臃肿并且后期不好维护,也不利于多人系统开发. 在Spring Boot中提供了统一处理异常的方法,SpringBoot中有一个ControllerAdvice的注解,使用该注解表示开启了全局异常的捕获,我们只需在自定义一个方法使用Exceptio

  • 详解如何在SpringBoot项目中使用统一返回结果

    目录 1.创建Spring Boot项目 2.返回结果的封装 3.后端接口实现 3.1 创建实体类 3.2 创建dao层 3.3 创建Controller层 4.前端部分 5.验证 在一个完整的项目中,如果每一个控制器的方法都返回不同的结果,那么对项目的维护和扩展都会很麻烦:并且现在主流的开发模式时前后端分离的模式,如果后端返回各式各样的结果,那么在前后端联调时会非常的麻烦,还会增加前后端的格外任务. 所以,在一个项目中统一返回结果就是一个十分必要和友好的做法.接下来就用一个简单的demo来看看

  • 详解如何在Javascript中使用Object.freeze()

    Object.freeze() Object.freeze() 方法可以冻结一个对象.一个被冻结的对象再也不能被修改:冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性.可配置性.可写性,以及不能修改已有属性的值.此外,冻结一个对象后该对象的原型也不能被修改.freeze() 返回和传入的参数相同的对象 用法 const objectExample = { prop1: 20, prop2: "羊先生" }; // 默认情况下,我们可以根据需

  • 详解json在SpringBoot中的格式转换

    @RestController自动返回json /** * json 三种实现方法 * 1 @RestController自动返回json */ @GetMapping("/json") public Student getjson() { Student student = new Student("bennyrhys",158 ); return student; } @ResponseBody+@Controller 组合返回json //@RestContr

  • 详解如何在Flutter中集成华为认证服务

    最近发现华为AGC认证服务支持Flutter框架了,期待这个平台的支持已经很久了,所以迫不及待接入了,关联了自己的邮箱等账号. 集成步骤 安装flutter环境 a) 下载Flutter sdk包,地址:https://flutter.dev/docs/get-started/install/windows 将压缩包解压到任意文件夹,例如D:\Flutter b) 将flutter命令文件添加到环境变量中,此处我添加的Path为D:\Flutter\flutter_windows_1.22.2-

  • 详解如何在Java中调用Python程序

    Java中调用Python程序 1.新建一个Maven工程,导入如下依赖 <dependency> <groupId>org.python</groupId> <artifactId>jython-standalone</artifactId> <version>2.7.0</version> </dependency> 2.在java中直接执行python代码片段 import org.python.util

  • 详解如何在Vue中动态添加类名

    目录 静态和动态类 有条件的类名 使用数组语法 使用对象语法 与自定义组件一起使用 快速生成类名 使用计算属性来简化类 能够向组件添加动态类名是非常强大的功能.它使我们可以更轻松地编写自定义主题,根据组件的状态添加类,还可以编写依赖于样式的组件的不同变体. 添加动态类名与在组件中添加 prop :class="classname"一样简单.无论classname的计算结果是什么,都将是添加到组件中的类名. 当然,对于Vue中的动态类,我们可以做的还有很多.在本文中,我们将讨论很多内容:

  • 详解如何在JavaScript中使用装饰器

    目录 安装 vite配置 webpack配置 使用 语法: @+函数名 类装饰器 带参数的修饰器 类成员装饰器 多个装饰器的执行顺序 应用 延迟 节流 防抖 Decorator装饰器是ES7的时候提案的特性,目前处于Stage 3候选阶段(2022年10月). 装饰器简单来说就是修改类和类方法的语法糖,很多面向对象语言都有装饰器这一特性. 为了使用装饰器特性,我们需要用进行babel转义.这里需要用到的是@babel/plugin-proposal-decorators. 安装 npm inst

  • 详解如何在pyqt中通过OpenCV实现对窗口的透视变换

    窗口的透视变换效果    当我们点击Win10的UWP应用中的小部件时,会发现小部件会朝着鼠标点击位置凹陷下去,而且不同的点击位置对应着不同的凹陷情况,看起来就好像小部件在屏幕上不只有x轴和y轴,甚至还有一个z轴.要做到这一点,其实只要对窗口进行透视变换即可.下面是对Qt的窗口和按钮进行透视变换的效果: 具体代码    1.下面先定义一个类,它的作用是将传入的 QPixmap 转换为numpy 数组,然后用 opencv 的 warpPerspective 对数组进行透视变换,最后再将 nump

随机推荐