Retrofit之Invocation

目前Android開(kāi)發(fā)接口請(qǐng)求流行使用 Retrofit+rxjava+okhttp, 絕大多數(shù)的請(qǐng)求也都可以很輕松的實(shí)現(xiàn)或者有現(xiàn)成的demo可以參考, 也有個(gè)別特殊情況.

需求

  • http 頭部加字段:
APP-PARAMS = version+||+client+||+channel+||+device+||+timestamp
參數(shù)名 類型 說(shuō)明
version string 版本號(hào)
client int 客戶端(安卓,ios)
channel int 渠道
device string 設(shè)備號(hào)
timestamp int 時(shí)間戳

以上基本字段全部參與加密簽名瑰抵。

  • 簽名規(guī)則

除了key(密鑰乓搬,由服務(wù)端提供)以外,其他參數(shù)(含post和get)按照ascii的順序排序茴迁,各個(gè)參數(shù)值以“||”拼接成字符串后再追加key,之后再用md5加密生成簽名酱塔。

encryptString = paramA + '||' + paramB + '||' + key
sign = MD5(encryptString);

出于安全和防刷和其他目的, 服務(wù)端做出上面的請(qǐng)求規(guī)則, 可能有更好的方式來(lái)做, 不在討論范圍, 這里只針對(duì)需求來(lái)實(shí)現(xiàn). 除了上面的基本字段需要加密外, 不同的請(qǐng)求有不同的加密字段, 可能額外需要5個(gè)字段但只有2個(gè)需要加密, 加密簽名后的字段最終放入sign字段發(fā)送請(qǐng)求.

參數(shù)名 必選 類型 說(shuō)明 加密
type int 類型(1-登錄,2-重置密碼)
xxx string 其他參數(shù)
sign string 簽名

分析

? 請(qǐng)求簽名這種屬于通用性的規(guī)則, 首選的做法就是使用okhttp的攔截器對(duì)每個(gè)請(qǐng)求進(jìn)行簽名加密. 從上面的需求可以看到請(qǐng)求需要傳一個(gè)Header, 請(qǐng)求需要一個(gè)簽名后的sign字段, Header和sign字段中需要使用到一些共同的字段(基本字段和接口中標(biāo)注需要加密的, 時(shí)間戳本地獲取要確保一樣), 哪些字段需要簽名都要可以隨接口來(lái)自定義的, 于是想到了注解. 自定義注解Sign作用于retrofit請(qǐng)求的參數(shù)表示需要簽名:

public interface Server{
  @GET("/server/a")
  Observable<ResponseEntity> request(@Sign @Query("type") int type, @Query("xxx") String xxx);
}

上面定義的請(qǐng)求接口可以清晰的看出需要參與簽名的字段, 變更時(shí)也可以靈活修改绳矩。 由于sign字段所有請(qǐng)求都需要,做統(tǒng)一處理疟呐。

? 接下來(lái)要處理的就是利用接口中的參數(shù)生成Header和sign配置到請(qǐng)求中去脚曾。而注解的方式的實(shí)現(xiàn),需要能在設(shè)置請(qǐng)求時(shí)將java方法和參數(shù)都獲取到启具。 所以如果在攔截器中處理本讥,那么攔截器需要獲取到調(diào)用的方法和參數(shù)。在Retrofit v2.4.0及之前的版本都不能在攔截器中獲取到任何方法的調(diào)用信息鲁冯,需要對(duì)源碼做一定的修改拷沸。

Retrofit源碼修改

這里針對(duì)v2.4.0版本做修改 retrofit-2.4.0

查找Request生成的代碼

ServiceMethod.toCall 方法中

  /** Builds an HTTP request from method arguments. */
  okhttp3.Call toCall(@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 callFactory.newCall(requestBuilder.build());
  }

添加接口

ServiceMethod.toCall 這個(gè)方法中雖然生成了請(qǐng)求的Request對(duì)象, 但是只傳了方法調(diào)用的參數(shù)進(jìn)來(lái), 并沒(méi)有方法提供給我們處理簽名字段∈硌荩看源碼的話其實(shí)ServiceMethod.Builder 中是有原始的java方法的.

final class ServiceMethod<R, T> {
  static final class Builder<T, R> {
    final Retrofit retrofit;
    final Method method;
  }
}

添加一個(gè)Method字段到ServiceMethod, toCall就可以獲取到原始方法了撞芍。然后筆者將toCall的return語(yǔ)句修改如下:

Request request = requestBuilder.build();
if (callParamsInjector != null) {
  request = callParamsInjector.onInject(request, mJavaMethod, args);
}
return callFactory.newCall(request);

設(shè)計(jì)接口:

public interface CallParamsInjector {
    /**
     * Call {@link Request} creating, inject something to {@link Request}.
     */
    Request onInject(Request request, Method method, Object... args);
}

添加Retrofit.Builder.parameterInjector(CallParamsInjector), ServiceMethod中的callParamsInjector從Retrofit對(duì)象中來(lái)(查看Retrofit源碼,這里簡(jiǎn)單描述)。

進(jìn)行簽名

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Sign {
  // 為不同類型的參數(shù)提供轉(zhuǎn)換, 如float只要兩位精度
  SignConverter value() default SignConverter.TOSTRING;

  enum Converter {
    TOSTRING {
      @Override
      String apply(Object value) {
        return String.valueOf(value);
      }
    },
    Float2 {
      @Override
      String apply(Object value) {
        if (value instanceof Float)
          return String.format("%.2f", value);
        return TOSTRING.apply(value);
      }
    };

    abstract String apply(Object value);
  }
}

// 該注解標(biāo)注的方法, 所有參數(shù)進(jìn)行簽名
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SignAll {
}

public class SignInjector implements CallParamsInjector {

  @Override
  public Request onInject(Request request, Method method, Object... args) {
    ArrayList<String> signParams = parseAnnotations(method, args);
    String[] params = SignUtil.defaultParams();
    Collections.addAll(signParams, params);
    String sign = SignUtil.sign(signParams);
    HttpUrl httpUrl = request.url().newBuilder()
      .addQueryParameter("sign", sign)
      .build();
    return request.newBuilder().url(httpUrl)
      .addHeader("APP-PARAMS", SignUtil.genHeader(params))
      .build();
  }

  private ArrayList<String> parseAnnotations(Method method, Object[] args) {
    ArrayList<String> signParams = new ArrayList<>();
    if(method.getAnnotation(SignAll.class) != null){
      for(Object arg : args)
        signParams.add(String.valueOf(arg));
      return signParams;
    }

    // not null
    Annotation[][] parameterAnnotations = method.getParameterAnnotations();
    loop: 
    for (int i = 0; i < parameterAnnotations.length; i++) {
      if (parameterAnnotations[i].length == 1) {
        continue;
      }
      for (Annotation annotation : parameterAnnotations[i]) {
        if (annotation instanceof Sign) {
          Sign.Converter converter = ((Sign) annotation).value();
          signParams.add(converter.apply(args.get(i)));
          continue loop;//抑制多個(gè)@Sign
        }
      }
    }
    return signParams;
  }
}

將SignInjector對(duì)象設(shè)置到Retrofit.Builder中即可跨扮。

注意

對(duì)retrofit修改后, gson-converter, adapter-rxjava2等也不要使用遠(yuǎn)程倉(cāng)庫(kù)的, 不然依賴可能會(huì)有問(wèn)題

Retrofit 2.5.0

當(dāng)準(zhǔn)備升級(jí)Retrofit到2.5.0時(shí)序无,發(fā)現(xiàn)2.5.0做了不小的改動(dòng),找到創(chuàng)建Request的方法

okhttp3.Request create(Object[] args) throws IOException {
  //...
  return requestBuilder.get()
    .tag(Invocation.class, new Invocation(method, argumentList))
    .build();
}

RequestFactory.create#L92 他將接口調(diào)用的方法和參數(shù)列表包裹到Invocation 對(duì)象中好港,放在了okhttp3.Request.Builder 的tag里了愉镰。簽名的首選也是攔截器,限于2.4.0前無(wú)法獲得方法和參數(shù)列表钧汹。而使用Retrofit 2.5.0只需要在攔截器中獲取到Invocation 對(duì)象丈探,后續(xù)的簽名如上文所述。然而這個(gè)功能似乎沒(méi)有現(xiàn)在其change log 中寫出來(lái)拔莱。

小結(jié)

Retrofit是一個(gè)非常好的開(kāi)源網(wǎng)絡(luò)請(qǐng)求框架碗降,非常值得研究。筆者為求實(shí)現(xiàn)而修改的拙劣代碼在優(yōu)雅的Request.Builder.tag 面前不值一提塘秦。謹(jǐn)以此短文來(lái)記錄與大神的差距讼渊。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市尊剔,隨后出現(xiàn)的幾起案子爪幻,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,729評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件挨稿,死亡現(xiàn)場(chǎng)離奇詭異仇轻,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)奶甘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門篷店,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人臭家,你說(shuō)我怎么就攤上這事疲陕。” “怎么了钉赁?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,461評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵术瓮,是天一觀的道長(zhǎng)蝎困。 經(jīng)常有香客問(wèn)我轮锥,道長(zhǎng)试吁,這世上最難降的妖魔是什么肴茄? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,135評(píng)論 1 300
  • 正文 為了忘掉前任店展,我火速辦了婚禮挥唠,結(jié)果婚禮上液走,老公的妹妹穿的比我還像新娘医吊。我一直安慰自己钱慢,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,130評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布卿堂。 她就那樣靜靜地躺著束莫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪草描。 梳的紋絲不亂的頭發(fā)上览绿,一...
    開(kāi)封第一講書(shū)人閱讀 52,736評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音穗慕,去河邊找鬼饿敲。 笑死,一個(gè)胖子當(dāng)著我的面吹牛逛绵,可吹牛的內(nèi)容都是我干的怀各。 我是一名探鬼主播,決...
    沈念sama閱讀 41,179評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼术浪,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼瓢对!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起胰苏,我...
    開(kāi)封第一講書(shū)人閱讀 40,124評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤硕蛹,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體法焰,經(jīng)...
    沈念sama閱讀 46,657評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡僵腺,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,723評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了壶栋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片辰如。...
    茶點(diǎn)故事閱讀 40,872評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖贵试,靈堂內(nèi)的尸體忽然破棺而出琉兜,到底是詐尸還是另有隱情,我是刑警寧澤毙玻,帶...
    沈念sama閱讀 36,533評(píng)論 5 351
  • 正文 年R本政府宣布豌蟋,位于F島的核電站,受9級(jí)特大地震影響桑滩,放射性物質(zhì)發(fā)生泄漏梧疲。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,213評(píng)論 3 336
  • 文/蒙蒙 一运准、第九天 我趴在偏房一處隱蔽的房頂上張望幌氮。 院中可真熱鬧,春花似錦胁澳、人聲如沸该互。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,700評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)宇智。三九已至,卻和暖如春胰丁,著一層夾襖步出監(jiān)牢的瞬間随橘,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,819評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工锦庸, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留机蔗,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,304評(píng)論 3 379
  • 正文 我出身青樓酸员,卻偏偏與公主長(zhǎng)得像蜒车,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子幔嗦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,876評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容