OkHttp+Retrofit+RxJava 實現過期Token自動刷新

在經歷了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
刷新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信息,其原理圖如下:
    defer原理圖
  2. flatMap:通過token信息,請求必要的接口。
  3. retryWhen:使用重訂閱的方式來處理token失效時的邏輯,這裏分爲三種情況:重試次數到達,那麼放棄重訂閱,直接返回錯誤;請求token接口,根據token請求的結果決定是否重訂閱;其它情況直接放棄重訂閱。

三、依賴於Token的數據獲取模塊

在這裏,我選擇抽離出項目中的獲取用戶信息模塊進行代碼重構演示
獲取用戶信息API
獲取個人信息API_1
獲取個人信息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 並重新發起請求 - 澤毛

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