暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

一次手误引发的spring请求映射实体的思考

面向优雅编程 2021-07-05
715

我们在使用springboot开发请求接口时,有时会使用注解进行body请求参数映射,在一次愉快的coding中,我误将映射实JSONObject(fastjson工具类)写成JsonObjcet(Gson工具类),导致映射数据为空对象,排查了好久(为低级错误买单),于是考虑从源码层分析这两者的差距。

事件还原:定义mvc接口,使用JSONObject获取参数,请求body参数示例:{"start": "hello"},应用响应:body参数:{"start":"hello"}:

     @RequestMapping("/bodyTest")
     @ResponseBody
     public void requestBodyTest(@RequestBody JSONObject object) {
         System.out.println("body参数:" + JSON.toJSONString(object));
    }

此时将参数类型JSONObject误写为JsonObject,此时传入参数:{"start": "hello"},应用响应:body参数:{}

     @RequestMapping("/bodyTest")
     @ResponseBody
     public void requestBodyTest(@RequestBody JsonObject object) {
         System.out.println("body参数:" + JSON.toJSONString(object));
    }

为什么看似用了两个差不多工具类,都是序列化框架自带的,映射结果却完全不同,这个需要进行研究,得到相似结果不同,要搞清楚这个逻辑需要分析服务层对映射的实现。

 


图1-1 springboot请求流程

1 spring请求解析

springboot通过@RequestBody注解来标识对body请求参数的映射,请求基于springMVC,请求链路如下:

org.springframework.web.servlet.DispatcherServlet#doService
org.springframework.web.servlet.DispatcherServlet#doDispatch
org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter#handle
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#handleInternal
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#invokeHandlerMethod
org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle
org.springframework.web.method.support.InvocableHandlerMethod#invokeForRequest
org.springframework.web.method.support.InvocableHandlerMethod#getMethodArgumentValues
org.springframework.web.method.support.HandlerMethodArgumentResolverComposite#resolveArgument
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters(org.springframework.http.HttpInputMessage, org.springframework.core.MethodParameter, java.lang.reflect.Type)

经过分析发现底层是通过AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters方法进行获取参数,包括两个方法,通过genericConverter.read实现参数获取,内部通过jackson序列化框架进行处理:

 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;
    }
 }

2 反序列化解析

追加断点,继续跟踪read方法内部调用时序。

2.1 JSONObject解析时序

org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter#readJavaType
com.fasterxml.jackson.databind.ObjectMapper#readValue(java.io.InputStream, com.fasterxml.jackson.databind.JavaType)
com.fasterxml.jackson.databind.ObjectMapper#_readMapAndClose
com.fasterxml.jackson.databind.deser.BeanDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext)
com.fasterxml.jackson.databind.deser.BeanDeserializer#deserializeFromObject

2.2 JsonObject解析时序

org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter#readJavaType
com.fasterxml.jackson.databind.ObjectMapper#readValue(java.io.InputStream, com.fasterxml.jackson.databind.JavaType)
com.fasterxml.jackson.databind.ObjectMapper#_readMapAndClose
com.fasterxml.jackson.databind.deser.std.MapDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext)
com.fasterxml.jackson.databind.deser.std.MapDeserializer#_readAndBindStringKeyMap

通过时序分析,可以发现,jackson在对处理不同类时使用了不同的Json反序列化器(JsonDeserializer),对JSONObject使用的是MapDeserializer,而对JsonObject使用的是BeanDeserializer。

3 获取反序列化器

继续对上面jackson源码ObjectMapper类的readMapAndClose方法进行分析,发现它是通过findRootDeserializer方法进行获取反序列化器的:

 protected Object _readMapAndClose(JsonParser p0, JavaType valueType)
     throws IOException
 {
     try (JsonParser p = p0) {
         Object result;
         JsonToken t = _initForReading(p, valueType);
         final DeserializationConfig cfg = getDeserializationConfig();
         final DeserializationContext ctxt = createDeserializationContext(p, cfg);
         if (t == JsonToken.VALUE_NULL) {
             // Ask JsonDeserializer what 'null value' to use:
             result = _findRootDeserializer(ctxt, valueType).getNullValue(ctxt);
        } else if (t == JsonToken.END_ARRAY || t == JsonToken.END_OBJECT) {
             result = null;
        } else {
             JsonDeserializer<Object> deser = _findRootDeserializer(ctxt, valueType);
             if (cfg.useRootWrapping()) {
                 result = _unwrapAndDeserialize(p, ctxt, cfg, valueType, deser);
            } else {
                 result = deser.deserialize(p, ctxt);
            }
             ctxt.checkUnresolvedObjectId();
        }
         if (cfg.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)) {
             _verifyNoTrailingTokens(p, ctxt, valueType);
        }
         return result;
    }
 }

进入内部可以进一步发现,在ObjectMapper类的内部维护了一个ConcurrentHashMap,作为存储反序列化工具的集合,使用时从该工具池中根据类型获取。

 final protected ConcurrentHashMap<JavaType, JsonDeserializer<Object>> _rootDeserializers
     = new ConcurrentHashMap<JavaType, JsonDeserializer<Object>>(64, 0.6f, 2);
 protected JsonDeserializer<Object> _findRootDeserializer(DeserializationContext ctxt,
         JavaType valueType)
     throws JsonMappingException
 {
     // First: have we already seen it?
     JsonDeserializer<Object> deser = _rootDeserializers.get(valueType);
     if (deser != null) {
         return deser;
    }
     // Nope: need to ask provider to resolve it
     deser = ctxt.findRootValueDeserializer(valueType);
     if (deser == null) { // can this happen?
         return ctxt.reportBadDefinition(valueType,
                 "Cannot find a deserializer for type "+valueType);
    }
     _rootDeserializers.put(valueType, deser);
     return deser;
 }

 

4 初始化反序列化工具池

上面讲的是从反序列化工具池获取反序列化器,那么反序列化工具池中的数据来源在哪呢,还是得从第一步spring源码中寻找。通过断点和分析,可以发现在readWithMessageConverters中的genericConverter.read执行反序列化之前,还有一步genericConverter.canRead的逻辑判断。

图4-1 readWithMessageConverters方法部分截取

canRead方法的内部调用链路如下:

org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter#canRead(java.lang.reflect.Type, java.lang.Class<?>, org.springframework.http.MediaType)
com.fasterxml.jackson.databind.ObjectMapper#canDeserialize(com.fasterxml.jackson.databind.JavaType, java.util.concurrent.atomic.AtomicReference<java.lang.Throwable>)
com.fasterxml.jackson.databind.ObjectMapper#createDeserializationContext
com.fasterxml.jackson.databind.deser.DeserializerCache#_createAndCacheValueDeserializer
com.fasterxml.jackson.databind.deser.DeserializerCache#_createAndCache2
com.fasterxml.jackson.databind.deser.DeserializerCache#_createDeserializer
com.fasterxml.jackson.databind.deser.DeserializerCache#_createDeserializer2

其中_createDeserializer2方法,根据JavaType的类型来创建不同的反序列器,里面有一个type.isContainerType()判断,可以做部分反序列化器的生成规则分划。

 protected JsonDeserializer<?> _createDeserializer2(DeserializationContext ctxt,
         DeserializerFactory factory, JavaType type, BeanDescription beanDesc)
     throws JsonMappingException
 {
     final DeserializationConfig config = ctxt.getConfig();
     // If not, let's see which factory method to use:
     if (type.isEnumType()) {
         return factory.createEnumDeserializer(ctxt, type, beanDesc);
    }
     if (type.isContainerType()) {
         if (type.isArrayType()) {
             return factory.createArrayDeserializer(ctxt, (ArrayType) type, beanDesc);
        }
         if (type.isMapLikeType()) {
             // 11-Mar-2017, tatu: As per [databind#1554], also need to block
             //   handling as Map if overriden with "as POJO" option.
             // Ideally we'd determine it bit later on (to allow custom handler checks)
             // but that won't work for other reasons. So do it here.
             // (read: rewrite for 3.0)
             JsonFormat.Value format = beanDesc.findExpectedFormat(null);
             if ((format == null) || format.getShape() != JsonFormat.Shape.OBJECT) {
                 MapLikeType mlt = (MapLikeType) type;
                 if (mlt.isTrueMapType()) {
                     return factory.createMapDeserializer(ctxt,(MapType) mlt, beanDesc);
                }
                 return factory.createMapLikeDeserializer(ctxt, mlt, beanDesc);
            }
        }
         if (type.isCollectionLikeType()) {
             /* 03-Aug-2012, tatu: As per [databind#40], one exception is if shape
              *   is to be Shape.OBJECT. Ideally we'd determine it bit later on
              *   (to allow custom handler checks), but that won't work for other
              *   reasons. So do it here.
              */
             JsonFormat.Value format = beanDesc.findExpectedFormat(null);
             if ((format == null) || format.getShape() != JsonFormat.Shape.OBJECT) {
                 CollectionLikeType clt = (CollectionLikeType) type;
                 if (clt.isTrueCollectionType()) {
                     return factory.createCollectionDeserializer(ctxt, (CollectionType) clt, beanDesc);
                }
                 return factory.createCollectionLikeDeserializer(ctxt, clt, beanDesc);
            }
        }
    }
     if (type.isReferenceType()) {
         return factory.createReferenceDeserializer(ctxt, (ReferenceType) type, beanDesc);
    }
     if (JsonNode.class.isAssignableFrom(type.getRawClass())) {
         return factory.createTreeDeserializer(config, type, beanDesc);
    }
     return factory.createBeanDeserializer(ctxt, type, beanDesc);
 }

通过断点调试,可以发现JSONObject类对应的JavaType的子类是MapType,而JsonObject类对应的JavaType的子类是SimpleType,这两个类对isContainerType()方法的重载实现分别是:

MapType(继承了MapLikeType类)方法的实现:

 @Override
 public boolean isContainerType() {
     return true;
 }

SimpleType方法的实现:

 @Override
 public boolean isContainerType() { return false; }

结合上述代码分析判断可得,JSONObject(对应MapType)执行createMapDeserializer方法,生成MapDeserializer,而JsonObject(对应SimpleType)执行createBeanDeserializer方法,生成BeanDeserializer,谜团终于被揭开。

5 总结与思考

JSONObject继承自Map类,所以jackson为它配备MapDeserializer的反序列化器进行解析,我们把JSONObject换成HashMap,效果是相同的,请求body参数示例:{"start": "hello"},应用响应:body参数:{"start":"hello"}:

     @RequestMapping("/bodyTest")
     @ResponseBody
     public void requestBodyTest(@RequestBody HashMap object) {
         System.out.println("body参数:" + JSON.toJSONString(object));
    }

JsonObject没有继承Map类,jackson为它配置了BeanDeserializer解析器,但是它没有属性名为“start”的field成员,我们把它换成一个含有“start”属性的类即可,如Param类,此时传入参数:{"start": "hello"},应用响应:body参数:{"start":"hello"}。

     public class Param {
         private String start;
 
         public String getStart() {
             return start;
        }
         public void setStart(String start) {
             this.start = start;
        }
    }
 
  @RequestMapping("/bodyTest")
     @ResponseBody
     public void requestBodyTest(@RequestBody Param object) {
         System.out.println("body参数:" + JSON.toJSONString(object));
    }

以上就是本次分析springmvc对body请求映射的处理过程的全部内容,主要通过对servlet层、springmvc层、反序列工具层进行的代码阅读和理解,来解释实际使用中遇到的一些坑或困惑,受益很大,便记录下来,以作学习和分享。


文章转载自面向优雅编程,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论