Retrofit 自定义请求参数注解的思路


目前我们的项目中仅使用到 GETPOST 两种请求方式,对于 GET 请求,请求的参数会拼接在 Url 中;对于 POST 请求来说,我们可以通过 Body表单来提交一些参数信息。

Retrofit 中使用方式

先来看看在 Retrofit 中对于这两种请求的声明方式:

GET 请求

@GET("transporter/info")
Flowable<Transporter> getTransporterInfo(@Query("uid") long id);

我们使用 @Query 注解来声明查询参数,每一个参数都需要用 @Query 注解标记

POST 请求

@POST("transporter/update")
Flowable<ResponseBody> changBind(@Body Map<String,Object> params);

在 Post 请求中,我们通过 @Body 注解来标记需要传递给服务器的对象

Post 请求参数的声明能否更直观

以上两种常规的请求方式很普通,没有什么特别要说明的。
有次团队讨论一个问题,我们所有的请求都是声明在不同的接口中的,如官方示例:

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

如果是 GET 请求还好,通过 @Query 注解我们可以直观的看到请求的参数,但如果是 POST 请求的话,我们只能够在上层调用的地方才能看到具体的参数,那么 POST 请求的参数声明能否像 GET 请求一样直观呢?

@Field 注解

先看代码,关于 @Field 注解的使用:

@FormUrlEncoded
@POST("user/edit")
Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);

使用了 @Field 注解之后,我们将以表单的形式提交数据(first_name = XXX & last_name = yyy)。

基于约定带来的问题

看上去 @Field 注解可以满足我们的需求了,但遗憾的是之前我们和 API 约定了 POST 请求数据传输的格式为 JSON 格式,显然我们没有办法使用该注解了

Retrofit 参数注解的处理流程

这个时候我想是不是可以模仿 @Field 注解,自己实现一个注解最后使得参数以 JSON 的格式传递给 API 就好了,在此之前我们先来看看 Retrofit 中对于请求的参数是如何处理的:

ServiceMethod 中 Builder 的构造函数

Builder(Retrofit retrofit, Method method) {
    this.retrofit = retrofit;
    this.method = method;
    this.methodAnnotations = method.getAnnotations();
    this.parameterTypes = method.getGenericParameterTypes();
    this.parameterAnnotationsArray = method.getParameterAnnotations();
}

我们关注三个属性:

  • methodAnnotations 方法上的注解,Annotation[] 类型
  • parameterTypes 参数类型,Type[] 类型
  • parameterAnnotationsArray 参数注解,Annotation[][] 类型

在构造函数中,我们主要对这 5 个属性赋值。

Builder 构造者的 build 方法

接着我们看看在通过 build 方法创建一个 ServiceMethod 对象的过程中发生了什么:

//省略了部分代码...

public ServiceMethod build() {
    //1. 解析方法上的注解
    for (Annotation annotation : methodAnnotations) {
        parseMethodAnnotation(annotation);
    }

    int parameterCount = parameterAnnotationsArray.length;
    parameterHandlers = new ParameterHandler<?>[parameterCount];
    for (int p = 0; p < parameterCount; p++) {
        Type parameterType = parameterTypes[p];

        Annotation[] parameterAnnotations = parameterAnnotationsArray[p];
        //2. 通过循环为每一个参数创建一个参数处理器
        parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations);
    }
    return new ServiceMethod<>(this);
}

解析方法上的注解 parseMethodAnnotation

if (annotation instanceof GET) {
    parseHttpMethodAndPath("GET", ((GET) annotation).value(), false);
}else if (annotation instanceof POST) {
    parseHttpMethodAndPath("POST", ((POST) annotation).value(), true);
} 

我省略了大部分的代码,整段的代码其实就是来判断方法注解的类型,然后继续解析方法路径,我们仅关注 POST 这一分支:

private void parseHttpMethodAndPath(String httpMethod, String value, boolean hasBody) {
    this.httpMethod = httpMethod;
    this.hasBody = hasBody;
    // Get the relative URL path and existing query string, if present.
    // ...
}

可以看到这条方法调用链其实就是确定 httpMethod 的值(请求方式:POST),hasBody(是否含有 Body 体)等信息

创建参数处理器

在循环体中为每一个参数都创建一个 ParameterHandler:

private ParameterHandler<?> parseParameter(
    int p, Type parameterType, Annotation[] annotations) {
    ParameterHandler<?> result = null;
    for (Annotation annotation : annotations) {
        ParameterHandler<?> annotationAction = parseParameterAnnotation(
        p, parameterType, annotations, annotation);
    }
    // 省略部分代码...
    return result;
}

可以看到方法内部接着调用了 parseParameterAnnotation 方法来返回一个参数处理器:

对于 @Field 注解的处理

else if (annotation instanceof Field) {
    Field field = (Field) annotation;
    String name = field.value();
    boolean encoded = field.encoded();

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

}
  1. 获取注解的值,也就是参数名
  2. 根据参数类型选取合适的 Converter
  3. 返回一个 Field 对象,也就是 @Field 注解的处理器

ParameterHandler.Field

//省略部分代码
static final class Field<T> extends ParameterHandler<T> {
    private final String name;
    private final Converter<T, String> valueConverter;
    private final boolean encoded;

    //构造函数...

    @Override
    void apply(RequestBuilder builder, @Nullable T value) throws IOException {
        String fieldValue = valueConverter.convert(value);
        builder.addFormField(name, fieldValue, encoded);
    }
}

通过 apply 方法将 @Filed 标记的参数名,参数值添加到了 FromBody 中

对于 @Body 注解的处理

else if (annotation instanceof Body) {
    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);
}
  1. 选取合适的 Converter
  2. gotBody 标记为 true
  3. 返回一个 Body 对象,也就是 @Body 注解的处理器

ParameterHandler.Body

 static final class Body<T> extends ParameterHandler<T> {
    private final Converter<T, RequestBody> converter;

    Body(Converter<T, RequestBody> converter) {
        this.converter = converter;
    }

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

通过 Converter 将 @Body 声明的对象转化为 RequestBody,然后设置赋值给 body 对象

apply 方法什么时候被调用

我们来看看 OkHttpCall 的同步请求 execute 方法:

//省略部分代码...
@Override
public Response<T> execute() throws IOException {
    okhttp3.Call call;

    synchronized (this) {
        call = rawCall;
        if (call == null) {
        try {
            call = rawCall = createRawCall();
        } catch (IOException | RuntimeException | Error e) { throwIfFatal(e); //  Do not assign a fatal error to creationFailure.
        creationFailure = e;
                    throw e;
        }
    }
    return parseResponse(call.execute());
}

在方法的内部,我们通过 createRawCall 方法来创建一个 call 对象,createRawCall 方法内部又调用了 serviceMethod.toRequest(args);方法来创建一个 Request 对象:

/**
 * 根据方法参数创建一个 HTTP 请求
 */
Request toRequest(@Nullable Object... args) throws IOException {
    RequestBuilder requestBuilder = new RequestBuilder(httpMethod, baseUrl, relativeUrl, headers, contentType, hasBody, isFormEncoded, isMultipart);
    ParameterHandler<Object>[] handlers = (ParameterHandler<Object>[]) parameterHandlers;

    int argumentCount = args != null ? args.length : 0;
    if (argumentCount != handlers.length) {
      throw new IllegalArgumentException("Argument count (" + argumentCount
          + ") doesn't match expected count (" + handlers.length + ")");
    }

    for (int p = 0; p < argumentCount; p++) {
      handlers[p].apply(requestBuilder, args[p]);
    }

    return requestBuilder.build();
}

可以看到在 for 循环中执行了每个参数对应的参数处理器的 apply 方法,给 RequestBuilder 中相应的属性赋值,最后通过 build 方法来构造一个 Request 对象,在 build 方法中还有至关重要的一步:就是确认我们最终的 Body 对象的来源,是来自于 @Body 注解声明的对象还是来自于其他

RequestBody body = this.body;
if (body == null) {
    // Try to pull from one of the builders.
    if (formBuilder != null) {
        body = formBuilder.build();
    } else if (multipartBuilder != null) {
        body = multipartBuilder.build();
    } else if (hasBody) {
        // Body is absent, make an empty body.
        body = RequestBody.create(null, new byte[0]);
    }
}

自定义 POST 请求的参数注解 @BodyQuery

根据上述流程,想要自定义一个参数注解的话,涉及到以下改动点:

  • 新增类 @BodyQuery 参数注解
  • 新增类 BodyQuery 用来处理 @BodyQuery 声明的参数
  • ServiceMethod 中的 parseParameterAnnotation 方法新增对 @BodyQuery 的处理分支
  • RequestBuilder 类,新增 boolean 值 hasBodyQuery,表示是否使用了 @BodyQuery 注解,以及一个 Map 对象 hasBodyQuery,用来存储 @BodyQuery 标记的参数

@BodyQuery 注解

public @interface BodyQuery {
    /**
     * The query parameter name.
     */
    String value();

    /**
     * Specifies whether the parameter {@linkplain #value() name} and value are already URL encoded.
     */
    boolean encoded() default false;
}

没有什么特殊的,copy 的 @Query 注解的代码

BodyQuery 注解处理器

static final class BodyQuery<T> extends ParameterHandler<T> {
    private final String name;
    private final Converter<T, String> valueConverter;

    BodyQuery(String name, Converter<T, String> valueConverter) {
        this.name = checkNotNull(name, "name == null");
        this.valueConverter = valueConverter;
    }

    @Override
    void apply(RequestBuilder builder, @Nullable T value) throws IOException {
        String fieldValue = valueConverter.convert(value);
        builder.addBodyQueryParams(name, fieldValue);
    }
}

在 apply 方法中我们做了两件事

  1. 模仿 Field 的处理,获取到 @BodyQuery 标记的参数值
  2. 将键值对添加到一个 Map 中
// 在 RequestBuilder 中新增的方法
void addBodyQueryParams(String name, String value) {
    bodyQueryMaps.put(name, value);
}

针对 @BodyQuery 新增的分支处理

else if (annotation instanceof BodyQuery) {
    BodyQuery field = (BodyQuery) annotation;
    String name = field.value();
    hasBodyQuery = true;

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

我省略对于参数化类型的判断,可以看到这里的处理和对于 @Field 的分支处理基本一致,只不过是返回的 ParameterHandler 对象类型不同而已

RequestBuilder

之前我们说过在 RequestBuilder#build() 方法中最重要的一点是确定 body 的值是来自于 @Body 还是表单还是其他对象,这里需要新增一种来源,也就是我们的 @BodyQuery 注解声明的参数值:

RequestBody body = this.body;
if (body == null) {
    // Try to pull from one of the builders.
    if (formBuilder != null) {
        body = formBuilder.build();
    } else if (multipartBuilder != null) {
        body = multipartBuilder.build();
    } else if (hasBodyQuery) {
        body = RequestBody.create(MediaType.parse("application/json; charset=UTF-8"), JSON.toJSONBytes(this.bodyQueryMaps));

    } else if (hasBody) {
    // Body is absent, make an empty body.
    body = RequestBody.create(null, new byte[0]);
    }
}

在 hasBodyQuery 的分支,我们会将 bodyQueryMaps 转换为 JSON 字符串然后构造一个 RequestBody 对象赋值给 body。

最后

通过一个例子来看一下 @BodyQuery 注解的使用:

@Test
public void simpleBodyQuery(){
    class Example{
        @POST("/foo")
        Call<ResponseBody> method(@BodyQuery("A") String foo,@BodyQuery("B") String ping){
            return null;
        }
    }
    Request request = buildRequest(Example.class,"hello","world");
    assertBody(request.body(), "{\"A\":\"hello\",\"B\":\"world\"}");
}

由于 Retrofit 中并没有提供这些类的修改和扩展的权限,因此这里仅仅是一个思路的扩展,我也仅仅是顺着 Retrofit 中对于 ParameterHandler 的处理,扩展了一套新的注解类型而已。

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

推荐阅读更多精彩内容