1. 程式人生 > >Android Hack Retrofit 增強引數(固定引數)

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,歡迎拍磚。

相關推薦

Android Hack Retrofit 增強引數固定引數

誰是 Retrofit 此處省去幾百字。。。。 可以看原文介紹。 需求是折騰的動力源泉 話說我最近開始忙活一個跟服務端互動頗多的專案,其中涉及到的全部 HTTP 請求都需要傳入 5 個相同的引數,並需要根據其他所有引數動態生成一個引數。當時也

Retrofit2.0 公共引數固定引數

在實際專案中,對於有需要統一進行公共引數新增的網路請求,可以使用下面的程式碼來實現: RestAdapter restAdapter = new RestAdapter.Builder() .setEndpoint(ct

初始化引數Initialization Parameter知識合集 based on 11g

初始化引數檔案分為: 1)pfile 靜態引數檔案 2)spfile 動態伺服器引數檔案 作用:儲存建立例項、啟動後臺程序所需引數值。 呼叫:例項啟動時,按如下順序調取初始化引數檔案 linux: $ORACLE_HOME/dbs/spfile<SID>.ora $ORACLE

Java中不定項引數可變引數的作用和使用方式

引言:   我們在編寫方法的過程中,可能會遇見一個方法有不確定引數個數的情況。一般我們會用方法過載來解決問題: //方法過載,解決引數個數不確定問題 public void method(); public void method(int i); public void method(int

PyTorch中使用預訓練的模型初始化網路的一部分引數(增減網路層,修改某層引數等) 固定引數

在預訓練網路的基礎上,修改部分層得到自己的網路,通常我們需要解決的問題包括: 1. 從預訓練的模型載入引數  2. 對新網路兩部分設定不同的學習率,主要訓練自己新增的層  一. 載入引數的方法:  載入引數可以參考apaszke推薦的做法,即刪除與當前mo

函式引數預設引數、可變引數、關鍵字引數

原文地址:https://www.cnblogs.com/mingshengling/p/7842826.html 1、預設引數 預設引數降低了函式呼叫的難度,而一旦需要更復雜的呼叫時,又可以傳遞更多的引數來實現。無論是簡單呼叫還是複雜呼叫,函式只需要定義一個。 有多個預設引數時,呼叫的時候,

大地測量——計算七引數程式設計作業

張家誠 2016301610045 需求:通過A,B座標系中提供的六個已知同名點,獲得由A轉換到B座標系的七個引數。 邏輯框圖:                       &nbs

網站分析引數例項分析SimilarWeb外掛引數

閒來無事,看小米官網的時候,點開了之前安裝的similar web外掛,對於網站分析也挺感興趣,藉著這個外掛工具,就初步瞭解一下網站分析相關。 那麼這些指標是什麼意思呢? SimilarWeb Rank:類似網站排名 Global Rank:全球網站排名 第三欄一般是類別,大概網站從事範圍,比如進

預設引數備胎

預設預設引數的定義:   宣告或定義函式時為函式的引數指定一個預設值,在呼叫該函式時,   ①如果沒有指定實參則採用該預設值,   ②否則使用指定的實參 #include<iostream> using namspace std; void Tes

不定引數rest 引數 ...

不定引數 如何實現不定引數 使用過 underscore.js 的人,肯定都使用過以下幾個方法: _.without(array, *values) //返回一個刪除所有values值後的array副本 _.union(*arrays) //返回傳入的arrays(陣列)並集 _.difference

SAP_ABAP_GS01/GS02/GS03資料集_引數條件表靈活配置GS01/GS02/GS03

在開發中,某段程式碼執行可能需要滿足某個條件,通常解決辦法有兩種:一種是在程式碼中寫死限制條件,此種方式當限制條件變化時需要修改程式碼;另一種辦法則是自定義資料表,將限制條件值儲存在表中,當程式執行時,可以直接從表中讀取條件值作為控制條件,這樣比較靈活,就像Java開發中的屬

SpringMVC第八篇——使用陣列接收引數批量刪除

選中批量進行刪除: 頁面提交的form表單 <form action="abc" method="post"> 1<input type="checkbox" na

EXCEL-VBA:Workbooks.Open 引數 開啟檔案

開啟一個工作簿。 語法 表示式 . Open( FileName , UpdateLinks , ReadOnly , Format , Password , WriteResPassword , IgnoreReadOnlyRecommended , Origin , 

如何用 linux 實現命令列引數可變引數實現

僅用main函式的引數實現一個整數計算器 #include <stdio.h> #include <string.h> #include <stdlib.h>

如何向OrderBy傳遞字串引數Entity Framework

AppBox 是基於 FineUI 的通用許可權管理框架,包括使用者管理、職稱管理、部門管理、角色管理、角色許可權管理等模組。 Entity Framework提供的排序功能  再來回顧一下上篇文章,載入使用者列表並進行排序資料庫分頁的程式碼: var q = DB.Users.Incl

正則表示式有無g引數全域性搜尋時,test()結果的差異

有g引數的正則表示式: var kk=//w/w/g;var kk=new RegExp("//w//w","g"); 如果正則表示式有指定g引數全域性匹配 , 則每次test()是依次獲得下一個匹配。舉例說明: var s="AABBCC";var kk=/(/w/w)

spring接收json格式的多個物件引數變通法

兩種方法 方法1 如果使用spring mvc同客戶端通訊,完全使用json資料格式,需要如下定義一個RequestMapping @Controller public class TestController{ @RequestMapping("\test"

js通過路徑傳引數頁面間

A頁面return '<span class="name" style="text-align: center;">' +'<a href="/manage/customer_details?acId='+obj.aData.acId+'">' +'檢

delphi如何用儲存資料庫連線引數INI篇

在設計資料庫應用程式的時候,經常需要將一些資訊從程式中獨立出來,以保證程式的可移植性。其中最重要的資訊就是資料庫的連線引數。在Delphi中,獲得正確的資料庫連線引數的方法十分簡單,你只需要建立一個數據模組,在其中新增一個ADO Connection,雙擊之,然後在彈出的視窗

Android RxJava+Retrofit 一次合併請求多個介面

在實際開發中,我們需要同時請求2個或者2個以上的介面,同時又有更新UI,怎麼辦呢? 最直接的最暴力的方法就是直接在一個方法裡同步呼叫兩個介面,那使用RxJava怎麼實現呢? 這個問題可以使用RxJava的Merge操作符實現,故名思議就是將兩個介面Obser