上一篇博客讲了Retrofit的简单使用,应该看过的都基本了解我们公司这个服务器请求网络数据的流程,我来简单梳理一下:
- 第一次登录,创建cookiejar,请求服务器数据,保存accesstoken到本地
- 请求其他网络数据,使用已存在的cookiejar,传入本地保存的accesstoken
-
基本上是这样,但是在实际操作中,用户可能登录之后,过了很久才去请求其他数据,这时候cookie已经失效就需要有重试机制。
- 请求接口数据
- 如果服务器返回的状态码是登录超时,则需要在代码中实现自动重新登录,并保存新的cookie和新的accesstoken
- 使用新的accesstoken和cookie再次请求该接口数据
- 如果返回状态码是成功,直接进行后续操作
-
需求已经梳理清楚了,如果要用okhttp来实现以上的这个功能,或者说用异步任务/Handler 来实现网络请求。
重试这一步就显得异常艰难,直接导致了代码重用困难的问题。
即使我们使用异步任务/Handler 实现了以上流程,我们也只能在每次请求的时候复制这部分的处理代码,因为对于重试这部分,每次请求的代码都不一样,难以抽取成通用代码。
当然okhttp有重试机制,但是如果要通过判断请求结果状态,然后再执行一定操作,再重试, 用okhttp实现起来也是非常困难的。
如果再增加一个重试次数限制,那可想而知,代码量会有多大了。但是,是用Rx我们能够轻松地对Obserable进行变换,判断,处理等等,前提是你得熟练Rx。Rx也提供了对变换的封装。我们能够将一系列相同的变换封装成一个Transform,这涉及到compose操作符。
接下来我先简单介绍一下compose、defer、retryWhen这几个操作符。compose
compose其实就是对Obserable进行一系列的变换。
举个最简单的例子,基本上在网上搜索compose都能够搜索到这个例子。
经常使用RxAndroid的小伙伴们应该知道,每次请求网络的时候必加的代码就是,线程切换。.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread())
这两句代码其实我也是深恶痛绝的, 每次都写错,不过没关系,我们可以通过compose来简单封装一下这两句代码。
<T> Transformer<T, T> applySchedulers() { return new Transformer<T, T>() { @Override public Observable<T> call(Observable<T> observable) { return observable.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()); } }; }
写一个applySchedulers()方法,内部实现这个变换。
使用的时候很简单。.compose(applySchedulers())
这一句就实现了线程切换,而且不容易出错。这是compose最基本的用法,看了上面的例子,对compose应该有了基本的了解。
defer
defer其实就是每次产生一个新的Obserable。
在我们创建Obserable的时候通常都会传递一些参数,有时候这个参数会在程序运行的过程中发生改变,当再次调用这个方法时,要使用最新的参数值,而不是原来的参数值。
这么描述可能还是不知道它到底有什么用,待会会在例子中用上,相信你一定能对它有个比较深刻的理解。retryWhen
retryWhen其实就是在某个条件达成的时候,重新调用Obserable中的方法。
接下来我们实现一下这个cookie or token过期,自动重新登录,并重新请求数据,并最终封装成通用的方法。
api.getCompanyInfo("GetCompanyInfo", Dic.key, Dic.accessToken) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .flatMap(new Func1<RequestResult<List<CompanyInfo>>, Observable<? extends RequestResult<List<CompanyInfo>>>>() { @Override public Observable<? extends RequestResult<List<CompanyInfo>>> call(RequestResult<List<CompanyInfo>> listRequestResult) { if (listRequestResult.getErrcode() == -1) { System.out.println("cookie失效"); //如果请求到的数据 错误码是 -1,抛出一个运行时异常错误。 return Observable.error(new RuntimeException("cookieError")); } //否则,说明数据正常获取到了,直接将数据传递下去。 return Observable.just(listRequestResult); } }) //重新发起请求,当...发生某个错误的时候。 // 在上一个flatmap中我们抛出的是 运行时异常,内容是"cookieError" .retryWhen(observable -> { //由于接收到的是一个Obserable.error对象,我们要对这个错误进行处理,需要flatMap一下。 return observable.flatMap(new Func1<Throwable, Observable<?>>() { @Override public Observable<?> call(Throwable throwable) { //当发生这个cookieError错误的时候,实现自动登录 if (throwable instanceof RuntimeException) { return api.UserLogin("action","key","fancy","123456") .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .flatMap(new Func1<RequestResult<String>, Observable<? extends Integer>>() { @Override public Observable<? extends Integer> call(RequestResult<String> stringRequestResult) { //自动登录后,把新的token保存下来。 Dic.accessToken = stringRequestResult.getAccesstoken(); //登录后,不需要为下一个步骤传递任何参数,所以直接使用 just就可以了。 return Observable.just(1); } }); } //发生其他错误,可以进行其他处理,这里就没做处理了,简单抛出一个非法参数错误。 // 最终会在 subscribe中的onError方法中得到统一处理 return Observable.error(new IllegalArgumentException("没救了")); } }); }) .subscribe(new ServerSubscriber<RequestResult<List<CompanyInfo>>>() { @Override public void onNext(RequestResult<List<CompanyInfo>> listRequestResult) { //输出结果 System.out.println(listRequestResult); } });
代码虽然很长,但每一个步骤都非常清晰。
如果替换成lambda可能看上去会好看一点。代码写好后,尝试着测试一下,由于写了线程切换(android线程),所以只能在android中测试了。 如果希望在junit中测试,删掉所有的线程切换代码即可。
跑一遍代码,发现cookie始终都是失效,并且会不断地重试。这是因为我们没对重试次数进行控制,这个次数控制在以上代码中实现非常简单, 加一个 次数技术就可以了,就不详细说了。
现在我们看看是什么原因导致cookie一直失效呢,首先我们用的相同的cookieJar,应该不会不一样才对,并且重新登录后把新的accesstoken保存到了内存中。
其实,之所以失效是因为对Obserable的工作机制没有理解透彻导致的。
在创建一个Obserable的时候,参数的内容,固定的步骤就已经决定好了。
即使在过程中参数发生了改变,retry的时候,还是使用原来的值去请求的。如何验证呢?同样用上篇博客中提到的HttpLoggingInterceptor
就可以验证是不是这样了。日志我就不贴了,验证结果就是参数并没有发生改变。所以,我们需要每次重试的时候,产生一个新的Obserable。这时候我们可以借助defer操作符来实现,每次调用都是一个新的Obserable。
把请求接口的Obserable用Obserable.defer包裹一下即可。Observable.defer(new Func0<Observable<RequestResult<List<CompanyInfo>>>>() { @Override public Observable<RequestResult<List<CompanyInfo>>> call() { return api.getCompanyInfo("GetCompanyInfo", Dic.key, Dic.accessToken); } })
用lambda格式化一下
Observable.defer(() -> api.getCompanyInfo("GetCompanyInfo", Dic.key, Dic.accessToken))
是不是非常简单。
用defer包裹之后再测试,就能够正常访问到接口的结果了。并且会在控制台输出一次cookie失效。说明成功调用了retry中的方法。到这里,基本上解决这个问题的步骤就差不多了,但是,我们还需要对这个重试操作进行封装一下,不用每次都复制这么长的代码,多累啊。
最开始我想到的封装,就是封装两次,就把两个Func1封装起来就行了。
最终调用方法如下:Observable.defer(() -> api.getCompanyInfo("GetCompanyInfo", Dic.key, Dic.accessToken)) .flatMap(new CookieDeal<List<CompanyInfo>>()) .retryWhen(new ReLoginDeal().getFunc1(api))
看上去还是封装的比较简单了。但是每次new CookieDeal的时候都要手动输入参数类型,这太不爽了,而且要写两个步骤,看上去一点也不优雅。
最后我通过了解compose这个操作符的作用,对这个重试机制进行了终极封装。
使用方法如下:.compose(ReLoginDeal.relogin())
一句话,是不是不能再简单,关键是不用再手动打一个类型上去,测试的时候很方便。
贴一下relogin()方法的代码吧:public static <T> Observable.Transformer<RequestResult<T>, RequestResult<T>> relogin() { return observable -> observable .flatMap(tRequestResult -> { if (tRequestResult.getErrcode() == -1) { //重新登录。 System.out.println("cookie 失效"); return Observable.error(new RuntimeException("cookieError")); } return Observable.just(tRequestResult); }) .retryWhen((Func1<Observable<? extends Throwable>, Observable<?>>) observable1 -> observable1.flatMap(new Func1<Throwable, Observable<?>>() { @Override public Observable<?> call(Throwable throwable) { if (throwable instanceof RuntimeException) { return RetrofitUtil.getUserApi().UserLogin("userLogin", MD5.hexdigest("key"), MyEncode.encode("fancy"), MyEncode.encode("123456")) .flatMap(stringRequestResult -> { Dic.accessToken = stringRequestResult.getAccesstoken(); return Observable.just(1); }); } return Observable.error(new IllegalArgumentException("没救了")); } })); }
这个重试机制中登录的用户名密码是写死的,可以通过参数传递做成一个动态的。
基本上封装就结束了,细节部分再自行优化一下就ok啦。
对本文内容有任何疑问欢迎加群讨论:283272067