上一篇博客讲了Retrofit的简单使用,应该看过的都基本了解我们公司这个服务器请求网络数据的流程,我来简单梳理一下:
基本上是这样,但是在实际操作中,用户可能登录之后,过了很久才去请求其他数据,这时候cookie已经失效就需要有重试机制。
需求已经梳理清楚了,如果要用okhttp来实现以上的这个功能,或者说用异步任务/Handler 来实现网络请求。
重试这一步就显得异常艰难,直接导致了代码重用困难的问题。
即使我们使用异步任务/Handler 实现了以上流程,我们也只能在每次请求的时候复制这部分的处理代码,因为对于重试这部分,每次请求的代码都不一样,难以抽取成通用代码。
当然okhttp有重试机制,但是如果要通过判断请求结果状态,然后再执行一定操作,再重试, 用okhttp实现起来也是非常困难的。
如果再增加一个重试次数限制,那可想而知,代码量会有多大了。
但是,是用Rx我们能够轻松地对Obserable进行变换,判断,处理等等,前提是你得熟练Rx。Rx也提供了对变换的封装。我们能够将一系列相同的变换封装成一个Transform,这涉及到compose操作符。
接下来我先简单介绍一下compose、defer、retryWhen这几个操作符。
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其实就是每次产生一个新的Obserable。
在我们创建Obserable的时候通常都会传递一些参数,有时候这个参数会在程序运行的过程中发生改变,当再次调用这个方法时,要使用最新的参数值,而不是原来的参数值。
这么描述可能还是不知道它到底有什么用,待会会在例子中用上,相信你一定能对它有个比较深刻的理解。
retryWhen其实就是在某个条件达成的时候,重新调用Obserable中的方法。
接下来我们实现一下这个cookie or token过期,自动重新登录,并重新请求数据,并最终封装成通用的方法。
api.getCompanyInfo("GetCompanyInfo", Dic.key, Dic.accessToken)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.flatMap(new Func1>, Observable extends RequestResult>>>() {
@Override
public Observable extends RequestResult>> call(RequestResult> 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>() {
@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, Observable extends Integer>>() {
@Override
public Observable extends Integer> call(RequestResult stringRequestResult) {
//自动登录后,把新的token保存下来。
Dic.accessToken = stringRequestResult.getAccesstoken();
//登录后,不需要为下一个步骤传递任何参数,所以直接使用 just就可以了。
return Observable.just(1);
}
});
}
//发生其他错误,可以进行其他处理,这里就没做处理了,简单抛出一个非法参数错误。
// 最终会在 subscribe中的onError方法中得到统一处理
return Observable.error(new IllegalArgumentException("没救了"));
}
});
})
.subscribe(new ServerSubscriber>>() {
@Override
public void onNext(RequestResult> 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>>>() {
@Override
public Observable>> 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>())
.retryWhen(new ReLoginDeal().getFunc1(api))
看上去还是封装的比较简单了。但是每次new CookieDeal的时候都要手动输入参数类型,这太不爽了,而且要写两个步骤,看上去一点也不优雅。
最后我通过了解compose这个操作符的作用,对这个重试机制进行了终极封装。
使用方法如下:
.compose(ReLoginDeal.relogin())
一句话,是不是不能再简单,关键是不用再手动打一个类型上去,测试的时候很方便。
贴一下relogin()方法的代码吧:
public static Observable.Transformer, RequestResult> 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>>) observable1 -> observable1.flatMap(new Func1>() {
@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