在通常情况下,我在写App调用接口的时候不会去判断本地登录状态,都是简单粗暴地直接调用后端接口,让接口对登录状态进行校验,只有在页面跳转等必须前端校验的情况下会去处理。我相信很多人都是这么做的。
由于我本人即做Android开发也做Java后台开发,某一天我在写接口的时候突然想到,在前端明确没有token的情况下。这种情况不需要后端对其进行校验才对(PS:但是在实际开发中,所有需要登录的接口都是要做校验的,不管前端有没有校验)。出于减轻服务器负担的考虑,我还是想尝试在前端先进行一波校验。
我的思路是,自定义一个@Login注解,声明在Retrofit方法上,在请求的时候如果方法上有@Login,则检查本地是否有token缓存,如果没有则直接跳转登录界面。
一开始我考虑的是通过OkHttp的Interceptor进行拦截,但是在研究了一番之后我发现Interceptor无法获取到Retrofit方法上相关的注解信息。找了很久,后来我想明白了,OkHttp作为一个独立的请求工具,本身跟Retrofit是没有什么关系的,只是Retrofit可以使用OkHttp作为请求的工具而已。但是我们还是可以通过自定义请求头的方式来达成目的,因为请求头是Http协议部分的内容,Interceptor中是可以支持读取请求头的,而事实上也是支持的,我在另外一个功能上使用了请求头作为标识,但这种方式不太优雅,暂且不提。
转变思路,在OkHttp这块走不通,那就看看别的路吧,既然注解是使用在Retrofit接口上,那我就从Retrofit上面下手吧。
最简单的情况下,我们可能是以以下方式创建一个Retrofit API实例
Retrofit.Builder builder= new Retrofit.Builder();
Api api = builder.baseUrl(host)
.client(getOkHttpClient())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(Api.class);
因此我从create方法下手,看retrofit的实现原理,又到了喜闻乐见的源码时刻:
简单的说就是通过Proxy代理类,构造一个代理,然后通过代理调用具体的方法,而且具体的方法是通过ServiceMethod类来描述的,通过loadServiceMethod来获取描述,需要注意的是,该方法是有缓存的,即每个ServiceMethod只会load一次。(此处有一大坑被我踩到了)。注意,最终是调用了ServiceMethod的callAdapter来执行请求。
很显然,callAdapter是通过Retrofit.Builder设置的,如果没有特殊的情况我们可以使用RxJava2CallAdapterFactory提供的CallAdapter。但是我们要实现自己的逻辑,必然需要自定义,那我参考一·下RxJava2CallAdapterFactory的写法自己写一个
可以看到,RxJava2CallAdapterFactory继承了CallAdapter.Factory,那么我也继承一下,继承后需要重写get方法
好家伙,一看这方法的参数,就知道是我要找的方法,有注解有返回类型,我啪的一声很快啊,
一段代码就写好了,请看
public class AnnotationCallAdapterFactory extends CallAdapter.Factory {
private static final RxJava2CallAdapterFactory rx = RxJava2CallAdapterFactory.create();
public static AnnotationCallAdapterFactory create() {
return new AnnotationCallAdapterFactory();
}
@Override
public CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
CallAdapter<?, ?> callAdapter = rx.get(returnType, annotations, retrofit);
for (Annotation annotation : annotations) {
if (annotation instanceof Login){
if (!UserStatus.Companion.getINSTANCE().isLogin()){
return new AnnotationAdapter<>(returnType);
}
}
}
return callAdapter;
}
}
public class AnnotationAdapter<R> implements CallAdapter<R, Object> {
private final Type responseType;
public AnnotationAdapter(Type responseType) {
this.responseType = responseType;
}
@Override
public Type responseType() {
return responseType;
}
@Override
public Object adapt(Call<R> call) {
call.cancel();
return new Observable<Response<R>>() {
@Override
protected void subscribeActual(Observer<? super Response<R>> observer) {
Loading.endLoading();
Loading.endNLoading();
RouterUtils.router(new Router(Dict.Path.LOGIN));
observer.onComplete();
}
};
}
}
很简单,在获取CallApdater的时候,判断注解上是否包含Login,有则返回我自定义的AnnotationAdapter,这个Adapter的功能也很简单,直接取消本次请求,跳转登录页。否则返回RxJava2CallAdapter。写完测试一波,在本地登录状态失效的情况下调用接口,直接就跳转了登录页面,既节省了流量,又减轻了服务器的负担,岂不美哉。
坑
事情到这里就结束了吗,还记得我前面提到的坑吗?CallApdater是有缓存的,在测试的时候,我跳转了登录页面,到这里我大意了,没有继续测试。没想到这BUG不讲武德啊,过了几天,在偶然的情况下,我想要点赞一篇文章,啪的一声跳到了登录页面,我立马输入账号密码登录一气呵成,回到上一页继续点赞,一下又给我跳转到了登录页面。我懵了,这咋跟说好的不一样呢?相信聪明的你已经想明白了,在这次拦截中,我返回了自定义的AnnotationAdapter,被这个接口方法给缓存起来了,以至于我每次点赞都是使用的自定义AnnotationAdapter,跳转登录页。想明白了就好办了,马上进行一波改造
public class AnnotationCallAdapterFactory extends CallAdapter.Factory {
private static final RxJava2CallAdapterFactory rx = RxJava2CallAdapterFactory.create();
public static AnnotationCallAdapterFactory create() {
return new AnnotationCallAdapterFactory();
}
@Override
public CallAdapter<?, ?> get(@NonNull Type returnType,@NonNull Annotation[] annotations,@NonNull Retrofit retrofit) {
CallAdapter<?, ?> callAdapter = rx.get(returnType, annotations, retrofit);
if (callAdapter == null) {
return null;
}
return new AnnotationAdapter<>(returnType, callAdapter, annotations);
}
}
public class AnnotationAdapter<R> implements CallAdapter<R, Object> {
private final Type responseType;
private final CallAdapter<R, ?> callAdapter;
private final Annotation[] annotations;
public AnnotationAdapter(Type responseType, CallAdapter<R, ?> callAdapter, Annotation[] annotations) {
this.responseType = responseType;
this.callAdapter = callAdapter;
this.annotations = annotations;
}
@Override
public Type responseType() {
return responseType;
}
@Override
public Object adapt(Call<R> call) {
for (Annotation annotation : annotations) {
if (annotation instanceof Login) {
if (!UserStatus.Companion.getINSTANCE().isLogin()) {
call.cancel();
return new Observable<Response<R>>() {
@Override
protected void subscribeActual(Observer<? super Response<R>> observer) {
Loading.endLoading();
Loading.endNLoading();
RouterUtils.router(new Router(Dict.Path.LOGIN));
observer.onComplete();
}
};
}
}
}
try {
return callAdapter.adapt(call);
} catch (Exception e) {
e.printStackTrace();
return new Observable<Response<R>>() {
@Override
protected void subscribeActual(Observer<? super Response<R>> observer) {
observer.onError(e);
}
};
}
}
}
也许有朋友会问为什么AnnotationAdapter不直接继承RxJava2CallAdapter呢?,很简单RxJava2CallAdapter不是公共的而且是final类,无法继承。这就造成了另外一个大坑,后面我们再讲。这一次的改造应该不用多解释,既然一个接口方法只会获取一次CallAadapter,那么我就把RxJava2CallAdapter也放到AnnotationAdapter里面按需调用不就可以了么?很可惜理想是丰满的,现实是骨感的,我再次运行App,发现报错了,查看控制台日志,返回的数据是正确的,在解析数据的时候报错了
java.lang.RuntimeException: Failed to invoke public io.reactivex.Observable() with no args
好在这个错误比较容易明白,JSON数据本该解析成结果对象的,现在却想要解析成Observable了,Observable没有无参构造函数,所以GG了。问题是为什么会这样呢?再次分析RxJava2CallAdapter的构造过程
最终返回的并不是参数中直接传递过来的returnType,而是经过了自己解析的responseType,知道问题出在哪里就简单了,再次改造AnnotationAdapter,将其他方法都返回RxJava2CallAdapter的结果。
public class AnnotationAdapter<R> implements CallAdapter<R, Object> {
private final CallAdapter<R, ?> callAdapter;
private final Annotation[] annotations;
public AnnotationAdapter(CallAdapter<R, ?> callAdapter, Annotation[] annotations) {
this.callAdapter = callAdapter;
this.annotations = annotations;
}
@Override
public Type responseType() {
return callAdapter.responseType();
}
@Override
public Object adapt(Call<R> call) {
for (Annotation annotation : annotations) {
if (annotation instanceof Login) {
if (!UserStatus.Companion.getINSTANCE().isLogin()) {
call.cancel();
return new Observable<Response<R>>() {
@Override
protected void subscribeActual(Observer<? super Response<R>> observer) {
Loading.endLoading();
Loading.endNLoading();
RouterUtils.router(new Router(Dict.Path.LOGIN));
observer.onComplete();
}
};
}
}
}
try {
return callAdapter.adapt(call);
} catch (Exception e) {
e.printStackTrace();
return new Observable<Response<R>>() {
@Override
protected void subscribeActual(Observer<? super Response<R>> observer) {
observer.onError(e);
}
};
}
}
}
再次运行测试,点赞->登录->点赞,这个流程终于正常了,大功告成。 这里只是提供了一种小小的思路,也许大家还有别的更好的方法,欢迎在下面留言。感谢大家的观看,再见。