前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SpringBoot自定义参数解析器

SpringBoot自定义参数解析器

作者头像
啃饼思录
发布2023-03-18 16:30:26
1.6K0
发布2023-03-18 16:30:26
举报

写在前面

今天我们来聊一聊SpringBoot中的参数解析器,这在某些场景下非常有用。一般来说,在一个Web请求里面参数要么是放在请求地址,要么就是放在请求体里面,极个别的会放在请求头中。

如果请求参数放在请求地址中,那么通常会采用@RequestParam/@PathVariable或者如下方式来获取参数:

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

如果请求参数放在请求体里面,那么通常会采用@RequestBody或者如下方式来获取参数:

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

如果请求参数放在请求头里面,那么通常会采用@RequestHeader或者如下方式来获取参数:

String username = request.getHeader("username");

如果参数是JSON形式的,那么会从输入流中获取并解析成JSON字符串,再通过JSON工具转化为POJO对象:

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

无论参数是key-value键值对,还是JSON形式数据,以上几种方式基本上涵盖了日常开发的所有需求。

现在我们以下面的接口为例,来深度分析SpringMVC如何从请求中获取参数:

@RestController
public class UserController {
    @GetMapping("/user")
    public String user(String username){
        return "I am" + username;
    }
}

毫无疑问这个username参数肯定是从HttpServletRequest中获取的,那么它是如何获取的呢?

方法参数解析器

HandlerMethodArgumentResolver接口

我们知道在SpringBoot中与Web相关的配置信息都在WebMvcConfigurer接口中,可以看到该接口中有一个名为addArgumentResolvers的默认方法,用于添加参数解析器:

default void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
}

所以可以看到HandlerMethodArgumentResolver这个其实就是具体的一些参数解析器,实际上它是一个接口。我们来看HandlerMethodArgumentResolver这一接口,该接口用于解析方法中的参数:

public interface HandlerMethodArgumentResolver {
    boolean supportsParameter(MethodParameter parameter);

    @Nullable
    Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}

可以看到这个HandlerMethodArgumentResolver接口中就两个方法,其中supportsParameter()方法表示是否启用该参数解析器,true表示启用,false表示不启用;resolveArgument()方法是具体的解析过程,即从HttpServletRequest中取出参数的过程,该方法的返回值就是接口中参数的值。所以如果开发者想自定义参数解析器,只需实现该接口并重写其中的两个方法。

由于SpringBoot采用约定大于配置这一规则,因此建议HandlerMethodArgumentResolver接口的实现类命名规则为“解析器对应的注解名称”+“MethodArgumentResolver”。

将下来我们分析几个常用的HandlerMethodArgumentResolver接口实现类,了解它们对于提升技能有非常大的帮助。

RequestParamMethodArgumentResolver类

通过前面的分析,可以得到这个解析器所对应的注解名称为RequestParam,因此首先我们看一下这个注解的源码:

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";

    boolean required() default true;

    String defaultValue() default "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n";
}

可以看到这个@RequestParam注解有四个属性,其中name和value互为各自的别名,required表示参数是否必须传,默认是必须;defaultValue则是参数的默认值。

接着查看一下这个RequestParamMethodArgumentResolver类中的supportsParameterresolveName方法,注意它的resolveArgument方法名称发生了变化:

    public boolean supportsParameter(MethodParameter parameter) {
        if (parameter.hasParameterAnnotation(RequestParam.class)) {
            if (!Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
                return true;
            } else {
                RequestParam requestParam = (RequestParam)parameter.getParameterAnnotation(RequestParam.class);
                return requestParam != null && StringUtils.hasText(requestParam.name());
            }
        } else if (parameter.hasParameterAnnotation(RequestPart.class)) {
            return false;
        } else {
            parameter = parameter.nestedIfOptional();
            if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
                return true;
            } else {
                return this.useDefaultResolution ? BeanUtils.isSimpleProperty(parameter.getNestedParameterType()) : false;
            }
        }
    }

    @Nullable
    protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
        HttpServletRequest servletRequest = (HttpServletRequest)request.getNativeRequest(HttpServletRequest.class);
        Object arg;
        if (servletRequest != null) {
            arg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
            if (arg != MultipartResolutionDelegate.UNRESOLVABLE) {
                return arg;
            }
        }

        arg = null;
        MultipartRequest 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)supportsParameter()方法,如果使用了@RequestParam注解且不是Map类型,或者是Map类型同时传入了name属性,又或者是没有使用@RequestParam@RequestPart注解且这个参数有多个组成,或者使用默认的解析器且参数的嵌套是简单类型,则使用该参数解析器;(2)resolveName()方法,首先获取HttpServletRequest对象,如果该对象存在,则解析请求中的多个参数并返回这些参数的值;如果该对象不存在,但是MultipartRequest存在,那么从这个MultipartRequest中通过参数的名称来得到这些参数值,如果参数值存在,那么返回参数值的信息。如果参数值不存在,那么从请求中根据参数名称来得到参数值,如果参数值存在,那么返回参数值的信息。

类似于这种的情况,我们会使用诸如下面的接口:

@RestController
public class UserController {
    @GetMapping("/user")
    public String user(@RequestParam("username") String username){
        return "I am " + username;
    }
}

RequestParamMapMethodArgumentResolver类

这个方法参数解析器所对应的注解名称同样也是RequestParam,接着查看一下这个RequestParamMapMethodArgumentResolver类中的supportsParameterresolveArgument方法:

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

    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
        ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
        Class valueType;
        HttpServletRequest servletRequest;
        Collection parts;
        Iterator var10;
        Part part;
        Map parameterMap;
        MultipartRequest multipartRequest;
        if (!MultiValueMap.class.isAssignableFrom(parameter.getParameterType())) {
            valueType = resolvableType.asMap().getGeneric(new int[]{1}).resolve();
            if (valueType == MultipartFile.class) {
                multipartRequest = MultipartResolutionDelegate.resolveMultipartRequest(webRequest);
                return multipartRequest != null ? multipartRequest.getFileMap() : new LinkedHashMap(0);
            } else if (valueType == Part.class) {
                servletRequest = (HttpServletRequest)webRequest.getNativeRequest(HttpServletRequest.class);
                if (servletRequest != null && MultipartResolutionDelegate.isMultipartRequest(servletRequest)) {
                    parts = servletRequest.getParts();
                    LinkedHashMap<String, Part> result = CollectionUtils.newLinkedHashMap(parts.size());
                    var10 = parts.iterator();

                    while(var10.hasNext()) {
                        part = (Part)var10.next();
                        if (!result.containsKey(part.getName())) {
                            result.put(part.getName(), part);
                        }
                    }

                    return result;
                } else {
                    return new LinkedHashMap(0);
                }
            } else {
                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;
            }
        } else {
            valueType = resolvableType.as(MultiValueMap.class).getGeneric(new int[]{1}).resolve();
            if (valueType == MultipartFile.class) {
                multipartRequest = MultipartResolutionDelegate.resolveMultipartRequest(webRequest);
                return multipartRequest != null ? multipartRequest.getMultiFileMap() : new LinkedMultiValueMap(0);
            } else if (valueType != Part.class) {
                parameterMap = webRequest.getParameterMap();
                MultiValueMap<String, String> result = new LinkedMultiValueMap(parameterMap.size());
                parameterMap.forEach((key, values) -> {
                    String[] var3 = values;
                    int var4 = values.length;

                    for(int var5 = 0; var5 < var4; ++var5) {
                        String value = var3[var5];
                        result.add(key, value);
                    }

                });
                return result;
            } else {
                servletRequest = (HttpServletRequest)webRequest.getNativeRequest(HttpServletRequest.class);
                if (servletRequest != null && MultipartResolutionDelegate.isMultipartRequest(servletRequest)) {
                    parts = servletRequest.getParts();
                    LinkedMultiValueMap<String, Part> result = new LinkedMultiValueMap(parts.size());
                    var10 = parts.iterator();

                    while(var10.hasNext()) {
                        part = (Part)var10.next();
                        result.add(part.getName(), part);
                    }

                    return result;
                } else {
                    return new LinkedMultiValueMap(0);
                }
            }
        }
    }

简单解释一下上述方法的逻辑:(1)supportsParameter()方法,如果使用了@RequestParam注解且参数是Map类型,同时@RequestParam注解中没有设置name属性,那么就可以使用该参数解析器;(2)resolveArgument()方法,总的来说分为两种情况,一种是MultiValueMap,另一种则是其他的Map。对于MultiValueMap来说,如果它是MultipartFile或者Part类型,那么就可以处理文件上传;如果是其他的则是普通的请求参数。如果是普通的Map,那么就直接从原始请求中获取请求参数,并将这些参数放到一个LinkedMultiValueMap中并返回。

类似于这种的情况,我们会使用诸如下面的接口:

@RestController
public class UserController {
    @PostMapping("/user")
    public void user(@RequestParam MultiValueMap map){
        System.out.println(map.get("username"));
    }
}

PrincipalMethodArgumentResolver类

查看一下PrincipalMethodArgumentResolver类的源码,如下所示:

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

    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
        HttpServletRequest request = (HttpServletRequest)webRequest.getNativeRequest(HttpServletRequest.class);
        if (request == null) {
            throw new IllegalStateException("Current request is not of type HttpServletRequest: " + webRequest);
        } else {
            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);
            } else {
                return principal;
            }
        }
    }

简单解释一下上述方法的逻辑:(1)supportsParameter()方法,用于判断参数类型是否为Principal这一类型,如果是则使用该参数解析器;(2)resolveArgument()方法,首先从原始请求中获取HttpServletRequest对象,如果该对象不存在则抛出异常;如果存在则从请求中获取Principal对象并返回。这个Principal对象里面包含登录的用户名称。

类似于这种的情况,我们会使用诸如下面的接口:

@RestController
public class UserController {
    @GetMapping("/user")
    public String user(Principal principal){
        return "My name is " + principal.getName();
    }
}

PathVariableMethodArgumentResolver类

通过前面的分析,可以得到这个解析器所对应的注解名称为PathVariable,因此首先我们看一下这个注解的源码:

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PathVariable {
    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";

    boolean required() default true;
}

可以看到这个@PathVariable注解有三个属性,其中name和value互为各自的别名,required表示参数是否必须传,默认是必须。

接着查看一下这个PathVariableMethodArgumentResolver类中的supportsParameterresolveName方法,注意它的resolveArgument方法名称发生了变化:

    public boolean supportsParameter(MethodParameter parameter) {
        if (!parameter.hasParameterAnnotation(PathVariable.class)) {
            return false;
        } else if (!Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
            return true;
        } else {
            PathVariable pathVariable = (PathVariable)parameter.getParameterAnnotation(PathVariable.class);
            return pathVariable != null && StringUtils.hasText(pathVariable.value());
        }
    }

    @Nullable
    protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
        Map<String, String> uriTemplateVars = (Map)request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, 0);
        return uriTemplateVars != null ? uriTemplateVars.get(name) : null;
    }

简单解释一下上述方法的逻辑:(1)supportsParameter()方法,用于判断如果参数上使用了@PathVariable注解,并且参数的类型不是Map及其子类,则使用该参数解析器。或者是Map类型,则当@PathVariable注解中的value属性有值时,才使用该参数解析器;(2)resolveName()方法,用于从请求中获取uriTemplateVars,如果uriTemplateVars不为空,则从uriTemplateVars根据名称来获取值并返回。

类似于这种的情况,我们会使用诸如下面的接口:

@RestController
public class UserController {
    @GetMapping("/user/{username}")
    public String user(@PathVariable("username") String username){
        return "My name is " + username;
    }
}

实战

假设如下接口中,我们需要获取用户传入的用户名,此时就可以使用自定义参数解析器这一方式:

@RestController
public class UserController {
    @GetMapping("/user")
    public String user(@CurrentUserName String username){
        return "I am" + username;
    }
}

即在方法中通过使用@CurrentUserName注解从HttpServletRequest中获取当前传入的用户名。要实现这个功能,步骤如下所示:

第一步,创建一个名为method-resolve的SpringBoot项目,然后添加spring webspring test依赖:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

第二步,新建一个名为CurrentUserName的注解:

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

第三步,新建一个名为CurrentUserNameHandlerMethodArgumentResolver的类,注意它需要实现HandlerMethodArgumentResolver接口并重写其中的上述两个方法:

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 {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        return request.getParameter(parameter.getParameterName());
    }
}

简单解释一下上述方法的逻辑:(1)supportsParameter()方法,判断当前参数类型是否为String且在参数上使用了@CurrentUserName注解,这有这样才使用该参数解析器;(2)resolveArgument()方法,用于返回接口中参数的值,这里直接调用request.getParameter()方法传入参数名称进而得到参数的值。

第四步,注册自定义参数解析器。定义一个名为WebConfig的类,注意这个类需要实现WebMvcConfigurer接口,并重写其中的addArgumentResolvers默认方法:

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

第五步,新建接口类。定义一个名为UserController的类,里面的代码如下所示:

@RestController
public class UserController {
    @GetMapping("/user")
    public String user(@CurrentUserName String username){
        return "I am " + username;
    }
}

第六步,启动项目,访问http://localhost:8080/user?username=melody链接,可以看到页面显示如下信息:

I am melody

小结

本文介绍了如何在SpringBoot中通过自定义类实现HandlerMethodArgumentResolver接口,并重写其中的supportsParameter()resolveArgument()方法来实现自定义参数解析器,同时也剖析了一些常用的参数注解以及背后的原理,最后通过实战学习了如何通过自定义一个注解来实现参数解析器。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-06-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 啃饼随笔 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面
  • 方法参数解析器
    • HandlerMethodArgumentResolver接口
      • RequestParamMethodArgumentResolver类
        • RequestParamMapMethodArgumentResolver类
          • PrincipalMethodArgumentResolver类
            • PathVariableMethodArgumentResolver类
            • 实战
            • 小结
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档