经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 移动开发 » Android » 查看文章
Retrofit自定义请求参数注解的实现思路
来源:jb51  时间:2018/12/10 9:19:33  对本文有异议

前言

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

Retrofit 中使用方式

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

GET 请求

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

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

POST 请求

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

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

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

以上两种常规的请求方式很普通,没有什么特别要说明的。

有次团队讨论一个问题,我们所有的请求都是声明在不同的接口中的,如官方示例:

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

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

@Field 注解

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

  1. @FormUrlEncoded
  2. @POST("user/edit")
  3. 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 的构造函数

  1. Builder(Retrofit retrofit, Method method) {
  2. this.retrofit = retrofit;
  3. this.method = method;
  4. this.methodAnnotations = method.getAnnotations();
  5. this.parameterTypes = method.getGenericParameterTypes();
  6. this.parameterAnnotationsArray = method.getParameterAnnotations();
  7. }

我们关注三个属性:

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

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

Builder 构造者的 build 方法

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

  1. //省略了部分代码...
  2.  
  3. public ServiceMethod build() {
  4. //1. 解析方法上的注解
  5. for (Annotation annotation : methodAnnotations) {
  6. parseMethodAnnotation(annotation);
  7. }
  8.  
  9. int parameterCount = parameterAnnotationsArray.length;
  10. parameterHandlers = new ParameterHandler<?>[parameterCount];
  11. for (int p = 0; p < parameterCount; p++) {
  12. Type parameterType = parameterTypes[p];
  13.  
  14. Annotation[] parameterAnnotations = parameterAnnotationsArray[p];
  15. //2. 通过循环为每一个参数创建一个参数处理器
  16. parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations);
  17. }
  18. return new ServiceMethod<>(this);
  19. }

解析方法上的注解 parseMethodAnnotation

  1. if (annotation instanceof GET) {
  2. parseHttpMethodAndPath("GET", ((GET) annotation).value(), false);
  3. }else if (annotation instanceof POST) {
  4. parseHttpMethodAndPath("POST", ((POST) annotation).value(), true);
  5. }

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

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

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

创建参数处理器

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

  1. private ParameterHandler<?> parseParameter(
  2. int p, Type parameterType, Annotation[] annotations) {
  3. ParameterHandler<?> result = null;
  4. for (Annotation annotation : annotations) {
  5. ParameterHandler<?> annotationAction = parseParameterAnnotation(
  6. p, parameterType, annotations, annotation);
  7. }
  8. // 省略部分代码...
  9. return result;
  10. }

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

对于 @Field 注解的处理

  1. else if (annotation instanceof Field) {
  2. Field field = (Field) annotation;
  3. String name = field.value();
  4. boolean encoded = field.encoded();
  5.  
  6. gotField = true;
  7. Converter<?, String> converter = retrofit.stringConverter(type, annotations);
  8. return new ParameterHandler.Field<>(name, converter, encoded);
  9.  
  10. }
  • 获取注解的值,也就是参数名
  • 根据参数类型选取合适的 Converter
  • 返回一个 Field 对象,也就是 @Field 注解的处理器

ParameterHandler.Field

  1. //省略部分代码
  2. static final class Field<T> extends ParameterHandler<T> {
  3. private final String name;
  4. private final Converter<T, String> valueConverter;
  5. private final boolean encoded;
  6.  
  7. //构造函数...
  8.  
  9. @Override
  10. void apply(RequestBuilder builder, @Nullable T value) throws IOException {
  11. String fieldValue = valueConverter.convert(value);
  12. builder.addFormField(name, fieldValue, encoded);
  13. }
  14. }

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

对于 @Body 注解的处理

  1. else if (annotation instanceof Body) {
  2. Converter<?, RequestBody> converter;
  3. try {
  4. converter = retrofit.requestBodyConverter(type, annotations, methodAnnotations);
  5. } catch (RuntimeException e) {
  6. // Wide exception range because factories are user code.throw parameterError(e, p, "Unable to create @Body converter for %s", type);
  7. }
  8. gotBody = true;
  9. return new ParameterHandler.Body<>(converter);
  10. }
  • 选取合适的 Converter
  • gotBody 标记为 true
  • 返回一个 Body 对象,也就是 @Body 注解的处理器

ParameterHandler.Body

  1. static final class Body<T> extends ParameterHandler<T> {
  2. private final Converter<T, RequestBody> converter;
  3.  
  4. Body(Converter<T, RequestBody> converter) {
  5. this.converter = converter;
  6. }
  7.  
  8. @Override
  9. void apply(RequestBuilder builder, @Nullable T value) {
  10. RequestBody body;
  11. try {
  12. body = converter.convert(value);
  13. } catch (IOException e) {
  14. throw new RuntimeException("Unable to convert " + value + " to RequestBody", e);
  15. }
  16. builder.setBody(body);
  17. }
  18. }

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

apply 方法什么时候被调用

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

  1. //省略部分代码...
  2. @Override
  3. public Response<T> execute() throws IOException {
  4. okhttp3.Call call;
  5.  
  6. synchronized (this) {
  7. call = rawCall;
  8. if (call == null) {
  9. try {
  10. call = rawCall = createRawCall();
  11. } catch (IOException | RuntimeException | Error e) { throwIfFatal(e); // Do not assign a fatal error to creationFailure.
  12. creationFailure = e;
  13. throw e;
  14. }
  15. }
  16. return parseResponse(call.execute());
  17. }

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

  1. /**
  2. * 根据方法参数创建一个 HTTP 请求
  3. */
  4. Request toRequest(@Nullable Object... args) throws IOException {
  5. RequestBuilder requestBuilder = new RequestBuilder(httpMethod, baseUrl, relativeUrl, headers, contentType, hasBody, isFormEncoded, isMultipart);
  6. ParameterHandler<Object>[] handlers = (ParameterHandler<Object>[]) parameterHandlers;
  7.  
  8. int argumentCount = args != null ? args.length : 0;
  9. if (argumentCount != handlers.length) {
  10. throw new IllegalArgumentException("Argument count (" + argumentCount
  11. + ") doesn't match expected count (" + handlers.length + ")");
  12. }
  13.  
  14. for (int p = 0; p < argumentCount; p++) {
  15. handlers[p].apply(requestBuilder, args[p]);
  16. }
  17.  
  18. return requestBuilder.build();
  19. }

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

  1. RequestBody body = this.body;
  2. if (body == null) {
  3. // Try to pull from one of the builders.
  4. if (formBuilder != null) {
  5. body = formBuilder.build();
  6. } else if (multipartBuilder != null) {
  7. body = multipartBuilder.build();
  8. } else if (hasBody) {
  9. // Body is absent, make an empty body.
  10. body = RequestBody.create(null, new byte[0]);
  11. }
  12. }

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

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

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

@BodyQuery 注解

  1. public @interface BodyQuery {
  2. /**
  3. * The query parameter name.
  4. */
  5. String value();
  6.  
  7. /**
  8. * Specifies whether the parameter {@linkplain #value() name} and value are already URL encoded.
  9. */
  10. boolean encoded() default false;
  11. }

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

BodyQuery 注解处理器

  1. static final class BodyQuery<T> extends ParameterHandler<T> {
  2. private final String name;
  3. private final Converter<T, String> valueConverter;
  4.  
  5. BodyQuery(String name, Converter<T, String> valueConverter) {
  6. this.name = checkNotNull(name, "name == null");
  7. this.valueConverter = valueConverter;
  8. }
  9.  
  10. @Override
  11. void apply(RequestBuilder builder, @Nullable T value) throws IOException {
  12. String fieldValue = valueConverter.convert(value);
  13. builder.addBodyQueryParams(name, fieldValue);
  14. }
  15. }

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

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

针对 @BodyQuery 新增的分支处理

  1. else if (annotation instanceof BodyQuery) {
  2. BodyQuery field = (BodyQuery) annotation;
  3. String name = field.value();
  4. hasBodyQuery = true;
  5.  
  6. Converter<?, String> converter = retrofit.stringConverter(type, annotations);
  7. return new ParameterHandler.BodyQuery<>(name, converter);
  8. }

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

RequestBuilder

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

  1. RequestBody body = this.body;
  2. if (body == null) {
  3. // Try to pull from one of the builders.
  4. if (formBuilder != null) {
  5. body = formBuilder.build();
  6. } else if (multipartBuilder != null) {
  7. body = multipartBuilder.build();
  8. } else if (hasBodyQuery) {
  9. body = RequestBody.create(MediaType.parse("application/json; charset=UTF-8"), JSON.toJSONBytes(this.bodyQueryMaps));
  10.  
  11. } else if (hasBody) {
  12. // Body is absent, make an empty body.
  13. body = RequestBody.create(null, new byte[0]);
  14. }
  15. }

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

最后

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

  1. @Test
  2. public void simpleBodyQuery(){
  3. class Example{
  4. @POST("/foo")
  5. Call<ResponseBody> method(@BodyQuery("A") String foo,@BodyQuery("B") String ping){
  6. return null;
  7. }
  8. }
  9. Request request = buildRequest(Example.class,"hello","world");
  10. assertBody(request.body(), "{\"A\":\"hello\",\"B\":\"world\"}");
  11. }

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

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对w3xue的支持。

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号