requestBody底层实现和实际使用

@RequestBody底层实现和实际使用时存在的问题

前言

要实现@RequestBody封装对象的功能要满足以下条件

  • 前端提交数据列表的时候用json对象的序列化格式提交
  • 后台controller方法的参数是实体类,并且用@RequestBody注解标记

底层实现

流程

  • 流程图
    在这里插入图片描述
  • 流程说明(含部分源码)
  1. 浏览器发出请求之后前端控制器请求处理器映射器处理请求,获取Handler执行链
  2. 前端控制器请求处理器适配器(RequestMappingHandlerAdapter)进行相关适配
  3. 适配器调用HandlerMethodArgumentResolver的reolveArgument方法的实现去处理请求参数
    3.1 reolveArgument源码
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    parameter = parameter.nestedIfOptional();
    //调用适配最合适数据转换器将参数转换成对象,具体如下3.2:
    Object arg = this.readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
    String name = Conventions.getVariableNameForParameter(parameter);
    if (binderFactory != null) {
    	//新建一个数据装订器,将封装对象传入
        WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
        if (arg != null) {
            this.validateIfApplicable(binder, parameter);
            if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) {
                throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
            }
        }
        if (mavContainer != null) {
        	//将封装对象的装订结果放入ModelAndViewContainer容器中,方便后面其他地方的获取
            mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
        }
    }
    //将封装对象再适配然后返回
    return this.adaptArgumentIfNecessary(arg, parameter);
}

3.2 readWithMessageConverters方法解读

先将请求封装成InputMessage流对象

protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter, Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
	//转换请求的类型
    HttpServletRequest servletRequest = (HttpServletRequest)webRequest.getNativeRequest(HttpServletRequest.class);
    Assert.state(servletRequest != null, "No HttpServletRequest");
    //将请求封装成流对象
    ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
    //调用readWithMessageConverters的具体实现
    Object arg = this.readWithMessageConverters(inputMessage, parameter, paramType);
    if (arg == null && this.checkRequired(parameter)) {
        throw new HttpMessageNotReadableException("Required request body is missing: " + parameter.getExecutable().toGenericString(), inputMessage);
    } else {
        return arg;
    }
}

readWithMessageConverters处理请求参数的实际逻辑

@Nullable
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
    boolean noContentType = false;

    MediaType contentType;
    try {
    	//获取请求的contentType,可得提交数据的传递方式(字符串类型),看是表单提交还是json还是。。。
        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;
    Object body = NO_VALUE;

    AbstractMessageConverterMethodArgumentResolver.EmptyBodyCheckingHttpInputMessage message;
    try {
        label94: {
            message = new AbstractMessageConverterMethodArgumentResolver.EmptyBodyCheckingHttpInputMessage(inputMessage);
            /*
			这里是封装参数的重点逻辑
			*/
			//获取所有数据转换器,并添加迭代器进行遍历
            Iterator var11 = this.messageConverters.iterator();
            HttpMessageConverter converter;
            Class converterType;
            GenericHttpMessageConverter genericConverter;
            //开始循环逐个取出数据转换器
            while(true) {
                if (!var11.hasNext()) {
                    break label94;
                }
				
				//将取出的转换器强转成GenericHttpMessageConverter(意图此处不深究)
                converter = (HttpMessageConverter)var11.next();
                converterType = converter.getClass();
                genericConverter = converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter)converter : null;
                if (genericConverter != null) {
                	//调用所有转换器都去实现的HttpMessageConverter接口的canRead方法
                	//  若返回true则此转换器可用于当前请求参数的转换,即跳出循环
                	//  若返回false则不可用,继续遍历其他转换器
                    if (genericConverter.canRead(targetType, contextClass, contentType)) {
                        break;
                    }
                } else if (targetClass != null && converter.canRead(targetClass, contentType)) {
                    break;
                }
            }

            if (message.hasBody()) {
                HttpInputMessage msgToUse = this.getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
                //穿UR参数类型,要封装成的对象的类型和请求数据流,调用转换器的read方法,将请求参数封装成bean对象
                body = genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) : converter.read(targetClass, msgToUse);
                body = this.getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
            } else {
                body = this.getAdvice().handleEmptyBody((Object)null, message, parameter, targetType, converterType);
            }
        }
    } catch (IOException var17) {
        throw new HttpMessageNotReadableException("I/O error while reading input message", var17, inputMessage);
    }

    if (body != NO_VALUE) {
        LogFormatUtils.traceDebug(this.logger, (traceOn) -> {
            String formatted = LogFormatUtils.formatValue(body, !traceOn);
            return "Read \"" + contentType + "\" to [" + formatted + "]";
        });
        return body;
    } else if (httpMethod != null && SUPPORTED_METHODS.contains(httpMethod) && (!noContentType || message.hasBody())) {
        throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
    } else {
        return null;
    }
}
  1. 将参数列表封装成java对象之后,回到resolverArgument方法,将对象进行进一步装订,放入ModelAndViewContain容器中并返回
  2. 逐步返回到适配器中,在返回中,逐步对封装对象进行再封装
  3. 适配器将对象与请求的方法的参数进行绑定,再进行其他适配,然后掉Handler(Controller)的方法

补充

  • 转换器示例
    HttpMessageConverter接口有很多个实现类,都是不同的数据转换器所以在寻找适配请求参数的转换器的时候,我们要获取适配器生成好的所有转换器,然后逐个遍历找出适用的那一个
//负责读取资源文件和写出资源文件数据
ResourceHttpMessageConverter 
//负责读取form提交的数据(能读取的数据格式为 application/x-www-form-urlencoded,不能读取multipart/form-data格式数据);负责写入application/x-www-from-urlencoded和multipart/form-data格式的数据
FormHttpMessageConverter       
//负责读取和写入json格式的数据
MappingJacksonHttpMessageConverter  
//负责读取和写入 xml 中javax.xml.transform.Source定义的数据
SouceHttpMessageConverter                  
//负责读取和写入xml 标签格式的数据
Jaxb2RootElementHttpMessageConverter 
//负责读取和写入Atom格式的数据
AtomFeedHttpMessageConverter              
//负责读取和写入RSS格式的数据
RssChannelHttpMessageConverter           

HttpMessageConverter接口定义了如下五个方法,所有转换器都要实现这些方法。在@RequestBody功能实现过程中,比较重要的是canRead和read这两个方法。我寻找使用的转换器时,遍历转换器集合,逐个调用canRead(canWrite)方法判断该转换器是否可用;找到适用的转换器后调用read(write)方法去将请求参数装换成Bean对象

public interface HttpMessageConverter<T> {
    boolean canRead(Class<?> var1, @Nullable MediaType var2);

    boolean canWrite(Class<?> var1, @Nullable MediaType var2);

    List<MediaType> getSupportedMediaTypes();

    T read(Class<? extends T> var1, HttpInputMessage var2) throws IOException, HttpMessageNotReadableException;

    void write(T var1, @Nullable MediaType var2, HttpOutputMessage var3) throws IOException, HttpMessageNotWritableException;
}

实际使用

  1. 表单提交的时候后台可用@RequestBody标记String类型的参数body去获取整个请求,这里只是直接获取键值对字符串,不进行封装
<form action="anno/testRequestBody1" method="post"><br>
  姓名:<input type="text" name="name"><br>
  密码:<input type="text" name="password"><br>
  <input type="submit" value="提交">
</form>
@RequestMapping("/testRequestBody1")
public String testRequestBody1(@RequestBody String body) throws UnsupportedEncodingException {
    body = URLDecoder.decode(body, "utf-8");
    System.out.println(body);
    return "success";
}

2.若前端表单提交,后台用@RequestBody标记实体类去接收会报错

Content type ‘application/x-www-form-urlencoded;charset=UTF-8’ not supported

  • 表单提交一
<form action="anno/testRequestBody5" method="post"><br>
  姓名:<input type="text" name="name"><br>
  密码:<input type="text" name="password"><br>
  <input type="submit" value="提交">
</form>
  • 表单提交二
function testAjax6() {
 $.ajax({
     url:"anno/testRequestBody5",
     type:"post",
     data:'{"name": "嘿嘿", "password": "123"}',
     //不写contentType也是默认表单提交
     contentType:"application/x-www-form-urlencoded;charset=utf-8",
     dataType:"json",
     success:function (data) {
       alert(data);
     }
   })
 }
  • 后台处理
@RequestMapping(value = "/testRequestBody5")
public void testRequestBody5(@RequestBody User user) {
    System.out.println("testRequestBody5方法执行了");
    System.out.println(user);
}

以上两种方式都会报错

  1. 正确的封装产生的写法
  • js写法一
function testAjax1() {
  $.ajax({
    url:"anno/testRequestBody5",
    type:"post",
    data:'{"name": "嘿嘿", "password": "123"}',
    contentType:"application/json;charset=utf-8",
    dataType:"json",
    success:function (data) {
      alert(data);
   }

  })
}
  • js写法二
function testAjax2() {
  var name = "嘿嘿";
  var password = "123";
  var age = 21;
  var id = 1;
  $.ajax({
    url:"anno/testRequestBody5",
    type:"post",
    data:'{"id": '+id+',"name": "'+name+'", "password": "'+password+'", "age": '+age+'}',
    contentType:"application/json;charset=utf-8",
    dataType:"json",
    success:function (data) {
      alert(data);
    }
  })
}
  • js写法三
function testAjax3() {
  var name = "嘿嘿";
  var password = "123";
  var json = {"name": name, "password": password};
  $.ajax({
    url:"anno/testRequestBody5",
    type:"post",
    data: JSON.stringify(json),
    contentType:"application/json;charset=utf-8",
    dataType:"json",
    success:function (data) {
      alert(data);
    }
  }) 
}
  • 后台接收
    @RequestMapping(value = "/testRequestBody5")
    public void testRequestBody5(@RequestBody User user) {
        System.out.println("testRequestBody5方法执行了");
        System.out.println(user);
    }
  1. 错误写法,此处json对象没有序列化成字符串,故封装时匹配不上,报错如下:

org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Unrecognized token ‘name’: was expecting ‘null’, ‘true’, ‘false’ or NaN; nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token ‘name’: was expecting ‘null’, ‘true’, ‘false’ or NaN

function testAjax4() {
var name = "嘿嘿";
  var password = "123";
  var json = {"name": name, "password": password};
  $.ajax({
    url:"anno/testRequestBody5",
    type:"post",
    data:json,
    contentType:"application/json;charset=utf-8",
    dataType:"json",
    success:function (data) {
      alert(data);
    }

  })
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值