@RequestBody

1,372 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第20天,点击查看活动详情

@RequestBody

显然这个注解用于读取 Request 请求(可能是 POST,PUT,DELETE,GET 请求)的 body 部分并且Content-Type 为 application/json 格式的数据,接收到数据之后会自动将数据绑定到 Java 对象上去。系统会使用HttpMessageConverter或者自定义的HttpMessageConverter将请求的 body 中的 json 字符串转换为 java 对象。

那到底是如何帮我们转换为java的类的呢?

起初我是没有,getter和setter的,读取出来就是null,后来我自己随便测试,就发现,其实,我们转化为java的类依靠的就是 无参构造+setter

我在SpringMVC里面阅读过对应的源码,因为我们使用的是注解,但是亘古不变的是找DispatcherServlet类中的doDispatch

进入mv = ha.handle(processedRequest, response, mappedHandler.getHandler())这行,

然后看你底子了,我们找到,getMethodArgumentValues这个方法

protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
      Object... providedArgs) throws Exception {
​
   // 获取参数信息
   MethodParameter[] parameters = getMethodParameters();
   // 判断参数是否为空
   if (ObjectUtils.isEmpty(parameters)) {
      return EMPTY_ARGS;
   }
​
   // 创建需要获得的参数数组
   Object[] args = new Object[parameters.length];
   for (int i = 0; i < parameters.length; i++) {
      MethodParameter parameter = parameters[i];
      parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
      // 是否提供相应参数的注解,一般是不提供的 为null
      args[i] = findProvidedArgument(parameter, providedArgs);
      if (args[i] != null) {
         continue;
      }
      // 拿到对应的参数解析器
      if (!this.resolvers.supportsParameter(parameter)) {
         throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
      }
      try {
         args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
      }
      catch (Exception ex) {
         // Leave stack trace for later, exception may actually be resolved and handled...
         if (logger.isDebugEnabled()) {
            String exMsg = ex.getMessage();
            if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
               logger.debug(formatArgumentError(parameter, exMsg));
            }
         }
         throw ex;
      }
   }
   return args;
}

我们就是从这个方法里面获得数据的,而且,只要你是在controller中的方法中的参数,只要是正确的,都能解析

前面先找参数类型,然后再遍历数组尝试一个一个找到参数信息,findProvidedArgument(parameter, providedArgs);就是找到提供的参数,但是我们看里面

protected static Object findProvidedArgument(MethodParameter parameter, @Nullable Object... providedArgs) {
   if (!ObjectUtils.isEmpty(providedArgs)) {
      for (Object providedArg : providedArgs) {
         if (parameter.getParameterType().isInstance(providedArg)) {
            return providedArg;
         }
      }
   }
   return null;
}

由于我们providedArgs是空的,是前面传参没有获取到,所以就返回了,但是这不重要,我们后面还有兜底的解决办法,if (!this.resolvers.supportsParameter(parameter))这里面supportsParameter好眼熟啊,没错,我们在SpringMVC里面也看到过,这说明我们学习基础的时候就不能马虎。

但是这次我们用的是Json,参数是能解析的,但是不用进去存储解析器到缓存了。

回到getMethodArgumentValues方法中,

try {
   args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}

这个是最重要的一步,获取前端传过来的数据,进去看

public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
​
   HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
   if (resolver == null) {
      throw new IllegalArgumentException("Unsupported parameter type [" +
            parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
   }
   return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}

getArgumentResolver看,其实就是查找可以解析参数的解析器

private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
   HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
   if (result == null) {
      for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
         if (resolver.supportsParameter(parameter)) {
            result = resolver;
            this.argumentResolverCache.put(parameter, result);
            break;
         }
      }
   }
   return result;
}

其实,这个方法之前我们执行过了,debug的一定知道,supportsParameter方法里面判断是否不为null就调用这个方法,正是因为前面我们执行过了,所以缓存中已经有数据了,直接return了

再到resolveArgument方法

public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
​
   parameter = parameter.nestedIfOptional();
   Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
   String name = Conventions.getVariableNameForParameter(parameter);
​
   if (binderFactory != null) {
      WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
      if (arg != null) {
         validateIfApplicable(binder, parameter);
         if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
            throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
         }
      }
      if (mavContainer != null) {
         mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
      }
   }
​
   return adaptArgumentIfNecessary(arg, parameter);
}

核心了,看readWithMessageConverters方法

@Override
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
      Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
   // 获取 request 请求
   HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
   Assert.state(servletRequest != null, "No HttpServletRequest");
   ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
​
   Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
   if (arg == null && checkRequired(parameter)) {
      throw new HttpMessageNotReadableException("Required request body is missing: " +
            parameter.getExecutable().toGenericString(), inputMessage);
   }
   return arg;
}

再进入readWithMessageConverters的重载

有点长

protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
      Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
​
   MediaType contentType;
   boolean noContentType = false;
   try {
      // 获得数据类型 application/json;charset=UTF-8
      contentType = inputMessage.getHeaders().getContentType();
   }
   catch (InvalidMediaTypeException ex) {
      throw new HttpMediaTypeNotSupportedException(ex.getMessage());
   }
   if (contentType == null) {
      noContentType = true;
      contentType = MediaType.APPLICATION_OCTET_STREAM;
   }
   // 获得controller类
   Class<?> contextClass = parameter.getContainingClass();
   // 获得我们参数的类,也就是pojo,就是前端传的
   Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);
   if (targetClass == null) {
      ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
      targetClass = (Class<T>) resolvableType.resolve();
   }
   // 获得http请求类型
   HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null);
   Object body = NO_VALUE;
​
   EmptyBodyCheckingHttpInputMessage message = null;
   try {
      // 抽取一下request请求中的头和体的输入信息
      message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
​
      // 遍历解析器,尝试转化为java类
      for (HttpMessageConverter<?> converter : this.messageConverters) {
         // 反射获得解析器类
         Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
         GenericHttpMessageConverter<?> genericConverter =
               (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
         if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
               (targetClass != null && converter.canRead(targetClass, contentType))) {
            if (message.hasBody()) {
               HttpInputMessage msgToUse =
                     getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
               body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
                     ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
               body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
            }
            else {
               body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
            }
            break;
         }
      }
   }
   catch (IOException ex) {
      throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
   }
   finally {
      if (message != null && message.hasBody()) {
         closeStreamIfNecessary(message.getBody());
      }
   }
​
   if (body == NO_VALUE) {
      if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
            (noContentType && !message.hasBody())) {
         return null;
      }
      throw new HttpMediaTypeNotSupportedException(contentType,
            getSupportedMediaTypes(targetClass != null ? targetClass : Object.class));
   }
​
   MediaType selectedContentType = contentType;
   Object theBody = body;
   LogFormatUtils.traceDebug(logger, traceOn -> {
      String formatted = LogFormatUtils.formatValue(theBody, !traceOn);
      return "Read "" + selectedContentType + "" to [" + formatted + "]";
   });
​
   return body;
}

前面是一些基础操作,我们在代码中注释了,在这详细看一下遍历解析器的操作

先获得解析器的类,是不是GenericHttpMessageConverter的子类,如果不是就将genericConverter置为空,是就强转为GenericHttpMessageConverter,有些解析器这里不是,所以是空的,后面的判断也是false的,所以一直在遍历,直到出现了这个类org.springframework.http.converter.json.MappingJackson2HttpMessageConverter,很熟悉,就是Jackon的解析器

所以执行了这个方法genericConverter.canRead(targetType, contextClass, contentType),就是用来判断是否可读

public boolean canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType) {
   // 判断 application/json 是否支持
   if (!canRead(mediaType)) {
      return false;
   }
   // 获得对应的java类型
   JavaType javaType = getJavaType(type, contextClass);
   ObjectMapper objectMapper = selectObjectMapper(javaType.getRawClass(), mediaType);
   if (objectMapper == null) {
      return false;
   }
   AtomicReference<Throwable> causeRef = new AtomicReference<>();
   // 看我们获得的 objectMapper 是否可以反序列化
   if (objectMapper.canDeserialize(javaType, causeRef)) {
      return true; 
   }
   logWarningIfNecessary(javaType, causeRef.get());
   return false;
}

到这里是可以进行操作的,返回了true,回到for循环中,判断了message是否有body,前端传了,肯定有,这个时候我们看里面的信息发现是inputBuffer,到下一步

HttpInputMessage msgToUse = getAdvice().beforeBodyRead(message, parameter, targetType, converterType);前置处理

public HttpInputMessage beforeBodyRead(HttpInputMessage request, MethodParameter parameter,
      Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
​
   for (RequestBodyAdvice advice : getMatchingAdvice(parameter, RequestBodyAdvice.class)) {
      if (advice.supports(parameter, targetType, converterType)) {
         request = advice.beforeBodyRead(request, parameter, targetType, converterType);
      }
   }
   return request;
}

advice.supports()方法

一个 RequestBodyAdvice 实现,增加了对在 Spring MVC @HttpEntity 或 @RequestBody 方法参数上声明的 Jackson 的 @JsonView 注释的支持。

public boolean supports(MethodParameter methodParameter, Type targetType,
      Class<? extends HttpMessageConverter<?>> converterType) {
​
   return (AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType) &&
         methodParameter.getParameterAnnotation(JsonView.class) != null);
}

这个不是我们的重点,但是知道一下还有这么一个注解可以过滤掉不想传给前端的属性,好,出来,得到过滤的变量 msgToUse,进入genericConverter.read(targetType, contextClass, msgToUse)方法

@Override
public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)
      throws IOException, HttpMessageNotReadableException {
​
   JavaType javaType = getJavaType(type, contextClass);
   return readJavaType(javaType, inputMessage);
}

读取java的类型readJavaType,其中参数inputMessage就是msgToUse

private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
   MediaType contentType = inputMessage.getHeaders().getContentType();
   Charset charset = getCharset(contentType);
​
   ObjectMapper objectMapper = selectObjectMapper(javaType.getRawClass(), contentType);
   Assert.state(objectMapper != null, "No ObjectMapper for " + javaType);
​
   boolean isUnicode = ENCODINGS.containsKey(charset.name()) ||
         "UTF-16".equals(charset.name()) ||
         "UTF-32".equals(charset.name());
   try {
      // 读取流中的内容
      InputStream inputStream = StreamUtils.nonClosing(inputMessage.getBody());
      if (inputMessage instanceof MappingJacksonInputMessage) {
         Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView();
         if (deserializationView != null) {
            ObjectReader objectReader = objectMapper.readerWithView(deserializationView).forType(javaType);
            if (isUnicode) {
               return objectReader.readValue(inputStream);
            }
            else {
               Reader reader = new InputStreamReader(inputStream, charset);
               return objectReader.readValue(reader);
            }
         }
      }
      if (isUnicode) {
         // 读取里面的值
         return objectMapper.readValue(inputStream, javaType);
      }
      else {
         Reader reader = new InputStreamReader(inputStream, charset);
         return objectMapper.readValue(reader, javaType);
      }
   }
   catch (InvalidDefinitionException ex) {
      throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
   }
   catch (JsonProcessingException ex) {
      throw new HttpMessageNotReadableException("JSON parse error: " + ex.getOriginalMessage(), ex, inputMessage);
   }
}

return objectMapper.readValue(inputStream, javaType);就是去内部读取值了,最后直接返回

body此时就是对应的java类,然后再后置处理一下,和前置处理感觉差不多,然后就能返回到args[i]中了


为什么需要无参构造?

我们在刚开始的时候就做出测试需要无参构造+setter,上面源码没有体现啊,那是因为在json序列化过程中,我debug好久最后定位到BeanDeserializer这个类中的deserializeFromObject方法

public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) throws IOException
{
    ...
        
    if (_nonStandardCreation) {  // <--- 看这里
        if (_unwrappedPropertyHandler != null) {
            return deserializeWithUnwrapped(p, ctxt);
        }
        if (_externalTypeIdHandler != null) {
            return deserializeWithExternalTypeId(p, ctxt);
        }
        Object bean = deserializeFromObjectUsingNonDefault(p, ctxt);
        /* 27-May-2014, tatu: I don't think view processing would work
         *   at this point, so commenting it out; but leaving in place
         *   just in case I forgot something fundamental...
         */
        /*
        if (_needViewProcesing) {
            Class<?> view = ctxt.getActiveView();
            if (view != null) {
                return deserializeWithView(p, ctxt, bean, view);
            }
        }
        */
        return bean;
    }
    final Object bean = _valueInstantiator.createUsingDefault(ctxt);
    ...
        
    return bean;
}

_nonStandardCreation最重要的就是这个参数,是否有无参构造

我们定位到溢出代码,是BeanDeserializerBase的构造方法里面

image-20220809232454310

红方框里面的值为true

image-20220809232703854

看里面代码

public boolean canCreateUsingDefault() {
        return (_defaultCreator != null);
    }

StdValueInstantiator类实现的,_defaultCreator的值是什么?去看看

/**
 * Default (no-argument) constructor to use for instantiation
 * (with {@link #createUsingDefault})
 */
protected AnnotatedWithParams _defaultCreator;

注释的意思就是:用于实例化的默认(无参数)构造函数(使用 createUsingDefault)

所以代码里面明码标价了,如果不是无参构造后面就执行不了。