前言
目前我们的项目中仅使用到 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();
}
我们关注三个属性:
在构造函数中,我们主要对这 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);
}
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);
}
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 注解
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 方法中我们做了两件事
// 在 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 的处理,扩展了一套新的注解类型而已。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对ZaLou.Cn的支持。