OkHttp3.0(結合Retrofit2/Rxjava)利用攔截器實現全域性超時自動登入、新增統一引數
阿新 • • 發佈:2019-02-19
應用場景: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、新增的引數)可能看在日誌裡就看不到了