zoukankan      html  css  js  c++  java
  • SpringBoot解析HTTP参数分析

    本文重点来看几种传参方式,看看它们都是如何被解析并应用到方法参数上的。

    一、HTTP请求处理流程

    在SpringBoot中,一个HTTP请求会被DispatcherServlet类接收,它本质是一个Servlet,因为它继承自HttpServlet。在这里,Spring负责解析请求,匹配到Controller类上的方法,解析参数并执行方法,最后处理返回值并渲染视图。 
    0
    我们今天的重点在于解析参数,对应到上图的目标方法调用这一步骤。既然说到参数解析,那么针对不同类型的参数,肯定有不同的解析器。Spring已经帮我们注册了一堆这东西。
    它们有一个共同的接口HandlerMethodArgumentResolver。supportsParameter用来判断方法参数是否可以被当前解析器解析,如果可以就调用resolveArgument去解析。
    public interface HandlerMethodArgumentResolver {
        //判断方法参数是否可以被当前解析器解析
        boolean supportsParameter(MethodParameter var1);
        //解析参数
        @Nullable
        Object resolveArgument(MethodParameter var1, 
                @Nullable ModelAndViewContainer var2, 
                NativeWebRequest var3, 
                @Nullable WebDataBinderFactory var4)throws Exception;
    } 

    二、RequestParam

    在Controller方法中,如果你的参数标注了RequestParam注解,或者是一个简单数据类型。
    @RequestMapping("/test1")
    @ResponseBody
    public String test1(String t1, @RequestParam(name = "t2",required = false) String t2,HttpServletRequest request){
        logger.info("参数:{},{}",t1,t2);
        return "Java";
    }
    我们的请求路径是这样的:http://localhost:8080/test1?t1=Jack&t2=Java。如果按照以前的写法,我们直接根据参数名称或者RequestParam注解的名称从Request对象中获取值就行。比如像这样:
    String parameter = request.getParameter("t1");
    在Spring中,这里对应的参数解析器是RequestParamMethodArgumentResolver。与我们的想法差不多,就是拿到参数名称后,直接从Request中获取值。 
    protected Object resolveName(String name, MethodParameter parameter, 
            NativeWebRequest request) throws Exception {
            
        HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
        //...省略部分代码...
        if (arg == null) {
            String[] paramValues = request.getParameterValues(name);
            if (paramValues != null) {
                arg = paramValues.length == 1 ? paramValues[0] : paramValues;
            }
        }
        return arg;
    } 

    三、RequestBody

    如果我们需要前端传输更多的参数内容,那么通过一个POST请求,将参数放在Body中传输是更好的方式。当然,比较友好的数据格式当属JSON。 
    0
    面对这样一个请求,我们在Controller方法中可以通过RequestBody注解来接收它,并自动转换为合适的Java Bean对象。
    @ResponseBody
    @RequestMapping("/test2")
    public String test2(@RequestBody SysUser user){
        logger.info("参数信息:{}",JSONObject.toJSONString(user));
        return "Hello";
    } 
    在没有Spring的情况下,我们考虑一下如何解决这一问题呢?首先呢,还是要依靠Request对象。对于Body中的数据,我们可以通过request.getReader()方法来获取,然后读取字符串,最后通过JSON工具类再转换为合适的Java对象。比如像下面这样:
    @RequestMapping("/test2")
    @ResponseBody
    public String test2(HttpServletRequest request) throws IOException {
        BufferedReader reader = request.getReader();
        StringBuilder builder = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null){
            builder.append(line);
        }
        logger.info("Body数据:{}",builder.toString());
        SysUser sysUser = JSONObject.parseObject(builder.toString(), SysUser.class);
        logger.info("转换后的Bean:{}",JSONObject.toJSONString(sysUser));
        return "Java";
    } 
    当然,在实际场景中,上面的SysUser.class需要动态获取参数类型。在Spring中,RequestBody注解的参数会由RequestResponseBodyMethodProcessor类来负责解析。它的解析由父类AbstractMessageConverterMethodArgumentResolver负责。整个过程我们分为三个步骤来看。

    1、获取请求辅助信息

    在开始之前需要先获取请求的一些辅助信息,比如HTTP请求的数据格式,上下文Class信息、参数类型Class、HTTP请求方法类型等。
    protected <T> Object readWithMessageConverters(){
                       
        boolean noContentType = false;
        MediaType contentType;
        try {
            contentType = inputMessage.getHeaders().getContentType();
        } catch (InvalidMediaTypeException var16) {
            throw new HttpMediaTypeNotSupportedException(var16.getMessage());
        }
        if (contentType == null) {
            noContentType = true;
            contentType = MediaType.APPLICATION_OCTET_STREAM;
        }
        Class<?> contextClass = parameter.getContainingClass();
        Class<T> targetClass = targetType instanceof Class ? (Class)targetType : null;
        if (targetClass == null) {
            ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
            targetClass = resolvableType.resolve();
        }
        HttpMethod httpMethod = inputMessage instanceof HttpRequest ?
                ((HttpRequest)inputMessage).getMethod() : null; 
    
        //.......

    2、确定消息转换器

    上面获取到的辅助信息是有作用的,就是要确定一个消息转换器。消息转换器有很多,它们的共同接口是HttpMessageConverter。在这里,Spring帮我们注册了很多转换器,所以需要循环它们,来确定使用哪一个来做消息转换。
    如果是JSON数据格式的,会选择MappingJackson2HttpMessageConverter来处理。它的构造函数正是指明了这一点。
    public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
        super(objectMapper, new MediaType[]{
            MediaType.APPLICATION_JSON, 
            new MediaType("application", "*+json")});
    } 

    3、解析

    既然确定了消息转换器,那么剩下的事就很简单了。通过Request获取Body,然后调用转换器解析就好了。
    protected <T> Object readWithMessageConverters(){
        if (message.hasBody()) {
          HttpInputMessage msgToUse = this.getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
          body = genericConverter.read(targetType, contextClass, msgToUse);
          body = this.getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
        }
    } 
    再往下就是Jackson包的内容了,不再深究。虽然写出来的过程比较啰嗦,但实际上主要就是为了寻找两个东西:
    • 方法解析器RequestResponseBodyMethodProcessor
    • 消息转换器MappingJackson2HttpMessageConverter
    都找到之后调用方法解析即可。

    四、GET请求参数转换Bean

    还有一种写法是这样的,在Controller方法上用Java Bean接收。
    @RequestMapping("/test3")
    @ResponseBody
    public String test3(SysUser user){
        logger.info("参数:{}",JSONObject.toJSONString(user));
        return "Java";
    } 
    然后用GET方法请求:
    http://localhost:8080/test3?id=1001&name=Jack&password=1234&address=北京市海淀区
    URL后面的参数名称对应Bean对象里面的属性名称,也可以自动转换。那么,这里它又是怎么做的呢 ?首先想到的就是Java的反射机制。从Request对象中获取参数名称,然后和目标类上的方法一一对应设置值进去。比如像下面这样:
    public String test3(SysUser user,HttpServletRequest request)throws Exception {
        //从Request中获取所有的参数key 和 value
        Map<String, String[]> parameterMap = request.getParameterMap();
        Iterator<Map.Entry<String, String[]>> iterator = parameterMap.entrySet().iterator();
        //获取目标类的对象
        Object target = user.getClass().newInstance();
        Field[] fields = target.getClass().getDeclaredFields();
        while (iterator.hasNext()){
            Map.Entry<String, String[]> next = iterator.next();
            String key = next.getKey();
            String value = next.getValue()[0];
            for (Field field:fields){
                String name = field.getName();
                if (key.equals(name)){
                    field.setAccessible(true);
                    field.set(target,value);
                    break;
                }
            }
        }
        logger.info("userInfo:{}",JSONObject.toJSONString(target));
        return "Python";
    } 
    除了反射,Java还有一种内省机制可以完成这件事。我们可以获取目标类的属性描述符对象,然后拿到它的Method对象, 通过invoke来设置。
    private void setProperty(Object target,String key,String value) {
        try {
            PropertyDescriptor propDesc = new PropertyDescriptor(key, target.getClass());
            Method method = propDesc.getWriteMethod();
            method.invoke(target, value);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    然后在上面的循环中,我们就可以调用这个方法来实现。
    while (iterator.hasNext()){
        Map.Entry<String, String[]> next = iterator.next();
        String key = next.getKey();
        String value = next.getValue()[0];
        setProperty(userInfo,key,value);
    }
    为什么要说到内省机制呢?因为Spring在处理这件事的时候,最终也是靠它处理的。简单来说,它是通过BeanWrapperImpl来处理的。关于BeanWrapperImpl有个很简单的使用方法:
    SysUser user = new SysUser();
    BeanWrapper wrapper = new BeanWrapperImpl(user.getClass());
    
    wrapper.setPropertyValue("id","20001");
    wrapper.setPropertyValue("name","Jack");
    
    Object instance = wrapper.getWrappedInstance();
    System.out.println(instance);
    wrapper.setPropertyValue最后就会调用到BeanWrapperImpl#BeanPropertyHandler.setValue()方法。它的setValue方法和我们上面的setProperty方法大致相同。
    private class BeanPropertyHandler extends PropertyHandler {
        //属性描述符
        private final PropertyDescriptor pd;
        public void setValue(@Nullable Object value) throws Exception {
            //获取set方法
            Method writeMethod = this.pd.getWriteMethod();
            ReflectionUtils.makeAccessible(writeMethod);
            //设置
            writeMethod.invoke(BeanWrapperImpl.this.getWrappedInstance(), value);
        }
    } 
    通过上面的方式,就完成了GET请求参数到Java Bean对象的自动转换。回过头来,我们再看Spring。虽然我们上面写的很简单,但真正用起来还需要考虑的很多很多。Spring中处理这种参数的解析器是ServletModelAttributeMethodProcessor。它的解析过程在其父类ModelAttributeMethodProcessor.resolveArgument()方法。整个过程,我们也可以分为三个步骤来看。

    1、获取目标类的构造函数

    根据参数类型,先生成一个目标类的构造函数,以供后面绑定数据的时候使用。

    2、创建数据绑定器WebDataBinder

    WebDataBinder继承自DataBinder。而DataBinder主要的作用,简言之就是利用BeanWrapper给对象的属性设值。

    3、绑定数据到目标类,并返回

    在这里,又把WebDataBinder转换成ServletRequestDataBinder对象,然后调用它的bind方法。接下来有个很重要的步骤是,将request中的参数转换为MutablePropertyValues pvs对象。然后接下来就是循环pvs,调用setPropertyValue设置属性。当然了,最后调用的其实就是BeanWrapperImpl#BeanPropertyHandler.setValue()。下面有段代码可以更好的理解这一过程,效果是一样的:
    //模拟Request参数
    Map<String,Object> map = new HashMap();
    map.put("id","1001");
    map.put("name","Jack");
    map.put("password","123456");
    map.put("address","北京市海淀区");
    
    //将request对象转换为MutablePropertyValues对象
    MutablePropertyValues propertyValues = new MutablePropertyValues(map);
    SysUser sysUser = new SysUser();
    //创建数据绑定器
    ServletRequestDataBinder binder = new ServletRequestDataBinder(sysUser);
    //bind数据
    binder.bind(propertyValues);
    System.out.println(JSONObject.toJSONString(sysUser)); 

    五、自定义参数解析器

    我们说所有的消息解析器都实现了HandlerMethodArgumentResolver接口。我们也可以定义一个参数解析器,让它实现这个接口就好了。首先,我们可以定义一个RequestXuner注解。
    @Target({ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface RequestXuner {
        String name() default "";
        boolean required() default false;
        String defaultValue() default "default";
    }
    然后是实现了HandlerMethodArgumentResolver接口的解析器类。
    public class XunerArgumentResolver implements HandlerMethodArgumentResolver {
        @Override
        public boolean supportsParameter(MethodParameter parameter) {
            return parameter.hasParameterAnnotation(RequestXuner.class);
        }
    
        @Override
        public Object resolveArgument(MethodParameter methodParameter,
                                      ModelAndViewContainer modelAndViewContainer,
                                      NativeWebRequest nativeWebRequest,
                                      WebDataBinderFactory webDataBinderFactory){
        
            //获取参数上的注解
            RequestXuner annotation = methodParameter.getParameterAnnotation(RequestXuner.class);
            String name = annotation.name();
            //从Request中获取参数值
            String parameter = nativeWebRequest.getParameter(name);
            return "HaHa,"+parameter;
        }
    }
    不要忘记需要配置一下。
    @Configuration
    public class WebMvcConfiguration extends WebMvcConfigurationSupport {
        @Override
        protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
            resolvers.add(new XunerArgumentResolver());
        }
    }

    一顿操作后,在Controller中我们可以这样使用它:

    @ResponseBody
    @RequestMapping("/test4")
    public String test4(@RequestXuner(name="xuner") String xuner){
        logger.info("参数:{}",xuner);
        return "Test4";
    }

    六、总结

    本文内容通过相关示例代码展示了Spring中部分解析器解析参数的过程。说到底,无论参数如何变化,参数类型再怎么复杂。它们都是通过HTTP请求发送过来的,那么就可以通过HttpServletRequest来获取到一切。Spring做的就是通过注解,尽量适配大部分应用场景。
  • 相关阅读:
    谈谈C++新标准带来的属性(Attribute)
    金融数据智能峰会 | 数据规模爆炸性增长,企业如何进行精准决策?云原生数据仓库数据化运营实战分享
    核桃编程:前端可观测性建设之路
    AI和大数据结合,智能运维平台助力流利说提升核心竞争力
    Python静态类型解析工具简介和实践
    盛京剑客系列24:极简估值教程——题记
    Echarts——关系图(人民的名义为例,简化)源码
    UVA10020(最小区间覆盖)
    LA4636积木艺术
    LA4636积木艺术
  • 原文地址:https://www.cnblogs.com/johnvwan/p/15601494.html
Copyright © 2011-2022 走看看