详解如何在SpringBoot中自定义参数解析器
作者:江南一点雨 发布时间:2023-07-24 16:06:51
前言
在一个 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)&¶meter.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 松哥以后再和大家详细聊。
来源:https://mp.weixin.qq.com/s/mbstcEEtwOS9ZRSSjusbfA
猜你喜欢
- 在Android中,线程内部或者线程之间进行信息交互时经常会使用消息,这些基础的东西如果我们熟悉其内部的原理,将会使我们容易、更好地架构系统
- 有时候,我们在同一个activity里面有很多fragment,在横竖屏的时候,有些fragment要求重新加载数据,有些不需要,如何简单控
- 自从SEOTcs系统11月份24日更新了一下SEO得分算法以来,一直困扰我的一个问题出现了,java的数据job任务,在执行过程中会经常报以
- 目录简介历史null带来的种种问题方案场景引入方法说明构造函数创建Optional对象使用map从Optional对象中提取和转换值使用fl
- C#串口模块的使用。使用VS .net框架下WinForm程序应用开发。C#开发的串口通信小工具。相比于QT添加的串口类,WinForm是通
- 本文实例为大家分享了Android实现图片设置圆角形式的具体代码,供大家参考,具体内容如下1.自定义的图片圆角形式CircleImageVi
- 最近在做上传文件的服务,简单看了网上的教程。结合实践共享出代码。由于网上的大多数没有服务端的代码,这可不行呀,没服务端怎么调试呢。Ok,先上
- 一般而言,Android 应用在请求数据时都是以 Get 或 Post 等方式向远程服务器发起请求,那你有没有想过其实我们也可以在 Andr
- 这里使用的是dynamic-datasource-spring-boot-starter ,它是一个基于springboot的快速集成多数据
- 前言本文的记录如何用CustomPaint、GestureDetector实现一个进度条控件。首先需要说明的是 flutter Materi
- 自定义 webflux 容器配置配置代码@Componentpublic class ContainerConfig extends Rea
- 前言虽然从学java的第一个程序——helloworld至今,已经有好几个年头了。当时自己找资料,看视频,学习了java的输入输出流,多线程
- Android 微信摇一摇功能实现,最近学习传感器,就想实现摇一摇的功能,上网查了些资料,就整理下。如有错误,还请指正。开发环境Androi
- 解决方法有如下两种:第一种如果你 repo sync 了 android 的整个源码,那么可以直接把你的 app 放到 /packages/
- 一,FileWritter写入文件FileWritter, 字符流写入字符到文件。默认情况下,它会使用新的内容取代所有现有的内容,然而,当指
- 之前我们在使用vue进行 h5 表单录入的过程中,遇到了Android软键盘弹出,覆盖 h5页面 输入框 问题,在此进行回顾并分享给大家:系
- 起源flutter作为一个跨平台的框架,在绘制上体现出了它跨平台的良好性能.那么,它是如何从runApp()后 绘制上屏的呢?本文将与你一起
- 前言之前我们提到了 CustomPaint er 的 Paint 可以使用渐变(GradientShader)来填充绘制的图形,本篇我们来介
- 最近碰到个需求,是希望在Unity有一个按钮,打开后直接跳转淘宝app,打开商品页面。百度了下没有相关的文章,于是我在此分享下。之前开发游戏
- jdk8之前 一、java.lang.Systemlong times = System.currentTimeMillis();