SpringBoot是如何解析HTTP参数的

前言

前几天笔者在写Rest接口的时候,看到了一种传值方式是以前没有写过的,就萌生了一探究竟的想法。在此之前,有篇文章曾涉及到这个话题,但那篇文章着重于处理流程的分析,并未深入。

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

一、HTTP请求处理流程

不论在SpringBoot还是SpringMVC中,一个HTTP请求会被DispatcherServlet类接收,它本质是一个Servlet,因为它继承自HttpServlet。在这里,Spring负责解析请求,匹配到Controller类上的方法,解析参数并执行方法,最后处理返回值并渲染视图。

image

我们今天的重点在于解析参数,对应到上图的目标方法调用这一步骤。既然说到参数解析,那么针对不同类型的参数,肯定有不同的解析器。Spring已经帮我们注册了一堆这东西。

它们有一个共同的接口HandlerMethodArgumentResolversupportsParameter用来判断方法参数是否可以被当前解析器解析,如果可以就调用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。

image

面对这样一个请求,我们在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做的就是通过注解,尽量适配大部分应用场景。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容