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

之前在Retrofit细节之http信息拼装(一)中,我们分析了Retrofit是如何处理方法级别的注解信息的,并且明确了对参数级别的注解解析就是每个注解都生成一个ParameterHandler,事先明确一点,我们这里记录的都是注解的信息,至于方法中参数的具体值,每次调用都不一样,是后面传入的,所以在ServiceMethod中是不会有这些信息存在的,则ParameterHandler只会记录注解信息,而不会记录注解对应的值。
在本文中,我们将一一分析参数级别的注解信息是如何生成ParameterHandler的。

生成过程是在parseParameter方法中进行的,先上代码:

private ParameterHandler<?> parseParameter(
        int p, Type parameterType, Annotation[] annotations) {
      ParameterHandler<?> result = null;
      for (Annotation annotation : annotations) {
        ParameterHandler<?> annotationAction = parseParameterAnnotation(
            p, parameterType, annotations, annotation);

        if (annotationAction == null) {
          continue;
        }

        if (result != null) {
          throw parameterError(p, "Multiple Retrofit annotations found, only one allowed.");
        }

        result = annotationAction;
      }

      if (result == null) {
        throw parameterError(p, "No Retrofit annotation found.");
      }

      return result;
    }

这个方法中核心就是parseParameterAnnotation方法,剩下的就是一些检查而已,所以继续跟进去:

    private ParameterHandler<?> parseParameterAnnotation(
        int p, Type type, Annotation[] annotations, Annotation annotation) {
      if (annotation instanceof Url) {
            //……
      } else if (annotation instanceof Path) {
            //……
      } else if (annotation instanceof Query) {
            //……
      } else if (annotation instanceof QueryName) {
            //……
      } else if (annotation instanceof QueryMap) {
            //……
      } else if (annotation instanceof Header) {
            //…… 
      } else if (annotation instanceof HeaderMap) {
            //…… 
      } else if (annotation instanceof Field) {
            //……
      } else if (annotation instanceof FieldMap) {
            //……
      } else if (annotation instanceof Part) {
            //…… 

      } else if (annotation instanceof PartMap) {
            //……
      } else if (annotation instanceof Body) {
            //……
      }

      return null; // Not a Retrofit annotation.
    }

这个方法非常长,大致结构就是判断注解类型,然后生成对应的ParameterHandler,我们要做的就是一一分析每个注解的ParameterHandler是如何生成的。

1. @Url

if (annotation instanceof Url) {
        if (gotUrl) {
          throw parameterError(p, "Multiple @Url method annotations found.");
        }
        if (gotPath) {
          throw parameterError(p, "@Path parameters may not be used with @Url.");
        }
        if (gotQuery) {
          throw parameterError(p, "A @Url parameter must not come after a @Query");
        }
        if (relativeUrl != null) {
          throw parameterError(p, "@Url cannot be used with @%s URL", httpMethod);
        }

        gotUrl = true;

        if (type == HttpUrl.class
            || type == String.class
            || type == URI.class
            || (type instanceof Class && "android.net.Uri".equals(((Class<?>) type).getName()))) {
          return new ParameterHandler.RelativeUrl();
        } else {
          throw parameterError(p,
              "@Url must be okhttp3.HttpUrl, String, java.net.URI, or android.net.Uri type.");
        }

      }

本注解用于标注动态url,处理起来比较简单,除了做一些检查外,就直接生成了一个ParameterHandler.RelativeUrl(),这个handler能做什么呢?我们看看它的apply方法:

 @Override void apply(RequestBuilder builder, @Nullable Object value) {
      checkNotNull(value, "@Url parameter is null.");
      builder.setRelativeUrl(value);
    }

这个handler主要就是吧value设置到RequestBuilder的relativeUrl上。

2. @Path

先回顾下@Path注解的作用,用来替换url中的动态占位符:

@POST("/{a}") Call<String> postRequestBody(@Path("a") Object a);

由于有可能有多个占位符,所以此处注解中加了一个name参数"a",用来知名本参数将用来替换哪个占位符。然后看源码:

else if (annotation instanceof Path) {
        if (gotQuery) {
          throw parameterError(p, "A @Path parameter must not come after a @Query.");
        }
        if (gotUrl) {
          throw parameterError(p, "@Path parameters may not be used with @Url.");
        }
        if (relativeUrl == null) {
          throw parameterError(p, "@Path can only be used with relative url on @%s", httpMethod);
        }
        gotPath = true;

        Path path = (Path) annotation;
        String name = path.value();
        validatePathName(p, name);

        Converter<?, String> converter = retrofit.stringConverter(type, annotations);
        return new ParameterHandler.Path<>(name, converter, path.encoded());

      }

首先是做一些常规检查,然后取出了@Path注解中的name字段,最后加上一个converter一起生成了一个ParameterHandler.Path对象。
其他的都能理解,但这里为什么需要那个converter对象呢?还是去看下这个handler的apply方法是怎么用到这个converter的:

@Override void apply(RequestBuilder builder, @Nullable T value) throws IOException {
      if (value == null) {
        throw new IllegalArgumentException(
            "Path parameter \"" + name + "\" value must not be null.");
      }
      builder.addPathParam(name, valueConverter.convert(value), encoded);
    }

可以看到,将name,value放到RequestBuilder之前要用converter对value进行处理下,因为这个value是我们自己传入到方法中的,可能是string,也可能是其他的对象,怎么处理要由这个converter决定。
通常情况我们传入的都是一个String,所以默认取得也是ToStringConverter,但如果传入的不是String,这个converter就得我们自己定义了,然后在初始化时传入到Retrofit对象中去。

3. @Query

@Query注解是用来将参数加到url后面的,看下处理的源码:

else if (annotation instanceof Query) {
        Query query = (Query) annotation;
        String name = query.value();
        boolean encoded = query.encoded();

        Class<?> rawParameterType = Utils.getRawType(type);
        gotQuery = true;
        //首先判断当前传入的参数是不是Iterable类型,此处以List<String>为例
        if (Iterable.class.isAssignableFrom(rawParameterType)) {
          if (!(type instanceof ParameterizedType)) {
            throw parameterError(p, rawParameterType.getSimpleName()
                + " must include generic type (e.g., "
                + rawParameterType.getSimpleName()
                + "<String>)");
          }
          //获取当前是那种Iterable,此处获取的就是List
          ParameterizedType parameterizedType = (ParameterizedType) type;
          //获取迭代对象的类型,此处获取的是List<String>中的String类型
          Type iterableType = Utils.getParameterUpperBound(0, parameterizedType);
          Converter<?, String> converter =
              retrofit.stringConverter(iterableType, annotations);
          return new ParameterHandler.Query<>(name, converter, encoded).iterable();
        } else if (rawParameterType.isArray()) {
        //此处获取的是数组成员的类型,如果是基本类型会返回对应的包装类型
          Class<?> arrayComponentType = boxIfPrimitive(rawParameterType.getComponentType());
          Converter<?, String> converter =
              retrofit.stringConverter(arrayComponentType, annotations);
          return new ParameterHandler.Query<>(name, converter, encoded).array();
        } else {
        //正常处理单个对象
          Converter<?, String> converter =
              retrofit.stringConverter(type, annotations);
          return new ParameterHandler.Query<>(name, converter, encoded);
        }

      }

对于Query的参数处理主要是分成了三类,一种是可迭代类型,比如list,一种是数组,还有一种是普通对象。
首先是对普通对象的处理,和@Path注解同理converter对象是必须要的,当然,这里还有一个encoded字段,主要是表明当前传入的参数有没有经过url编码,没有的话系统会进行编码。而这个handler的apply方法我们也研究下:

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

      String queryValue = valueConverter.convert(value);
      if (queryValue == null) return; // Skip converted but null values

      builder.addQueryParam(name, queryValue, encoded);
    }

其实就是简单的吧value用converter转化下,然后将相应的信息赛道RequesetBuilder中。
接下来就是对于参数是数组或Iterable类型的处理,这里不是通过正常的构造方法生成handler对象,而是调用了iterable和array方法,这些都是父类中的,我们看下源码:

final ParameterHandler<Iterable<T>> iterable() {
    return new ParameterHandler<Iterable<T>>() {
      @Override void apply(RequestBuilder builder, @Nullable Iterable<T> values)
          throws IOException {
        if (values == null) return; // Skip null values.

        for (T value : values) {
          ParameterHandler.this.apply(builder, value);
        }
      }
    };
  }

  final ParameterHandler<Object> array() {
    return new ParameterHandler<Object>() {
      @Override void apply(RequestBuilder builder, @Nullable Object values) throws IOException {
        if (values == null) return; // Skip null values.

        for (int i = 0, size = Array.getLength(values); i < size; i++) {
          //noinspection unchecked
          ParameterHandler.this.apply(builder, (T) Array.get(values, i));
        }
      }
    };
  }

通过这两个方法生成的对象还是对应的对象,但重写了apply方法,会遍历所有值去调用正常handler对象中的apply方法。

4. @QueryName

@QueryNamey用来向url中添加没有的value的key,其处理方式和@Query几乎一模一样,只是没有value而已,这里就不介绍了。

5. @QueryMap

@QueryMap是用来将一个map形式的参数添加到Url后面,看下处理源码:

else if (annotation instanceof QueryMap) {
        Class<?> rawParameterType = Utils.getRawType(type);
        if (!Map.class.isAssignableFrom(rawParameterType)) {
          throw parameterError(p, "@QueryMap parameter type must be Map.");
        }
        Type mapType = Utils.getSupertype(type, rawParameterType, Map.class);
        if (!(mapType instanceof ParameterizedType)) {
          throw parameterError(p, "Map must include generic types (e.g., Map<String, String>)");
        }
        ParameterizedType parameterizedType = (ParameterizedType) mapType;
        Type keyType = Utils.getParameterUpperBound(0, parameterizedType);
        if (String.class != keyType) {
          throw parameterError(p, "@QueryMap keys must be of type String: " + keyType);
        }
        Type valueType = Utils.getParameterUpperBound(1, parameterizedType);
        Converter<?, String> valueConverter =
            retrofit.stringConverter(valueType, annotations);

        return new ParameterHandler.QueryMap<>(valueConverter, ((QueryMap) annotation).encoded());

      } 

这里三个if做了三个检查,分别要求传入的参数必须是map类型,map的值类型必须是基本类型,map的key必须是string类型。
最后利用传入map的value的类型和一个converter生成了一个handler,到这一部我们已经可以猜测这个handler必然可以遍历这个map,然后将键值对放到url中去,可以看下apply的代码:

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

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

        String convertedEntryValue = valueConverter.convert(entryValue);
        if (convertedEntryValue == null) {
          throw new IllegalArgumentException("Query map value '"
              + entryValue
              + "' converted to null by "
              + valueConverter.getClass().getName()
              + " for key '"
              + entryKey
              + "'.");
        }

        builder.addQueryParam(entryKey, convertedEntryValue, encoded);
      }
    }

这里证实了之前的猜测,整个apply方法就是遍历map,然后将一个个的value的值经过converter转换后最终传到RequestBuilder中去。

6. @Header

@Header 注解用来将参数放到http请求的header中去,此注解需要设置name字段,看源码:

else if (annotation instanceof Header) {
        Header header = (Header) annotation;
        String name = header.value();

        Class<?> rawParameterType = Utils.getRawType(type);
        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);
          Converter<?, String> converter =
              retrofit.stringConverter(iterableType, annotations);
          return new ParameterHandler.Header<>(name, converter).iterable();
        } else if (rawParameterType.isArray()) {
          Class<?> arrayComponentType = boxIfPrimitive(rawParameterType.getComponentType());
          Converter<?, String> converter =
              retrofit.stringConverter(arrayComponentType, annotations);
          return new ParameterHandler.Header<>(name, converter).array();
        } else {
          Converter<?, String> converter =
              retrofit.stringConverter(type, annotations);
          return new ParameterHandler.Header<>(name, converter);
        }

      } 

和@Query注解类似,这里也将传入的参数类型分为了三类,Iterable,Array以及普通的单个的类型,对应生成了不同的handler,核心handler还是ParameterHandler.Header中的apply方法:

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

      String headerValue = valueConverter.convert(value);
      if (headerValue == null) return; // Skip converted but null values.

      builder.addHeader(name, headerValue);
    }

这里就只是简单的将注解修饰的参数用converter转换后塞给RequestBuilder。那么我们可以知道,利用ParameterHandler.iterable()方法以及array方法生成的也是这种handler,只不过apply方法中会遍历所有的数据,对每一个调用一次上面的apply方法罢了。

7. @HeaderMap

@HeaderMap注解是将一个map中的数据添加到http请求的Header中,整个处理过程和@Header一模一样,只是对应的handler的apply方法会遍历一遍这个map,然后一一放到RequestBuilder中去,这里就不贴代码了。

8. @Field,@FieldMap

经过上面7个注解的分析,对于这两个注解的处理其实大致一样,都是生成对应的handler,然后大家理解handler中的apply方法就行了,这里留给大家自己分析。

9. @Part 重点

写到这里,即使我偷懒省掉了几个注解的分析,留给了大家,但篇幅还是有点长了,尤其是@Part注解涉及到文件上传,和之前的截然不同,要理解源码得先弄懂http协议关于文件上传的部分,所以我会另起一篇文章,这次就到这里。

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

推荐阅读更多精彩内容