乐趣区

关于spirngmvc:SpringBoot-中如何自定义参数解析器

在一个 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(对 Spring Security 不相熟的小伙伴,能够在公众号江南一点雨后盾回复 ss,有教程),如果我在接口的参数上增加了 @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 中获取即可(具体参数松哥的 Spring Security 教程,公号后盾回复 ss)。

最初,咱们再将自定义的参数解析器配置到 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 的小伙伴应该都晓得(不相熟 Spring Security 的小伙伴能够在公众号【江南一点雨】后盾回复 ss)。

那么这个性能是怎么实现的呢?当然就是 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 松哥当前再和大家具体聊。同时还有一个问题就是这些参数解析器具体是在哪里调用的,这个也会在松哥近期的 SpringMVC 源码解析系列文章中和大家分享,好啦,明天周末,就这点简略的小常识祝大家周末欢快~

退出移动版