優雅地使用Retrofit+RxJava(二)

前言

在我上一篇講Retrofit+RxJava在MVP模式中優雅地處理異常(一)中,發現很是多網友發郵箱給我表示期待個人下一篇文章,正好趁着清明假期。我就寫寫平時我在使用RxJava+Retrofit怎麼去靈活地處理一些場景。比方說一些比較常見的場景:php

  • 網絡請求過程當中token的處理
  • 網絡請求數據的加密與解密
  • 爲每個請求加入固定的頭部。比方說當前版本,Rsa的密鑰等等
  • 規範化每個網絡請求,讓代碼僅僅寫一次

我本身平時對代碼的簡潔性要求很是高,因此retrofit+rxjava正好切中了個人痛點,這也是激發我寫這篇文章的緣由,我想要與你們一塊兒交流進步,可以看看個人代碼演示樣例css

一個簡單的演示樣例

(可以選擇先忽略,等看完這篇文章再回頭來看)java

/** * @author whaoming * github:https://github.com/whaoming * created at 2017/2/14 15:59 * Description:數據請求的管理類 */
public class HttpMethods {
    //retrofit相應的接口
    private ApiService myService;

    //構造方法私有
    private HttpMethods() {
        List<Interceptor> interceptors = new ArrayList<>();
        Map<String,String> headers = new HashMap<>();
        headers.put("userid",25);
        TokenGetInterceptor tokenGetInterceptor = new TokenGetInterceptor(headers);
        HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
        AESInterceptor aesInterceptor = new AESInterceptor();
         //建立一個http頭部處理器攔截器(這裏主要處理server返回token的捕獲)
        interceptors.add(tokenGetInterceptor );
        //日誌打印攔截器
        interceptors.add(loggingInterceptor );
        //數據的加密與解密攔截器
        interceptors.add(aesInterceptor);

        RetrofitHelper.getInstance().init(ConstantValue.SERVER_URL,interceptors );
        //建立service
        myService = RetrofitHelper.getInstance().createService(ApiService.class);
    }

    //依據id用戶一個用戶的信息
    public Observable<UserCommonInfo> getUserInfoById(int userid){
        return  Direct2.create(myService.getUserInfoById(userid),new TokenProviderImpl());
    }
}

/** * Created by Mr.W on 2017/2/14. * E-maiil:122627018@qq.com * github:https://github.com/whaoming * TODO: 依照建立者模式的思想。把一個訪問server的操做規格化 */
public class Direct {
   public static<T> Observable<T> create(Observable<Result<T>> resurce,TokenProvider tokenProvider){
       return resurce
               //解析固定格式json
               .map(new ResultParseInterceptor<T>())
               //處理token過時,tokenProvider爲當發現token過時時候詳細的處理方式
               .retryWhen(new TokenExpireInterceptor(tokenProvider))
               //捕獲整個請求過程當中的錯誤
               .onErrorResumeNext(new ErrorInterceptor<T>())
                .observeOn(AndroidSchedulers.mainThread())
               .subscribeOn(Schedulers.io());
   }
}

網絡層:RxJava+Retrofit

相對來講。retrofit+rxjava的學習成本仍是比較高的。git

舉個樣例,就拿數據打印來講,假設使用okHttp的話,可以直接在回調裏面打印server返回的json數據,但是放在retrofit中。因爲retrofit會本身主動幫你封裝成相應的bean,這使得數據解析這個過程不可見。須要經過retrofit的攔截器才幹實現,因此攔截器對於retrofit來講,是一個很是很是重要的東西。github

retrofit攔截器的使用場景

日誌攔截器

還記得剛開始使用retrofit的時候,就被這個功能嚇到了,大哥我僅僅是想簡單地打印下server給了我什麼數據,爲何要這麼麻煩啊。。!只是後面也愈來愈理解retrofit這樣作的緣由了,(我的愚見)這樣使得所有的操做都規範化。用我本身的話說。就是retrofit告訴你,僅僅要你想要」入侵」數據發送和解析的過程,不管是什麼操做,你就得給我使用攔截器。那麼事實上說難也不難。僅僅是幾行代碼而已:算法

HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
            @Override
            public void log(String message) {
                try {
                    String text = URLDecoder.decode(message, "utf-8");
                    Log.d("OKHttp", text);
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                    Log.d("OKHttp", message);
                }
            }
});
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient okHttpClient =builder.build();
mRetrofit = new Retrofit.Builder()
                .baseUrl(baseURL)
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .client(okHttpClient)
                .build();

token攔截器

token機制我相信大多數client都必須要有的一個東西,這裏咱們這個攔截器的工做是爲每個請求加入頭部,還有攔截server返回的頭信息裏面是否包括token,有的話取出並存在本地。先上代碼:json

/** * Created by Mr.W on 2017/2/6. * E-maiil:122627018@qq.com * github:https://github.com/whaoming * TODO: 攔截server返回的token並進行保存,並且在發起請求的時候本身主動爲頭部加入token */
public class TokenGetInterceptor implements Interceptor {

    private Map<String,String> headers = null;
    public TokenGetInterceptor(Map<String,String> headers){
        this.headers = headers;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request newRequest;
        if (headers!=null || !Account.isShortCookieEmpty()) {        
            Request.Builder builder = chain.request().newBuilder();
            if(headers!=null){
                for(Map.Entry<String,String> item : headers.entrySet()){
                    //加入一些其它頭部信息,好比appid,userid等。由外部傳入
                    builder.addHeader(item.getKey(),item.getValue());
                }
            }
            if (!Account.isShortCookieEmpty()) {
                builder.addHeader("token", Account.getShortCookie());
            }
            newRequest = builder.build();
        } else {
            newRequest = chain.request().newBuilder()
                    .build();
        }
        Response response = chain.proceed(newRequest);
        if (response.header("token") != null) {
            //發現短token。保存到本地
            Account.updateSCookie(response.header("token"));
        }
        String long_token = response.header("long_token");
        if (long_token != null) {
            //發現長token,保存到本地
            Account.updateLCookie(long_token);
        }
        return response;
    }
}
/** 什麼是長token,短token? 區分長token與短token的緣由是因爲倆種token的算法與生效時間不同。當發現短token過時的時候,client會帶上長token向server再次獲取短token。而後再又一次發起請求。固然每個系統的token機制均可能不同。這裏也可以看出retrofit可以很是靈活地處理很是多種狀況 */

那麼關於整個流程token的維護。包括髮現token過時以後,怎麼請求新token。怎麼又一次發起請求。這些操做retrofit要配合rxjava來實現。後面關於rxjava我會說到。markdown

加密解密攔截器

在這裏先簡單講一下個人加密機制,主要是經過rsa+aes,也就是client表單提交的數據,經過aes加密,而後aes的key再經過client本地保存的公鑰進行加密(此公鑰由server經過rsa算法生成,打包的時候保存在client本地)。把加密以後的key放在請求頭裏面,一塊兒發送給server。網絡

攔截器的代碼例如如下:app

/** * @author whaoming * github:https://github.com/whaoming * created at 2017/2/6 10:13 * Description:對錶單提交的數據進行aes加密 */
public class AESInterceptor implements Interceptor {

    public String key = "123456789aaaaaaa";
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        try {
            Request newRequest = null;
            if (request.body() instanceof FormBody) {
                //發現表單數據
                FormBody formBody = (FormBody) request.body();
                FormBody.Builder formBuilder = new FormBody.Builder();
                String keyMI = null;
                for (int i = 0; i < formBody.size(); i++) {
                    if (formBody.name(i).equals("param")) {
                        //對提交的表單數據進行加密
                        String json = AESUtil.encrypt(formBody.value(i), key);
                        if (!TextUtils.isEmpty(json)) {
                            formBuilder.add("data", json);
                            //對aes的key經過rsa公鑰加密
                            RSAPublicKey pk = RSAKeyProvider.loadPublicKeyByStr(AppContext.getPublicKeyStore());
                            keyMI = RSAUtils.encryptByPublicKey(key,pk);
                        }
                    }else{
                        formBuilder.addEncoded(formBody.encodedName(i), formBody.encodedValue(i));
                    }
                }
                FormBody newFormBody = formBuilder.build();
                Request.Builder builder = request.newBuilder();
                if(!TextUtils.isEmpty(keyMI)){
                    //將加密後的aes的key放在頭部
                    builder.header("key",keyMI);
                }
                newRequest = builder
                        .method(request.method(), newFormBody)
                        .removeHeader("Content-Length")
                        .addHeader("Content-Length", newFormBody.contentLength() + "")
                        .build();
            }
            Response response = chain.proceed(newRequest == null ? request : newRequest);
            String result = response.body().string();
            return response.newBuilder().body(ResponseBody.create(response.body().contentType(), result)).build();
        }catch (Exception e){
            e.printStackTrace();
        }
        return chain.proceed(request);
    }
}

Rxjava操做符的靈活使用

(ps:強烈建議讀第一篇文章後再繼續往下看:Retrofit+RxJava在MVP模式中優雅地處理異常(一)

返回數據的錯誤碼統一解析

這裏事實上就是第一篇博文的內容,傳送門:Retrofit+RxJava在MVP模式中優雅地處理異常(一)

錯誤攔截

這裏事實上也是在第一篇中講過的內容。主要就是利用RxJava的onErrorResumeNext操做符來作錯誤的攔截,可以使整個網絡訪問過程的錯誤都在一個地方解析。從而大大下降view層的工做量,並且使得view層與m層耦合度大大下降。靈活性提升,代碼量大大下降。

/** * Created by Mr.W on 2017/2/14. * E-maiil:122627018@qq.com * github:https://github.com/122627018 * TODO: 異常解析的一個攔截器 */
public class ErrorInterceptor<T> implements Func1<Throwable, Observable<T>> {
    @Override
    public Observable<T> call(Throwable throwable) {
        throwable.printStackTrace();
        //ExceptionProvider:一個錯誤解析器
        return Observable.error(ExceptionProvider.handleException(throwable));
    }
}

token過時處理

這裏的處理邏輯事實上還蠻複雜的,看看下圖(畫的比較醜,不要介意)
這裏寫圖片描寫敘述
在這裏可以使用RxJava的retryWhen操做符。先看看server返回的數據格式:

/** * 這是server返回數據的一個固定格式 * @author Mr.W */
public class Result<T> {
    public int state;
    public String error;
    public T infos;
}

那麼一個主要的流程是這種:
這裏寫圖片描寫敘述

因此retryWhen就可以在攔截錯誤的時候發揮做用,可以這樣理解retryWhen。當發現onError事件的時候,在retryWhen內部:

  • 返回一個新的Observable,會觸發又一次訂閱
  • 返回Observable.onError,會繼續原來的訂閱事件

當發現錯誤碼爲500的時候。調用傳入的接口(此接口用於token的又一次獲取)

/** * Created by Mr.W on 2017/2/14. * E-maiil:122627018@qq.com * github:https://github.com/122627018 * TODO: 短token過時的處理 */ public class TokenExpireInterceptor implements Func1<Observable<?

extends Throwable>, Observable<?>> { TokenProvider tokenProvider; public TokenExpireInterceptor(TokenProvider tokenProvider){ this.tokenProvider = tokenProvider; } @Override public Observable<?> call(Observable<?

extends Throwable> observable) { return observable.flatMap(new Func1<Throwable, Observable<?

>>() { @Override public Observable<?

> call(Throwable throwable) { if(throwable instanceof ServerException){ ServerException ex = (ServerException)throwable; if(ex.getCode() == 500){ //發現token過時標識,調用獲取token的接口 return tokenProvider.getToken(); } } return Observable.error(throwable); } }); } } /** * token又一次獲取的接口 * Created by Mr.W on 2017/2/14. * E-maiil:122627018@qq.com * github:https://github.com/122627018 */ public interface TokenProvider { Observable<String> getToken(); }

這樣就可以很是完美的處理了token過時的情景。關於token過時的處理

RxJava+Retrofit網絡訪問流程的規範化

好了。到這裏咱們總結一下上面咱們說到的點,那麼事實上每個點都是我本身的項目中實際使用到的,可以看看如下這個業務邏輯:

這裏寫圖片描寫敘述
可以看出。在發出網絡請求的時候的邏輯,都是由Retrofit的攔截器來實現的。那麼在處理請求結果的時候,都是由RxJava來實現的。因此,整個邏輯就很是清晰很是舒服了

/** * Created by Mr.W on 2017/2/14. * E-maiil:122627018@qq.com * github:https://github.com/whaoming * TODO: 依照建立者模式的思想,把一個處理請求結果的操做流程化 */
public class Direct {
   public static<T> Observable<T> create(Observable<Result<T>> resurce,TokenProvider tokenProvider){
       return resurce
               //解析固定格式json
               .map(new ResultParseInterceptor<T>())
               //處理token過時,tokenProvider爲詳細的處理方式
               .retryWhen(new TokenExpireInterceptor(tokenProvider))
               //檢查是否有錯誤
               .onErrorResumeNext(new ErrorInterceptor<T>())
                .observeOn(AndroidSchedulers.mainThread())
               .subscribeOn(Schedulers.io());
   }
}


/** * @author whaoming * github:https://github.com/whaoming * created at 2017/2/14 15:59 * Description:數據請求的管理類,負責建立請求 */
public class HttpMethods {
    //retrofit相應的接口
    private ApiService myService;

    //構造方法私有
    private HttpMethods() {
        List<Interceptor> interceptors = new ArrayList<>();
        Map<String,String> headers = new HashMap<>();
        headers.put("userid",25);
        TokenGetInterceptor tokenGetInterceptor = new TokenGetInterceptor(headers);
        HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
        AESInterceptor aesInterceptor = new AESInterceptor();
         //建立一個http頭部處理器攔截器(這裏主要處理server返回token的捕獲)
        interceptors.add(tokenGetInterceptor );
        //日誌打印攔截器
        interceptors.add(loggingInterceptor );
        //數據的加密與解密攔截器
        interceptors.add(aesInterceptor);

        RetrofitHelper.getInstance().init(ConstantValue.SERVER_URL,interceptors );
        //建立service
        myService = RetrofitHelper.getInstance().createService(ApiService.class);
    }

    //依據id用戶一個用戶的信息
    public Observable<UserCommonInfo> getUserInfoById(int userid){
        return  Direct2.create(myService.getUserInfoById(userid),new TokenProviderImpl());
    }
}

總結

歡迎你們私信我交流一下,你們也可以看看下個人我的項目,關於我平時的一些文章分享到的技術,我基本都集成在上面:github地址
歡迎star哦!

相關文章
相關標籤/搜索