优雅地使用Retrofit+RxJava(二)

来源:互联网 发布:网络监控巡检记录表 编辑:程序博客网 时间:2024/06/05 21:09
[转载自](http://blog.csdn.net/qq122627018/article/details/68957782)

前言

在我上一篇讲Retrofit+RxJava在MVP模式中优雅地处理异常(一)中,发现很多网友发邮箱给我表示期待我的下一篇文章,正好趁着清明假期,我就写写平时我在使用RxJava+Retrofit怎么去灵活地处理一些场景。比如说一些比较常见的场景:

  • 网络请求过程中token的处理
  • 网络请求数据的加密与解密
  • 为每个请求添加固定的头部,比如说当前版本号,Rsa的密钥等等
  • 规范化每个网络请求,让代码只写一次

我自己平时对代码的简洁性要求非常高,所以retrofit+rxjava正好切中了我的痛点,这也是激发我写这篇文章的原因,我想要与大家一起交流进步,可以看看我的代码示例

一个简单的示例

(可以选择先忽略,等看完这篇文章再回头来看)

/*** @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头部处理器拦截器(这里主要处理服务器返回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: 按照创建者模式的思想,把一个访问服务器的操作规格化 */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());   }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

网络层:RxJava+Retrofit

相对来说,retrofit+rxjava的学习成本还是比较高的。举个例子,就拿数据打印来说,如果使用okHttp的话,可以直接在回调里面打印服务器返回的json数据,但是放在retrofit中,因为retrofit会自动帮你封装成对应的bean,这使得数据解析这个过程不可见,需要通过retrofit的拦截器才能实现,所以拦截器对于retrofit来说,是一个非常非常重要的东西。

retrofit拦截器的使用场景

日志拦截器

还记得刚开始使用retrofit的时候,就被这个功能吓到了,大哥我只是想简单地打印下服务器给了我什么数据,为什么要这么麻烦啊!!!不过后面也越来越理解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();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

token拦截器

token机制我相信大多数客户端都必须要有的一个东西,这里我们这个拦截器的工作是为每个请求添加头部,还有拦截服务器返回的头信息里面是否包含token,有的话取出并存在本地。先上代码:

/** * Created by Mr.W on 2017/2/6. * E-maiil:122627018@qq.com * github:https://github.com/whaoming * TODO: 拦截服务器返回的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过期的时候,客户端会带上长token向服务器再次获取短token,然后再重新发起请求。当然每个系统的token机制都可能不一样,这里也可以看出retrofit可以很灵活地处理很多种情况*/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

那么关于整个流程token的维护,包括发现token过期之后,怎么请求新token,怎么重新发起请求,这些操作retrofit要配合rxjava来实现,后面关于rxjava我会说到。

加密解密拦截器

在这里先简单讲一下我的加密机制,主要是通过rsa+aes,也就是客户端表单提交的数据,通过aes加密,然后aes的key再通过客户端本地保存的公钥进行加密(此公钥由服务器通过rsa算法生成,打包的时候保存在客户端本地),把加密之后的key放在请求头里面,一起发送给服务器。拦截器的代码如下:

/*** @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);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

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));    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

token过期处理

这里的处理逻辑其实还蛮复杂的,看看下图(画的比较丑,不要介意)
这里写图片描述
在这里可以使用RxJava的retryWhen操作符,先看看服务器返回的数据格式:

/** * 这是服务器返回数据的一个固定格式 * @author Mr.W */public class Result<T> {    public int state;    public String error;    public T infos;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

那么一个基本的流程是这样的:
这里写图片描述

所以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();}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

这样就可以很完美的处理了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头部处理器拦截器(这里主要处理服务器返回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());    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

总结

欢迎大家私信我交流一下,大家也可以看看下我的个人项目,关于我平时的一些文章分享到的技术,我基本都集成在上面:github地址
欢迎star哦!!

(function () {('pre.prettyprint code').each(function () { var lines = (this).text().split(\n).length;varnumbering = $('
    ').addClass('pre-numbering').hide(); (this).addClass(hasnumbering).parent().append(numbering); for (i = 1; i
    0 0
    原创粉丝点击