1. 程式人生 > >OkHttp3.0(結合Retrofit2/Rxjava)利用攔截器實現全域性超時自動登入、新增統一引數

OkHttp3.0(結合Retrofit2/Rxjava)利用攔截器實現全域性超時自動登入、新增統一引數


應用場景:1.服務端為了統計各個平臺、版本的使用情況,有時在介面中要求傳遞統一的諸如version(客戶端版本)、os(客戶端平臺android/iOS)、userId等引數,這時如果在介面中一一新增就比較繁瑣了,考慮做全域性處理;另外,一次登入成功後,登入狀態都是有時效的,所以在發生登入失效後,需要自動重新重新整理登入狀態,而且一般情況下,單個請求在發出前是沒法判斷是否已經登入超時的,所以就需要一個全域性的處理方案。

其實這個與Retrofit2/Rxjava貌似沒有關係,之所以標題裡提到這個,是因為我的專案是結合這倆庫用的,我在搜尋這類問題的解決方法時就是從Retrofit2/Rxjava的retryWhen方法下手的,後來發現直接在OkHttpClient新增攔截器,即可實現想要的效果,而且是全域性性的,這應該是這類問題的最簡單解決方式了。

主要程式碼如下:
Network.java

import android.text.TextUtils;
import android.util.Log;

import com.google.gson.Gson;
import com.google.gson.JsonObject;

import org.greenrobot.eventbus.EventBus;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.nio.charset.Charset;
import
java.util.ArrayList; import java.util.List; import okhttp3.Cookie; import okhttp3.CookieJar; import okhttp3.FormBody; import okhttp3.HttpUrl; import okhttp3.Interceptor; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import
okhttp3.ResponseBody; import okhttp3.logging.HttpLoggingInterceptor; import okio.BufferedSource; import retrofit2.Call; import retrofit2.CallAdapter; import retrofit2.Converter; import retrofit2.Retrofit; import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory; import retrofit2.converter.gson.GsonConverterFactory; public class Network { private static final String TAG = "Network"; private static APIService apis; private static ReLoginService reLoginService; private static Converter.Factory mExtraGsonConverterFactory = ExtraGsonConverterFactory.create(); private static CallAdapter.Factory rxJavaCallAdapterFactory = RxJavaCallAdapterFactory.create(); public static APIService getAPIService() { if (apis == null) { // TODO 最後關閉日誌 HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(); loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); // OkHttp3.0的使用方式 OkHttpClient okHttpClient = new OkHttpClient.Builder() .retryOnConnectionFailure(true) .addInterceptor(new Interceptor() { @Override public Response intercept(Chain chain) throws IOException { Request original = chain.request(); Request.Builder requestBuilder = original.newBuilder(); if (original.body() instanceof FormBody) { FormBody.Builder newFormBody = new FormBody.Builder(); FormBody oldFormBody = (FormBody) original.body(); for (int i = 0; i < oldFormBody.size(); i++) { newFormBody.addEncoded(oldFormBody.encodedName(i), oldFormBody.encodedValue(i)); } newFormBody.add("os", "android"); requestBuilder.method(original.method(), newFormBody.build()); } else if (original.body() instanceof MultipartBody) { MultipartBody.Builder newFormBody = new MultipartBody.Builder(); // 預設是multipart/mixed,大坑【主要是我們php後臺接收時頭資訊要求嚴格】 newFormBody.setType(MediaType.parse("multipart/form-data")); MultipartBody oldFormBody = (MultipartBody) original.body(); for (int i = 0; i < oldFormBody.size(); i++) { newFormBody.addPart(oldFormBody.part(i)); } newFormBody.addFormDataPart("os", "android"); requestBuilder.method(original.method(), newFormBody.build()); } else if (TextUtils.equals(original.method(), "POST")) { FormBody.Builder newFormBody = new FormBody.Builder(); newFormBody.add("os", "android"); requestBuilder.method(original.method(), newFormBody.build()); } Request request = requestBuilder.build(); return chain.proceed(request); } }) .addInterceptor(new Interceptor() { @Override public Response intercept(final Chain chain) throws IOException { // 原始請求 Request request = chain.request(); Response response = chain.proceed(request); ResponseBody responseBody = response.body(); BufferedSource source = responseBody.source(); source.request(Long.MAX_VALUE); String respString = source.buffer().clone().readString(Charset.defaultCharset()); Log.d(TAG, "--->返回報文,respString = " + respString); // TODO 這裡判斷是否是登入超時的情況 JSONObject j = null; try { j = new JSONObject(respString); } catch (JSONException e) { e.printStackTrace(); } // 這裡與後臺約定的狀態碼700表示登入超時【後臺是java,客戶端自己維護cookie,沒有token機制。但此處如果重新整理token,方法也一樣】 if (j!= null && j.optInt("status") == 700) { Log.d(TAG, "--->登入失效,自動重新登入"); // TODO 本地獲取到之前的user資訊 UserInfo user = SysApplication.getInstance().getDB().getCurrentUser(); if (user == null) { Log.d(TAG, "--->使用者為空需要使用者主動去登入"); // 扔出需要手動重新登入的異常(BaseSubscriber裡處理) throw new ExtraApiException(700, "請登入"); } String phoneNum = user.getPhoneNum(); String password = user.getPass(); Call<JsonObject> call = getReloginService().reLogin(phoneNum, password); JsonObject json = call.execute().body(); // 判斷是否登入成功了 if (json.get("status").getAsInt() == 200) { // TODO 登入成功後,根據需要儲存使用者資訊、會話資訊等 // 最重要的是將當前請求重新執行一遍!!! response = chain.proceed(request); Log.d(TAG, "--->完成二次請求"); } else { Log.d(TAG, "--->自動登入失敗"); // TODO 扔出需要手動重新登入的異常(BaseSubscriber裡處理,此時已經是自動重新登入也不行,如密碼在其他終端修改了之類的) throw new ExtraApiException(700, "請重新登入"); } } return response; } }) .cookieJar(new CookieJar() { List<Cookie> cookies; @Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) { // TODO 根據實際後臺返回資訊,儲存cookies } @Override public List<Cookie> loadForRequest(HttpUrl url) { return cookies; } }) .addInterceptor(loggingInterceptor) .build(); Retrofit retrofit = new Retrofit.Builder() .baseUrl(Constant.HOST_APP) .addConverterFactory(mExtraGsonConverterFactory) .addCallAdapterFactory(rxJavaCallAdapterFactory) .client(okHttpClient) .build(); apis = retrofit.create(APIService.class); } return apis; } // 重新登入之所以單獨寫在一個interface中,是因為其他所有方法都採用了自定義Gson解析器,在解析器裡就處理了最外層的status,只獲取有效資訊 // 重新登入時,需要自己重新處理,並儲存cookie等資訊 public static ReLoginService getReloginService() { if (reLoginService == null) { // TODO 最後關閉日誌 HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(); loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); // OkHttp3.0的使用方式 OkHttpClient okHttpClient = new OkHttpClient.Builder() .addInterceptor(loggingInterceptor) .cookieJar(new CookieJar() { @Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) { // TODO } @Override public List<Cookie> loadForRequest(HttpUrl url) { return new ArrayList<>(); } }) .build(); Retrofit retrofit = new Retrofit.Builder() .baseUrl(Constant.HOST_APP) .addConverterFactory(GsonConverterFactory.create()) .client(okHttpClient) .build(); reLoginService = retrofit.create(ReLoginService.class); } return reLoginService; } }

ExtraGsonResponseBodyConverter.java這個是自定義解析器,作用是去除服務端返回的最外層資訊,只保留內部有效資訊體。另外可以統一處理返回的status,丟擲自定義異常,然後在全域性統一的Subscriber中onError處理。

import android.text.TextUtils;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.Charset;

import okhttp3.MediaType;
import okhttp3.ResponseBody;
import retrofit2.Converter;

import static okhttp3.internal.Util.UTF_8;

final class ExtraGsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
    private final Gson gson;
    private final TypeAdapter<T> adapter;

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

    @Override
    public T convert(ResponseBody value) throws IOException {
        try {
            JSONObject response = new JSONObject(value.string());


            // 結果狀態不對的,統一丟擲異常,進入Subscriber的onError回撥函式
            if (response.optInt("status") != 200) {
                value.close();
                throw new ExtraApiException(response.optInt("status"), response.optString("message"));
            }

            // 後臺返回不統一、不規範,客戶端來背鍋處理……
            String info = response.optString("json");
            if (TextUtils.isEmpty(info)) {
                info = response.optString("resultList");
            }
            if (TextUtils.isEmpty(info) || TextUtils.equals(info.toLowerCase(), "null")) {
                info = "{}";
            }

            MediaType contentType = value.contentType();
            Charset charset = contentType != null ? contentType.charset(UTF_8) : UTF_8;


            InputStream inputStream = new ByteArrayInputStream(info.getBytes());
            Reader reader = new InputStreamReader(inputStream, charset);
            JsonReader jsonReader = gson.newJsonReader(reader);

            return adapter.read(jsonReader);
        } catch (JSONException e) {
            throw new IOException();
        } finally {
            value.close();
        }
    }
}

ReLoginService.java用於單獨進行重新登入請求,保留了服務端返回的所有資訊體。

import com.google.gson.JsonObject;

import retrofit2.Call;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.POST;


public interface ReLoginService {

    /**
     * 登入
     *
     * @param name 使用者名稱
     * @param pass ,密碼
     */
    @FormUrlEncoded
    @POST("msLogin")
    Call<JsonObject> reLogin(
            @Field("name") String name,
            @Field("pass") String pass
    );
}

至此,問題解決,主要思路就是,利用OkHttpClient的OkHttpClient方法:

  • 將原有請求重新組裝
  • 將響應資訊預處理,如果對應的是登入失效,則進行重新登入,若登入成功就再次執行原請求;如登入不成功,則提示使用者自己去登入

此外,還有幾點說明:

  • 自定義ConverterFactory有利於將返回資訊中的無效資訊或過多層級統一處理掉,簡化單個請求成功後的處理邏輯
  • 自定義Exception有利於在全域性統一的Subscriber(結合RxJava)中處理異常
  • 用於輔助檢視請求資訊的HttpLoggingInterceptor,要到最後進行addInterceptor,不然你之前做的處理(cookie、新增的引數)可能看在日誌裡就看不到了