今天为大家分享Prinln的文章:
Android 下午茶:Hack Retrofit 之 增强参数
谁是 Retrofit
此处省去几百字。。。。
可以看原文介绍。
需求是折腾的动力源泉
话说我最近开始忙活一个跟服务端交互颇多的项目,其中涉及到的全部 HTTP 请求都需要传入 5 个相同的参数,并需要根据其他所有参数动态生成一个参数。当时也没多想,用 retrofit 匆匆写完接口,结果就像这样:
public interface Apis {
@FormUrlEncoded
@POST("/apia")
Call<WecarResult> apiA(
@Field("id") String id,
@Field("key") String key,
@Field("q") int q,
@Field("p") String p,
@Field("x") String x,
@Field("param0") String param0,
@Field("param1") String param1,
@Field("param2") int param2);
@FormUrlEncoded
@POST("/apib")
Call<WecarResult> apiB(
@Field("id") String id,
@Field("key") String key,
@Field("q") int q,
@Field("p") String p,
@Field("x") String x,
@Field("param0") String param0);
...
}
其中, id/key/q/p/x 这几个参数是公共参数,每个接口都需要上传,而且 p 是固定参数,即针对一个固定的接口, p 的值是固定的,例如 apiA 的 p=apiA ;而 x 则用来校验其他参数,即 x 的值需要根据其他参数值来计算得到。
而类似 apiA 这样的接口可能还会有好多,你能想象每次调用这些接口时重复填写的几个参数是多么的让人郁闷——真是叔可忍婶儿不可忍。
FixedField
对于参数 p,它的值既然是固定的,为何不能直接配置好呢?
Hack Parameter的注解Field
我最初想到的是直接给 Field 这个注解增加两个参数 isFixed 和 fixedValue ,修改后的 Field 就像这样:
@Documented
@Target(PARAMETER)
@Retention(RUNTIME)
public @interface Field {
String value();
/**
* If true, the parameter value should always be what the fixedValue returns.
* @return
*/
boolean isFixed() default false;
/**
* When isFixed returns true, this value should be the parameter value.
* @return
*/
String fixedValue() default "";
/** Specifies whether the {@linkplain #value() name} and value are already URL encoded. */
boolean encoded() default false;
}
然后我只需要在 Retrofit 提供的代理方法当中用 fixedValue 的返回值偷换掉实际调用的参数即可:
...
Annotation[][] annotations = method.getParameterAnnotations();
for (int i = 0; i < annotations.length; i++) {
Annotation[] annotations1 = annotations[i];
for(Annotation annotation : annotations1){
if(annotation.annotationType() == Field.class){
...
if(field.isFixed()){
args[i] = field.fixedValue();
}
...
}
}
}
...
然后我们稍微改一下我们的接口配置:
@FormUrlEncoded
@POST("/apia")
Call<WecarResult> apiA(
@Field("id") String id,
@Field("key") String key,
@Field("q") int q,
@Field(value = "p", isFixed = true, fixedValue = "apiA") String p,
@Field("x") String x,
@Field("param0") String param0,
@Field("param1") String param1,
@Field("param2") int param2);
比如我们调用 apiA:
apis.apiA("myid","mykey", 0, "fixed to be replaced", "x", "param0", "param1", 0);
我会偷偷用 “apiA” 去替换 “fixed to be replaced”,这样我的第一步目标就达成了,它的方便之处在于我们在调用接口时可以不 care 究竟传什么值给参数 p,你随便传什么,结果都是配置好的;而缺点也是显而易见的,尽管我们可以随便传一个值进去,但我们终究还是要传这么个参数。烦不烦啊。
新增 FixedField 注解
说到这里,我们其实不想在调用接口时还关心这么个配置好的参数。那我们干脆把它从参数列表里面移除好了。于是我想到了把配置写到方法上面,如果配置支持下面的这种样子,那岂不是非常方便?
@FormUrlEncoded
@POST("/apia")
@FixedField(keys = "p", values = "apiA")
Call<WecarResult> apiA(
@Field("id") String id,
@Field("key") String key,
@Field("q") int q,
@Field("x") String x,
@Field("param0") String param0,
@Field("param1") String param1,
@Field("param2") int param2);
那么这时我们再调用 apiA,参数就会少一个:
apis.apiA("myid","mykey", 0, /*"fixed to be replaced", */ "x", "param0", "param1", 0);
那么 FixedField 是如何工作的呢?
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface FixedField {
String[] keys();
String[] values();
}
我们定义了两个数组,keys 表示固定值的参数的 key 列表,values 则是对应这些 key 的值。
在解析时,我们找到 ServiceMethod 这个类,并在下面的方法中添加解析 FixedField 的代码,在这里我们把这些配置好的值存入 fixedFields 这个 Map 当中。
private void parseMethodAnnotation(Annotation annotation) {
if (annotation instanceof DELETE) {
...
}else if(annotation instanceof FixedField){
FixedField fixedField = ((FixedField) annotation);
String[] values = fixedField.values();
String[] keys = fixedField.keys();
for (int i = 0; i < keys.length; i++) {
fixedFields.put(keys[i], values[i]);
}
}else if( annotation instanceof GeneratedField){
...
}
}
这时有朋友就会有疑问,你是什么时候把 FixedField 的 key 绑定到 apiA 上的?其实在存入 fixedFields 之后,接下来这段代码便是绑定 key 的过程:
Converter<?, String> converter = BuiltInConverters.ToStringConverter.INSTANCE;
for(Map.Entry<String, String> entry : fixedFields.entrySet()){
parameterHandlers[p++] =new ParameterHandler.Field(entry.getKey(), converter, false);
}
这个 parameterHandlers 数组存放的其实就是绑定好的参数处理对象,而到了这里,在 apiA 发起请求时已经知道它有一些参数是来自 FixedField 了。
下一个问题,那么参数的值是什么时候应用的?
ServiceMethod serviceMethod = loadServiceMethod(method);
+ args = serviceMethod.rebuildArgs(method, args);
OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
这段代码在 Retrofit 的 create 方法当中,在接口被调用时,我们通过 rebuildArgs 这个方法来偷梁换柱~
public Object[] rebuildArgs(Method method, Object[] args){
List<Object> argsList = new ArrayList<>(Arrays.asList(args));
Map<String, String> extendFieldMap = new TreeMap<>();
if(!this.fixedFields.isEmpty()){
for(Map.Entry<String, String> entry : fixedFields.entrySet()){
argsList.add(entry.getValue());
}
}
...
return argsList.toArray();
}
这样,我们的第二步目标也达成了。来,[]~( ̄▽ ̄)~* 乾杯!
给接口类配置 FixedField
哦,等等,apiA apiB apiC 。。。这些所有的 api 都有一个共同的参数 key (你可以理解为这个 key 是官网申请来的),我要给每一个接口都要配置一遍:
public interface Apis {
@FormUrlEncoded
@POST("/apia")
@FixedField(keys = {"p", "key"}, values = {"apiA", "mykey"})
Call<WecarResult> apiA(
@Field("id") String id,
@Field("q") int q,
@Field("p") String p,
@Field("x") String x,
@Field("param0") String param0,
@Field("param1") String param1,
@Field("param2") int param2);
@FormUrlEncoded
@POST("/apib")
@FixedField(keys = {"p", "key"}, values = {"apiB", "mykey"})
Call<WecarResult> apiB(
@Field("id") String id,
@Field("q") int q,
@Field("x") String x,
@Field("param0") String param0);
...
}
难道我们就不能一次配置,处处可用么?就像这样?
@FixedField(keys = "key", values = "mykey")
public interface Apis {
@FormUrlEncoded
@POST("/apia")
@FixedField(keys = "p", values = "apiA")
Call<WecarResult> apiA(
@Field("id") String id,
@Field("q") int q,
@Field("p") String p,
@Field("x") String x,
@Field("param0") String param0,
@Field("param1") String param1,
@Field("param2") int param2);
@FormUrlEncoded
@POST("/apib")
@FixedField(keys = "p", values = "apiB")
Call<WecarResult> apiB(
@Field("id") String id,
@Field("q") int q,
@Field("x") String x,
@Field("param0") String param0);
...
}
为什么不呢。其实有了前面的基础,我们只需要令我们的 FixedField 支持 Type,并且在解析注解时,加上对类的注解的解析即可。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface FixedField {
String[] keys();
String[] values();
}
下面这段代码在 ServiceMethod.Builder.build() 方法中,解析操作与前面的类似,不再细说。
Annotation[] classAnnotations = method.getDeclaringClass().getDeclaredAnnotations();
for (Annotation classAnnotation : classAnnotations) {
parseClassAnnotation(classAnnotation);
}
哈哈,于是,我们的又解锁了新的技能!
小结
FixedField 提供了一种能力,我们可以在接口上直接配置固定值的参数,让他们不再成为我们的烦恼。当然,你可以配置多个,上面的例子我们已经这么做了。
GeneratedField 和GeneratedFieldMap
GeneratedField
前一节提到了如何配置值固定的参数,我们这一节要考虑配置动态生成的参数。与值固定的参数不同的是,动态生成的参数值不固定,不过它们的计算方法却是固定的。
这一节的大部分内容与前面相似,不同之处就是下面这个 Generator 了:
public interface FieldGenerator {
public String generate(Method method, Map<String, String> extendFields, Object ... args);
}
根据参数的生成算法不同,你需要实现 FieldGenerator 接口,并返回生成的值,generate 方法的参数解释如下:
- method:就是被调用的 api 方法
- extendFields:包括全部的 FixedField 和已经生成的 GeneratedField
- args:api 方法调用时正常传入的参数
而 GeneratedField 的定义如下:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface GeneratedField {
String[] keys();
Class<? extends FieldGenerator>[] generators();
}
于是我们便可以如此配置:
@FixedField(keys = "key", values = "mykey")
@GeneratedField(keys = "x", generators = XGen.class)
public interface Apis {
@FormUrlEncoded
@POST("/apia")
@FixedField(keys = "p", values = "apiA")
Call<WecarResult> apiA(
@Field("id") String id,
@Field("q") int q,
@Field("p") String p,
@Field("param0") String param0,
@Field("param1") String param1,
@Field("param2") int param2);
@FormUrlEncoded
@POST("/apib")
@FixedField(keys = "p", values = "apiB")
Call<WecarResult> apiB(
@Field("id") String id,
@Field("q") int q,
@Field("param0") String param0);
...
}
XGen.class 便是我们自定义的 x 的值的 Generator。
绑定 key 的方式与 FixedField 完全相同,而绑定参数值的方法同样见于 rebuildArgs 方法中:
public Object[] rebuildArgs(Method method, Object[] args){
List<Object> argsList = new ArrayList<>(Arrays.asList(args));
Map<String, String> extendFieldMap = new TreeMap<>();
if(!this.fixedFields.isEmpty()){
for(Map.Entry<String, String> entry : fixedFields.entrySet()){
argsList.add(entry.getValue());
}
}
extendFieldMap.putAll(fixedFields);
Map<String, String> generatedFieldMap = null;
if(this.fieldMapGenerator != null){
try{
generatedFieldMap = fieldMapGenerator.newInstance().generate(method, extendFieldMap, args);
extendFieldMap.putAll(generatedFieldMap);
}catch (Exception e){
e.printStackTrace();
}
}
if(!this.generatedFields.isEmpty()){
for(Map.Entry<String, Class<? extends FieldGenerator>> entry : generatedFields.entrySet()){
try {
FieldGenerator generator = entry.getValue().newInstance();
String generatedArg = generator.generate(method, extendFieldMap, args);
argsList.add(generatedArg);
extendFieldMap.put(entry.getKey(), generatedArg);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
if(generatedFieldMap != null){
argsList.add(generatedFieldMap);
}
return argsList.toArray();
}
其中关键代码就是这一句:
FieldGenerator generator = entry.getValue().newInstance();
String generatedArg = generator.generate(method, extendFieldMap, args);
GeneratedFieldMap
与 GeneratedField 其实没什么太大的差别,看上去更像是 GeneratedField 的一个增强版,如果你有很多个这样的动态生成的参数,而又不想写太多的配置,试试这个呗,你只需要实现你的 FieldMapGenerator,一切又变得简单起来!
public interface FieldMapGenerator {
Map<String, String> generate(Method method, Map<String, String> extendFields, Object... args);
}
小结
这次折腾,除了确实得到的开发上的便利之外,我也对 Retrofit 有了更进一步的了解。Hack 后的代码放到了我的 github,欢迎拍砖。