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 的处理,扩展了一套新的注解类型而已。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。