OkHttp+Retrofit+RxJava 實現過期Token自動重新整理

weixin_34128411發表於2019-02-24

在經歷了OkHttp、Retrofit、RxJava的學習後,終於可以開始寫程式碼rua!
附我的學習筆記:https://blog.csdn.net/qq_42895379/article/details/83786905#RxJava_221

由於網路上安利這幾款火的不行的框架的部落格實在是太多太多太多了,介紹、優缺點之類的廢話就不多說了,這裡只介紹下關係。

  • 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

16471224-70dd0dcc8f714404
重新整理tokenAPI

實現思路

利用 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資訊,其原理圖如下:
    1949836-75602a96deed77e4.png
    defer原理圖
  2. flatMap:通過token資訊,請求必要的介面。
  3. retryWhen:使用重訂閱的方式來處理token失效時的邏輯,這裡分為三種情況:重試次數到達,那麼放棄重訂閱,直接返回錯誤;請求token介面,根據token請求的結果決定是否重訂閱;其它情況直接放棄重訂閱。

三、依賴於Token的資料獲取模組

在這裡,我選擇抽離出專案中的獲取使用者資訊模組進行程式碼重構演示
獲取使用者資訊API

16471224-a0576d92a58c5315
獲取個人資訊API_1

16471224-bd63a55531f19286
獲取個人資訊API_2

我選擇在將個人資訊請求寫在了觀察者的方法裡,再次巢狀一個鏈式結構

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 並重新發起請求 - 澤毛

相關文章