一起來寫OKHttp的攔截器

00:00

一開始就不多說廢話了,主要因爲工作時遇到了一些使用 OKHttp 攔截器的問題,所以在此特寫這篇以作記錄。

現如今,做 Android 開發在選擇網絡框架時,大多數都會首推 Retrofit 。Retrofit 以其簡潔優雅的代碼俘獲了大多數開發者的心。

然而 Retrofit 內部請求也是基於 OKHttp 的,所以在做一些自定義修改 HTTP 請求時,需要對 OKHttp 攔截器具有一定了解。相信熟悉 OKHttp 的同學都知道,OKHttp 內部是使用攔截器來完成請求和響應的,利用的是責任鏈設計模式。所以可以說,攔截器是 OKHttp 的精髓所在。

那麼接下來,我們就通過一些例子來學習怎樣編寫 OKHttp 的攔截器吧,其實這些例子也正是之前我遇到的情景。

00:01

添加請求 Header

假設現在後臺要求我們在請求 API 接口時,都在每一個接口的請求頭上添加對應的 token 。使用 Retrofit 比較多的同學肯定會條件反射出以下代碼:

1
2
3
@FormUrlEncoded
@POST("/mobile/login.htm")
Call<ResponseBody> login(@Header("token") String token, @Field("mobile") String phoneNumber, @Field("smsCode") String smsCode);

這樣的寫法自然可以,無非就是每次調用 login API 接口時都把 token 傳進去而已。但是需要注意的是,假如現在有十多個 API 接口,每一個都需要傳入 token ,難道我們去重複一遍又一遍嗎?

相信有良知的程序員都會拒絕,因爲這會導致代碼的冗餘。

那麼有沒有好的辦法可以一勞永逸呢?答案是肯定的,那就要用到攔截器了。

代碼很簡單:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TokenHeaderInterceptor implements Interceptor {

    @Override
    public Response intercept(Chain chain) throws IOException {
        // get token
        String token = AppService.getToken();
        Request originalRequest = chain.request();
        // get new request, add request header
        Request updateRequest = originalRequest.newBuilder()
                .header("token", token)
                .build();
        return chain.proceed(updateRequest);
    }

}

我們先攔截得到 originalRequest ,然後利用 originalRequest 生成新的 updateRequest ,再交給 chain 處理進行下一環。

最後,在 OKHttpClient 中使用:

1
2
3
4
5
6
OkHttpClient client = new OkHttpClient.Builder()
                .addNetworkInterceptor(new TokenHeaderInterceptor())
                .build();

Retrofit retrofit = new Retrofit.Builder().baseUrl(BuildConfig.BASE_URL)
                .client(client).addConverterFactory(GsonConverterFactory.create()).build();

改變請求體

除了增加請求頭之外,攔截器還可以改變請求體。

假設現在我們有如下需求:在上面的 login 接口基礎上,後臺要求我們傳過去的請求參數是要按照一定規則經過加密的。

規則如下:

  • 請求參數名統一爲content;
  • content值:JSON 格式的字符串經過 AES 加密後的內容;

舉個例子,根據上面的 login 接口,現有

1
{"mobile":"157xxxxxxxx", "smsCode":"xxxxxx"}

JSON 字符串,然後再將其加密。最後以 content=[加密後的 JSON 字符串] 方式發送給後臺。

看完了上面的 TokenHeaderInterceptor 之後,這需求對於我們來說可以算是信手拈來:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class RequestEncryptInterceptor implements Interceptor {

    private static final String FORM_NAME = "content";
    private static final String CHARSET = "UTF-8";

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        RequestBody body = request.body();
        if (body instanceof FormBody) {
            FormBody formBody = (FormBody) body;
            Map<String, String> formMap = new HashMap<>();
            // 從 formBody 中拿到請求參數,放入 formMap 中
            for (int i = 0; i < formBody.size(); i++) {
                formMap.put(formBody.name(i), formBody.value(i));
            }
            // 將 formMap 轉化爲 json 然後 AES 加密
            Gson gson = new Gson();
            String jsonParams = gson.toJson(formMap);
            String encryptParams = AESCryptUtils.encrypt(jsonParams.getBytes(CHARSET), AppConstant.getAESKey());
            // 重新修改 body 的內容
            body = new FormBody.Builder().add(FORM_NAME, encryptParams).build();
        }
        if (body != null) {
            request = request.newBuilder()
                    .post(body)
                    .build();
        }
        return chain.proceed(request);
    }
}

代碼中已經添加了關鍵的註釋,相信我已經不需要多解釋什麼了。

經過了這兩種攔截器,相信同學們已經充分體會到了 OKHttp 的優點和與衆不同。

最後,自定義攔截器的使用情景通常是對所有網絡請求作統一處理。如果下次你也碰到這種類似的需求,別忘記使用自定義攔截器哦!

00:02

呃呃呃,按道理來講應該要結束了。

但是,我在這裏開啓一個番外篇吧,不過目標不是針對攔截器而是 ConverterFactory 。

還是後臺需求,login 接口返回的數據也是經過 AES 加密的。所以需要我們針對所有響應體都做解密處理。

另外,還有很重要的一點,就是數據正常和異常時返回的 JSON 格式不一致。

在業務數據正常的時候(即 code 等於 200 時):

1
2
3
4
5
6
7
8
{
    "code":200,
    "msg":"請求成功",
    "data":{
        "nickName":"Hello",
        "userId": "1234567890"
    }
}

業務數據異常時(即 code 不等於 200 時):

1
2
3
4
5
{
    "code":7008,
    "msg":"用戶名或密碼錯誤",
    "data":"用戶名或密碼錯誤"
}

而這會在使用 Retrofit 自動從 JSON 轉化爲 bean 類時報錯。因爲 data 中的正常數據中是 JSON ,而另一個異常數據中是字符串。

那麼,如何解決上述的兩個問題呢?

利用 自定義 ConverterFactory !!

我們先創建包名 retrofit2.converter.gson ,爲什麼要創建這個包名呢?

因爲自定義的 ConverterFactory 需要繼承 Converter.Factory ,而 Converter.Factory 類默認是包修飾符。

代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public final class CustomConverterFactory extends Converter.Factory {

    private final Gson gson;

    public static CustomConverterFactory create() {
        return create(new Gson());
    }

    @SuppressWarnings("ConstantConditions") // Guarding public API nullability.
    public static CustomConverterFactory create(Gson gson) {
        if (gson == null) throw new NullPointerException("gson == null");
        return new CustomConverterFactory(gson);
    }
   
    private CustomConverterFactory(Gson gson) {
        this.gson = gson;
    }

    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
        TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
        // attention here!
        return new CustomResponseConverter<>(gson, adapter);
    }

    @Override
    public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
        TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
        return new GsonRequestBodyConverter<>(gson, adapter);
    }

}

從代碼中可知,CustomConverterFactory 內部是根據 CustomResponseConverter 來轉化 JSON 的,這纔是我們的重點。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class CustomResponseConverter<T> implements Converter<ResponseBody, T> {

    private final Gson gson;
    private final TypeAdapter<T> adapter;
    private static final String CODE = "code";
    private static final String DATA = "data";

    CustomResponseConverter(Gson gson, TypeAdapter<T> adapter) {
        this.gson = gson;
        this.adapter = adapter;
    }

    @Override
    public T convert(ResponseBody value) throws IOException {
        try {
            String originalBody = value.string();
            // 先 AES 解密
            String body = AESCryptUtils.decrypt(originalBody, AppConstant.getAESKey());
            // 再獲取 code 
            JSONObject json = new JSONObject(body);
            int code = json.optInt(CODE);
            // 當 code 不爲 200 時,設置 data 爲 null,這樣轉化就不會出錯了
            if (code != 200) {
                Map<String, String> map = gson.fromJson(body, new TypeToken<Map<String, String>>() {
                }.getType());
                map.put(DATA, null);
                body = gson.toJson(map);
            }
            return adapter.fromJson(body);
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        } finally {
            value.close();
        }
    }
}

代碼也是很簡單的,相信也不需要解釋了。o(∩_∩)o

最後就是使用了 CustomConverterFactory :

1
2
3
4
5
6
OkHttpClient client = new OkHttpClient.Builder()
                .addNetworkInterceptor(new TokenHeaderInterceptor())
                .addInterceptor(new RequestEncryptInterceptor())
                .build();
Retrofit retrofit = new Retrofit.Builder().baseUrl(BuildConfig.BASE_URL)
                .client(client).addConverterFactory(CustomConverterFactory.create()).build();

好了,這下真的把該講的都講完了,大家可以散了。

完結了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章