天天看點

學習筆記| OkHttp+Retrofit+RxJava 實作過期Token自動重新整理一、添加依賴二、更新token子產品三、依賴于Token的資料擷取子產品四、總結五、更多

在經曆了OkHttp、Retrofit、RxJava的學習後,終于可以開始寫代碼rua!

附架構學習筆記:學習筆記| OkHttp+Retrofit+Dagger2+RxJava+MVP架構

由于網絡上安利這幾款火的不行的架構的部落格實在是太多太多太多了,介紹、優缺點之類的廢話就不多說了,這裡隻介紹下關系。

  • Retrofit:Retrofit是Square公司開發的一款針對Android 網絡請求的架構(底層預設是基于OkHttp 實作)。
  • OkHttp:也是Square公司的一款開源的網絡請求庫。
  • RxJava :“a library for composing asynchronous and event-based programs using observable sequences for the Java VM”(一個在 Java VM 上使用可觀測的序列來組成異步的、基于事件的程式的庫)。RxJava使異步操作變得非常簡單。

各自職責:Retrofit 負責 請求的資料 和 請求的結果,使用 接口的方式 呈現,OkHttp 負責請求的過程,RxJava 負責異步,各種線程之間的切換。

一、添加依賴

在build.gradle檔案中添加如下配置:

// rxjava
    implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
    implementation 'io.reactivex.rxjava2:rxjava:2.2.3'
	// retrofit
    implementation 'com.squareup.retrofit2:retrofit:2.4.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
    implementation 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
    // okhttp
    implementation 'com.squareup.okhttp3:okhttp:3.11.0'
    //gson
    implementation 'com.google.code.gson:gson:2.8.5'
           

二、更新token子產品

有關JWT的知識可以看一下大神的部落格:JSON Web Token 入門教程 - 阮一峰

重新整理tokenAPI

學習筆記| OkHttp+Retrofit+RxJava 實作過期Token自動重新整理一、添加依賴二、更新token子產品三、依賴于Token的資料擷取子產品四、總結五、更多

實作思路

利用 Observale 的 retryWhen 的方法,識别 token 過期失效的錯誤資訊,此時發出重新整理 token 請求的代碼塊,完成之後更新 token,這時之前的請求會重新執行,但将它的 token 更新為最新的。另外通過代理類對所有的請求都進行處理,完成之後,我們隻需關注單個 API 的實作,而不用每個都考慮 token 過期,大大地實作解耦操作。

Token 存儲子產品

存儲token使用的是SharedPreferences + 單例模式 避免并發請求行為

public class Store {
    private SharedPreferences mStore;
	// 單例模式
    private Store(){
        mStore = App.getContext().getSharedPreferences(App.MY_SP_NAME, Context.MODE_PRIVATE);
    }

    public static Store getInstance() {
        return Holder.INSTANCE;
    }

    private static final class Holder {
        private static final Store INSTANCE = new Store();
    }

    public void setToken(String token) {
        mStore.edit().putString(App.USER_TOKEN_KEY, token).apply();
    }

    public String getToken() {
        return mStore.getString(App.USER_TOKEN_KEY, "");
    }
}
           

完整的Token請求過程

我将token請求提取到retrofit service層友善全局調用,使用直接傳回一個observable對象,對其訂閱在觀察者裡實作攜帶token的請求資料操作。并且這裡特意沒有用lambda表達式寫,對于了解會友善很多

/**
     * 擷取新的Token
     */
    private static final String ERROR_TOKEN = "error_token";
    private static final String ERROR_RETRY = "error_retry";
    public static Observable<String> getNewToken() {
        return Observable.defer(new Callable<ObservableSource<String>>() {
            @Override
            public ObservableSource<String> call() throws Exception {
                OkHttpClient client = new OkHttpClient();
                MediaType mediaType = MediaType.parse("text/x-markdown; charset=utf-8");
                String requestBody = "";
                Request request = new Request.Builder()
                        .url(NetConfig.BASE_GETNEWTOKEN_PLUS)
                        .header("Authorization", "Bearer " + Store.getInstance().getToken())
                        .post(RequestBody.create(mediaType, requestBody))
                        .build();
                Log.e("print","發起Token請求");
                return Observable.just(client.newCall(request).execute().body().string());
            }
        })
                // Token判斷
                .flatMap(new Function<String, ObservableSource<String>>() {
                    @Override
                    public ObservableSource<String> apply(String s) throws Exception {
                        return Observable.create(new ObservableOnSubscribe<String>() {
                            @Override
                            public void subscribe(ObservableEmitter<String> emitter) {
                                JSONObject obj = JSON.parseObject(s);
                                if (obj.getInteger("code") != 20000) {
                                    emitter.onError(new Throwable(ERROR_RETRY));
                                } else {
                                    String token = obj.getString("result");
                                    Store.getInstance().setToken(token);
                                    emitter.onNext(token);
                                }
                            }
                        });
                    }
                })
                // flatMap若onError進入retrywhen,否則onNext()
                .retryWhen(new Function<Observable<Throwable>, ObservableSource<?>>() {
                    private int mRetryCount = 0;

                    @Override
                    public ObservableSource<?> apply(Observable<Throwable> throwableObservable) {
                        return throwableObservable.flatMap(new Function<Throwable, ObservableSource<?>>() {
                            @Override
                            public ObservableSource<?> apply(Throwable throwable) throws Exception {
                                if (mRetryCount++ < 3 && throwable.getMessage().equals(ERROR_TOKEN))
                                    return Observable.error(new Throwable(ERROR_RETRY));
                                return Observable.error(throwable);
                            }
                        });
                    }
                });
    }
           

為了友善大家閱讀,我把所有的邏輯都寫在了一整個調用鍊裡,整個調用鍊分為三個部分:

  1. defer:讀取緩存中的token資訊,這裡調用了TokenLoader中讀取緩存的接口,而這裡使用defer操作符,是為了在重訂閱時,重新建立一個新的Observable,以讀取最新的緩存token資訊,其原理圖如下:
    學習筆記| OkHttp+Retrofit+RxJava 實作過期Token自動重新整理一、添加依賴二、更新token子產品三、依賴于Token的資料擷取子產品四、總結五、更多
  2. flatMap:通過token資訊,請求必要的接口。
  3. retryWhen:使用重訂閱的方式來處理token失效時的邏輯,這裡分為三種情況:重試次數到達,那麼放棄重訂閱,直接傳回錯誤;請求token接口,根據token請求的結果決定是否重訂閱;其它情況直接放棄重訂閱。

三、依賴于Token的資料擷取子產品

在這裡,我選擇抽離出項目中的擷取使用者資訊子產品進行代碼重構示範

擷取使用者資訊API

學習筆記| OkHttp+Retrofit+RxJava 實作過期Token自動重新整理一、添加依賴二、更新token子產品三、依賴于Token的資料擷取子產品四、總結五、更多
學習筆記| OkHttp+Retrofit+RxJava 實作過期Token自動重新整理一、添加依賴二、更新token子產品三、依賴于Token的資料擷取子產品四、總結五、更多

我選擇在将個人資訊請求寫在了觀察者的方法裡,再次嵌套一個鍊式結構

public synchronized void getUserAvator() {
		// 建立被觀察者的執行個體
        Observable<String> observable = RetrofitService.getNewToken();
		// 定義觀察者
        DisposableObserver<String> observer = new DisposableObserver<String>() {
            @Override
            public void onNext(String s) {
            	// 發起使用者資訊請求
                Observable.create((ObservableOnSubscribe<String>) emitter -> {
                    OkHttpClient client = new OkHttpClient();
                    Request request = new Request.Builder()
                            .url(NetConfig.BASE_USERDETAIL_PLUS)
                            .header("Authorization", "Bearer " + Store.getInstance().getToken())
                            .get()
                            .build();
                    String response = client.newCall(request).execute().body().string();
                    emitter.onNext(response);
                })
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(responseString -> {
                            if (!responseString.contains("result")){
                                printLog("HomeFragment_getAvatar_subscribe:擷取使用者資訊出錯 需要處理");
                                return;
                            }
                            JSONObject jsonObject = JSON.parseObject(responseString);
                            String path = "";
                            JSONObject obj = JSON.parseObject(jsonObject.getString("result"));
                            path = obj.getString("avatar");
                            Picasso.get()
                                    .load(path)
                                    .placeholder(R.drawable.image_placeholder)
                                    .into(ciHomeImg);
                        }, throwable -> printLog("HomeFragment_getAvatar_subscribe_onError:" + throwable.getMessage()));

            }

            @Override
            public void onError(Throwable e) {
                Log.e("print", "HomeFragment_getUserAvator_onError: " + e.getMessage());
            }

            @Override
            public void onComplete() {

            }
        };
        // 進行訂閱
        observable.subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(observer);
    }
           

四、總結

這種實作方法實際上每次從伺服器擷取資訊執行順序是:更新Token → 擷取資料資訊,這樣會向伺服器産生過多不必要的請求,加大伺服器的負擔。是以正确的請求姿勢應該是:發起資料請求 → 傳回token過期資訊 → 發送更新token請求 → 傳回new token → 再次發起資料請求,可以有效地減輕伺服器的負擔,可以在上面的基礎上再次封裝token service服務達到這樣的效果。

五、更多

在 coding 前參考了很多部落格,推薦幾篇好的文章

defer操作符實作代碼支援鍊式調用 - Chiclaim

retryWhen操作符實作錯誤重試機制 - Chiclaim

在 token 過期時,重新整理過期 token 并重新發起請求 - 澤毛