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

前言

目前我们的项目中仅使用到 GET 和 POST 两种请求方式,对于 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);

}
  • 获取注解的值,也就是参数名
  • 根据参数类型选取合适的 Converter
  • 返回一个 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);
}
  • 选取合适的 Converter
  • gotBody 标记为 true
  • 返回一个 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 方法中我们做了两件事

  • 模仿 Field 的处理,获取到 @BodyQuery 标记的参数值
  • 将键值对添加到一个 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 的处理,扩展了一套新的注解类型而已。

总结

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

(0)

相关推荐

  • 简略分析Android的Retrofit应用开发框架源码

    面对一个项目,对于Android应用开发框架的选择,我想过三种方案: 1.使用Loader + HttpClient + GreenDao + Gson + Fragment,优点是可定制性强,由于使用Google家自己的Loader和LoaderManager,代码健壮性强. 缺点是整套代码学习成本较高,使用过程中样板代码较多,(比如每一个Request都需要产生一个新类) 2.Volley,作为Google在IO大会上得瑟过的一个网络库,其实不算什么新东西(2013 IO发布),使用较为简单

  • Retrofit 源码分析初探

    现如今,Android开发中,网络层Retrofit+Okhttp组合好像已成标配,身为技术人员,这么火的框架当然得一探究竟,不为装逼,纯粹是为了充电而已. 基本使用介绍 介绍源码前,我们先看下Retrofit的基本使用,大致了解下流程,跟着这个流程来分析源码才不会乱. 1.初始化Retrofit对象 Retrofit retrofit = new Retrofit.Builder() //使用自定义的mGsonConverterFactory .addConverterFactory(Gson

  • Android Retrofit的使用详解

    关于Retrofit的学习,我算是比较晚的了,而现在Retrofit已经是Android非常流行的网络请求框架了.之前,我没有学过Retrofit,但最近公司的新项目使用了Retrofit.Rxjava和OkHttp来进行封装,使用起来非常简便,增加代码的美观程度,也降低了耦合度,这是一个非常棒的框架,特别是这三者一起使用. 简介 Retrofit是Square公司开发的一款针对Android网络请求的框架,现在已经更新到2.3版本了.Retrofit的最大特点是使用运行时注解的方式提供功能.

  • Android Retrofit的简单介绍和使用

    Retrofit与okhttp共同出自于Square公司,retrofit就是对okhttp做了一层封装.把网络请求都交给给了Okhttp,我们只需要通过简单的配置就能使用retrofit来进行网络请求了,其主要作者是Android大神JakeWharton. 导包: compile 'com.squareup.retrofit2:retrofit:2.0.0-beta4'//Retrofit2所需要的包 compile 'com.squareup.retrofit2:converter-gso

  • 详解Retrofit2.0 公共参数(固定参数)

    本文主要介绍了Retrofit2.0 公共参数(固定参数),分享给大家,具体如下: 请先阅读: Retrofit 动态参数(非固定参数.非必须参数)(Get.Post请求) 在实际项目中,对于有需要统一进行公共参数添加的网络请求,可以使用下面的代码来实现: RestAdapter restAdapter = new RestAdapter.Builder() .setEndpoint(ctx).setRequestInterceptor(new RequestInterceptor() { @O

  • Retrofit实现图文上传至服务器

    前言:现在大多数的项目中都涉及图片+文字上传了,下面请详见实现原理: 开发环境:AndroidStudio 1.引入依赖: compile 'com.squareup.retrofit2:retrofit:2.1.0'   2.网络权限: <uses-permission android:name="android.permission.INTERNET" />   3.创建上传对象OkHttpClient : private static final OkHttpClie

  • Android网络请求框架Retrofit详解

    介绍: Retrofit 是Square公司开发的一款针对Android网络请求的框架,Retrofit2底层基于OkHttp实现的,OkHttp现在已经得到Google官方认可,大量的app都采用OkHttp做网络请求.本文使用Retrofit2.0.0版本进行实例演示. 使用Retrofit可以进行GET,POST,PUT,DELETE等请求方式. 同步请求:需要在子线程中完成,会阻塞主线程. Response response = call.execute().body(); 异步请求:请

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

    前言 目前我们的项目中仅使用到 GET 和 POST 两种请求方式,对于 GET 请求,请求的参数会拼接在 Url 中:对于 POST 请求来说,我们可以通过 Body 或表单来提交一些参数信息. Retrofit 中使用方式 先来看看在 Retrofit 中对于这两种请求的声明方式: GET 请求 @GET("transporter/info") Flowable<Transporter> getTransporterInfo(@Query("uid"

  • Spring Gateway自定义请求参数封装的实现示例

    一.需求 在使用spring gateway作为网关时,我们需要在经过网关的请求中添加一些需要传递给后续服务的公共参数,这个时候就可以用到spring gateway提供的自定义请求参数功能了. 二.寻找解决途径 1.参考官方文档 我们可以猜测,spring gateway作为网关功能,肯定会提供很多处理请求参数的功能,于是我们查询文档得到如下内容: 2.探索GatewayFilterFactory实现规律 通过查询spring官方文档可以看到,spring gateway为我们提供了很多xxx

  • SpringBoot 如何自定义请求参数校验

    目录 一.Bean Validation基本概念 二.基本用法 三.自定义校验 3.1 自定义注解 3.2 自定义Validator 3.3 以编程的方式校验(手动) 3.4 定义分组校验 3.5 定制返回码和消息 3.6 更加细致的返回码和消息 四.小结 最近在工作中遇到写一些API,这些API的请求参数非常多,嵌套也非常复杂,如果参数的校验代码全部都手动去实现,写起来真的非常痛苦. 正好Spring轮子里面有一个Validation,这里记录一下怎么使用,以及怎么自定义它的返回结果. 一.B

  • Spring Boot请求处理之常用参数注解使用教程

    目录 请求处理-SpringBoot常用参数注解使用 1.@PathVariable注解 2.@RequestHeader注解 3.@RequestParam注解 4.@CookieValue注解 5.@RequestAttribute注解 6.@RequestBody注解 7.@MatrixVariable与UrlPathHelper 7.1.基本简介 7.2.MatrixVariable注解 7.3.使用细节 7.3.1.WebMvcAutoConfiguration自动装配 7.3.2.U

  • 使用自定义注解进行restful请求参数的校验方式

    目录 自定义注解进行restful请求参数的校验 1.首先我们使用@interface定义一个注解 2.实现注解实现类(和@interface定义的注解在同一个包下) 3.在需要校验的对象的字段上加上@ByteLength注解 springboot小技巧:restful接口参数校验,自定义校验规则 restful风格接口参数校验 自定义参数校验注解方法 自定义注解进行restful请求参数的校验 在使用springmvc开发的时候,我们通常会在controller中的方法参数实体类中加上@Not

  • SpringBoot常见get/post请求参数处理、参数注解校验及参数自定义注解校验详解

    目录 springboot常见httpget,post请求参数处理 PathVaribale获取url路径的数据 RequestParam获取请求参数的值 注意 GET参数校验 POSTJSON参数校验 自定义注解校验 总结 spring boot 常见http get ,post请求参数处理 在定义一个Rest接口时通常会利用GET.POST.PUT.DELETE来实现数据的增删改查:这几种方式有的需要传递参数,后台开发人员必须对接收到的参数进行参数验证来确保程序的健壮性 GET一般用于查询数

  • SpringBoot之自定义Filter获取请求参数与响应结果案例详解

    一个系统上线,肯定会或多或少的存在异常情况.为了更快更好的排雷,记录请求参数和响应结果是非常必要的.所以,Nginx 和 Tomcat 之类的 web 服务器,都提供了访问日志,可以帮助我们记录一些请求信息. 本文是在我们的应用中,定义一个Filter来实现记录请求参数和响应结果的功能. 有一定经验的都知道,如果我们在Filter中读取了HttpServletRequest或者HttpServletResponse的流,就没有办法再次读取了,这样就会造成请求异常.所以,我们需要借助 Spring

  • SpringBoot请求参数相关注解说明小结

    目录 一.@PathVariable 二.@RequestHeader 三.@RequestParam 三.@CookieValue 四.@RequestBody 一.@PathVariable 1.作用映射 url 路径中的变量 2.使用方法 @RestController public class BookController { @GetMapping("/book/{id}") public Integer getBook(@PathVariable("id"

  • Nginx如何获取自定义请求header头和URL参数详解

    目录 一.获取 header 请求头 二.获取url参数 总结 一.获取 header 请求头 在 ngx_lua 中访问 Nginx 内置变量 ngx.var.http_HEADER 即可获得请求头HEADER的内容. 在 nginx配置中,通过$http_HEADER 即可获得请求头HEADER的内容. 案例: $.ajax({ ....... headers: { Accept: "application/json; charset=utf-8", X-TimerLocal: &

  • 聊聊springmvc中controller的方法的参数注解方式

    绪论 相信接触过springmvc的同学都知道,在springmvc的控制层中,我们在方法的参数中可以使用注解标识.比如下面例子: public Map<String, Object> login(@PathVariable("loginParams") String loginParams) @PathVariable注解就标识了这个参数是作为一个请求地址模板变量的(不清楚的同学可以先学习一下restful设计风格).这些注解都是spring内置注解,那么 我们可不可以自

随机推荐