Retrofit源码之http信息拼装(三)

通过前几篇文章的分析,对于Retrofit的注解我们基本大致了解了是如何解析的,又是如何将数据委托给OkHttp的,但是我们还剩下一个@Part,@PartMap,@Body注解。

1. @Part

首先我们看下正常的上传文件是如何定义的:

@Multipart
    @POST("UploadServlet")
    Call<ResponseBody> upload(@Part("description") RequestBody description,
                              @Part MultipartBody.Part file);

从这里我们可以看到,@Part注解有两种情形,一种是有name的,一种是没有的。知道这些我们再看源码:

else if (annotation instanceof Part) {
        if (!isMultipart) {
          throw parameterError(p, "@Part parameters can only be used with multipart encoding.");
        }
        Part part = (Part) annotation;
        gotPart = true;

        String partName = part.value();
        Class<?> rawParameterType = Utils.getRawType(type);
        if (partName.isEmpty()) {
            // ……
        } else {
           // ……
          
        }

首先确保使用@Part注解时必须使用@MultiPart注解,然后获取@Part注解的name,结合上面提到的@Part注解使用时的两种情况,这里对name是否为空分别处理。

加了name属性

对于加了name属性的情况:

else {
         //构建上传文件所需要的headers
          Headers headers =
              Headers.of("Content-Disposition", "form-data; name=\"" + partName + "\"",
                  "Content-Transfer-Encoding", part.encoding());

         //判断参数类型是否是Iterable类型
          if (Iterable.class.isAssignableFrom(rawParameterType)) {
            if (!(type instanceof ParameterizedType)) {
              throw parameterError(p, rawParameterType.getSimpleName()
                  + " must include generic type (e.g., "
                  + rawParameterType.getSimpleName()
                  + "<String>)");
            }
            ParameterizedType parameterizedType = (ParameterizedType) type;
            //获取Iterable集合中数据的类型
            Type iterableType = Utils.getParameterUpperBound(0, parameterizedType);
            if (MultipartBody.Part.class.isAssignableFrom(Utils.getRawType(iterableType))) {
            //此处保证加了name属性的@Part注解 所修饰的参数不能是MultipartBody.Part类型
              throw parameterError(p, "@Part parameters using the MultipartBody.Part must not "
                  + "include a part name in the annotation.");
            }
            Converter<?, RequestBody> converter =
                retrofit.requestBodyConverter(iterableType, annotations, methodAnnotations);
            return new ParameterHandler.Part<>(headers, converter).iterable();
          } else if (rawParameterType.isArray()) {
          //判断修饰的参数是否是数组类型
            Class<?> arrayComponentType = boxIfPrimitive(rawParameterType.getComponentType());
            if (MultipartBody.Part.class.isAssignableFrom(arrayComponentType)) {
              throw parameterError(p, "@Part parameters using the MultipartBody.Part must not "
                  + "include a part name in the annotation.");
            }
            Converter<?, RequestBody> converter =
                retrofit.requestBodyConverter(arrayComponentType, annotations, methodAnnotations);
            return new ParameterHandler.Part<>(headers, converter).array();
          } else if (MultipartBody.Part.class.isAssignableFrom(rawParameterType)) {
            throw parameterError(p, "@Part parameters using the MultipartBody.Part must not "
                + "include a part name in the annotation.");
          } else {
            Converter<?, RequestBody> converter =
                retrofit.requestBodyConverter(type, annotations, methodAnnotations);
            return new ParameterHandler.Part<>(headers, converter);
          }

源码每一句都不难懂,尤其是稍微复杂的地方我都加了注释,这里主要解释下源码的思路。
首先,对于加了name的注解来说,必须要利用name生成一个Header。
然后就是之前遇到的常规操作,将注解修饰的参数分为了三类,Iterable类型、数组类型、常规类型,分别生成对应的Handler。但是我们也清楚,Iterable类型和数组类型相对于常规类型没有本质上的区别,只是前两种对应的Handler的apply方法被重写了,会遍历所有参数分别执行常规参数对应的Handler的apply方法罢了。
所以我们重点看常规参数类型的Handler怎么生成的:

else {
            Converter<?, RequestBody> converter =
                retrofit.requestBodyConverter(type, annotations, methodAnnotations);
            return new ParameterHandler.Part<>(headers, converter);
          }

这里主要就是拿到了一个converter加上之前生成的Header构造了一个ParameterHandler.Part对象罢了。然后我们再看下apply方法:

@Override void apply(RequestBuilder builder, @Nullable T value) {
      if (value == null) return; // Skip null values.

      RequestBody body;
      try {
        body = converter.convert(value);
      } catch (IOException e) {
        throw new RuntimeException("Unable to convert " + value + " to RequestBody", e);
      }
      builder.addPart(headers, body);
    }

可以看到,这里主要就是利用converter将我们传入的参数转成了RequestBody对象,然后和headers一起塞进了RequestBuilder中。

总结下,@Part如果加了name属性的话,不允许修饰MultipartBody.Part类型,利用name然后会生成一个header,拿到一个converter,生成一个对应的Handler,该handler提供了一个apply方法可以将必要数据设置到RequestBuilder中去。

不加name属性

接下来我们看下不加name的情况:

if (partName.isEmpty()) {
          if (Iterable.class.isAssignableFrom(rawParameterType)) {
            if (!(type instanceof ParameterizedType)) {
              throw parameterError(p, rawParameterType.getSimpleName()
                  + " must include generic type (e.g., "
                  + rawParameterType.getSimpleName()
                  + "<String>)");
            }
            ParameterizedType parameterizedType = (ParameterizedType) type;
            Type iterableType = Utils.getParameterUpperBound(0, parameterizedType);
            if (!MultipartBody.Part.class.isAssignableFrom(Utils.getRawType(iterableType))) {
              throw parameterError(p,
                  "@Part annotation must supply a name or use MultipartBody.Part parameter type.");
            }
            return ParameterHandler.RawPart.INSTANCE.iterable();
          } else if (rawParameterType.isArray()) {
            Class<?> arrayComponentType = rawParameterType.getComponentType();
            if (!MultipartBody.Part.class.isAssignableFrom(arrayComponentType)) {
              throw parameterError(p,
                  "@Part annotation must supply a name or use MultipartBody.Part parameter type.");
            }
            return ParameterHandler.RawPart.INSTANCE.array();
          } else if (MultipartBody.Part.class.isAssignableFrom(rawParameterType)) {
            return ParameterHandler.RawPart.INSTANCE;
          } else {
            throw parameterError(p,
                "@Part annotation must supply a name or use MultipartBody.Part parameter type.");
          }
        }

有了之前的基础,这里就很好分析了。同样也是对注解修饰的参数分成了Iterable、数组、常规类型三类,都做了检查,这里要保证不加name时参数只能是MultipartBody.Part类型,是固定的,也就不需要converter来转换,但不同的是这里生成的不再是ParameterHandler.Part对象,而是ParameterHandler.RawPart类型。我们来研究下它的apply方法能干什么:

    @Override void apply(RequestBuilder builder, @Nullable MultipartBody.Part value)
        throws IOException {
      if (value != null) { // Skip null values.
        builder.addPart(value);
      }
    }

这里主要就是将MultipartBody.Part对象塞给RequestBuilder对象。

2.@PartMap

@PartMap其实就是@Part的加强版,我们先看下他的用法:

@Multipart
@POST()
Observable<ResponseBody> uploadFiles(
        @Url String url,
        @PartMap() Map<String, RequestBody> maps);

从用法中我们可以推测出,其实就是将@Part的带name的用法和不带name的用法综合起来了,我们主要看下对应的Handler的apply用法:

@Override void apply(RequestBuilder builder, @Nullable Map<String, T> value)
        throws IOException {
      if (value == null) {
        throw new IllegalArgumentException("Part map was null.");
      }

      for (Map.Entry<String, T> entry : value.entrySet()) {
        String entryKey = entry.getKey();
        if (entryKey == null) {
          throw new IllegalArgumentException("Part map contained null key.");
        }
        T entryValue = entry.getValue();
        if (entryValue == null) {
          throw new IllegalArgumentException(
              "Part map contained null value for key '" + entryKey + "'.");
        }

        Headers headers = Headers.of(
            "Content-Disposition", "form-data; name=\"" + entryKey + "\"",
            "Content-Transfer-Encoding", transferEncoding);

        builder.addPart(headers, valueConverter.convert(entryValue));
      }
    }

从这里可以看出,利用了map中的key构造了Headers,相当于@Part注解中带name的用法,用value构造了body,相当于@Part中不带name的用法。

3.@Body

先上代码:

else if (annotation instanceof Body) {
        if (isFormEncoded || isMultipart) {
          throw parameterError(p,
              "@Body parameters cannot be used with form or multi-part encoding.");
        }
        if (gotBody) {
          throw parameterError(p, "Multiple @Body method annotations found.");
        }

        Converter<?, RequestBody> converter;
        try {
          converter = retrofit.requestBodyConverter(type, annotations, methodAnnotations);
        } catch (RuntimeException e) {
          // Wide exception range because factories are user code.
          throw parameterError(e, p, "Unable to create @Body converter for %s", type);
        }
        gotBody = true;
        return new ParameterHandler.Body<>(converter);
      }

这里没什么,主要也是拿了一个Converter,然后生成了一个Handler对象,现在我们都可以推测出这个handler的apply方法就是将这个body用converter转换下传给一个RequestBuilder,看看是不是:

@Override void apply(RequestBuilder builder, @Nullable T value) {
      if (value == null) {
        throw new IllegalArgumentException("Body parameter value must not be null.");
      }
      RequestBody body;
      try {
        body = converter.convert(value);
      } catch (IOException e) {
        throw new RuntimeException("Unable to convert " + value + " to RequestBody", e);
      }
      builder.setBody(body);
    }

看到源码,一切不言而喻。

总结

进过三篇文章,我们将Retrofit的注解处理全部了解了一遍,下面我们主要总结以下几点:

  • 注解是什么时候处理的?
    其实核心的处理可以看到是在Retrofit的loadServiceMethod方法中的这句:
result = new ServiceMethod.Builder<>(this, method).build();

即:在第一次调用自定义的接口方法时处理注解,之后将会缓存起来,无需重新分析注解。

  • 注解修饰的参数类型是固定的吗?
    视情况而定,凡是处理注解时获取了Converter对象的都能够通过自定义converter对象来处理任意类型,比如@Header,如果没有获取converter对象的,则不可以,比如@Part不加name属性时。
  • 能否自定义注解?
    从现有源码来看,由于是通过if-else的形式去处理一个个注解的,所以不修改Retrofit源码,无法自定义注解。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,644评论 18 139
  • Scala与Java的关系 Scala与Java的关系是非常紧密的!! 因为Scala是基于Java虚拟机,也就是...
    灯火gg阅读 3,429评论 1 24
  • 之前在Retrofit细节之http信息拼装(一)中,我们分析了Retrofit是如何处理方法级别的注解信息的,并...
    低情商的大仙阅读 1,484评论 0 1
  • 《铁屑》 一粒铁屑飞溅到眼睛里 我狠狠地眨了几下眼 用手轻轻地将它揉出 眼睛红红的,还带着泪 它以为我恋恋不舍 其...
    酒言醉语阅读 162评论 6 5
  • 作者:李忠秋 一 什么是结构思考力 结构思考力是一种先总后分,富有逻辑性的理性思维能力。通过运用结构思考力把握事物...
    卓安安阅读 187评论 0 0