Android OKHttp3+Retrofit2自定义注解的一种方法

在通常情况下,我在写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的实现原理,又到了喜闻乐见的源码时刻:

image.png

简单的说就是通过Proxy代理类,构造一个代理,然后通过代理调用具体的方法,而且具体的方法是通过ServiceMethod类来描述的,通过loadServiceMethod来获取描述,需要注意的是,该方法是有缓存的,即每个ServiceMethod只会load一次。(此处有一大坑被我踩到了)。注意,最终是调用了ServiceMethod的callAdapter来执行请求。

image.png

很显然,callAdapter是通过Retrofit.Builder设置的,如果没有特殊的情况我们可以使用RxJava2CallAdapterFactory提供的CallAdapter。但是我们要实现自己的逻辑,必然需要自定义,那我参考一·下RxJava2CallAdapterFactory的写法自己写一个


image.png

可以看到,RxJava2CallAdapterFactory继承了CallAdapter.Factory,那么我也继承一下,继承后需要重写get方法


image.png

好家伙,一看这方法的参数,就知道是我要找的方法,有注解有返回类型,我啪的一声很快啊,
一段代码就写好了,请看
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 implements CallAdapter {
    private final Type responseType;

    public AnnotationAdapter(Type responseType) {
        this.responseType = responseType;
    }

    @Override
    public Type responseType() {
        return responseType;
    }

    @Override
    public Object adapt(Call call) {
        call.cancel();
        return new Observable>() {
            @Override
            protected void subscribeActual(Observer> 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 implements CallAdapter {
    private final Type responseType;
    private final CallAdapter callAdapter;
    private final Annotation[] annotations;

    public AnnotationAdapter(Type responseType, CallAdapter callAdapter, Annotation[] annotations) {
        this.responseType = responseType;
        this.callAdapter = callAdapter;
        this.annotations = annotations;
    }

    @Override
    public Type responseType() {
        return responseType;
    }


    @Override
    public Object adapt(Call call) {
        for (Annotation annotation : annotations) {
            if (annotation instanceof Login) {
                if (!UserStatus.Companion.getINSTANCE().isLogin()) {
                    call.cancel();
                    return new Observable>() {
                        @Override
                        protected void subscribeActual(Observer> 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>() {
                @Override
                protected void subscribeActual(Observer> 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的构造过程


image.png

image.png

最终返回的并不是参数中直接传递过来的returnType,而是经过了自己解析的responseType,知道问题出在哪里就简单了,再次改造AnnotationAdapter,将其他方法都返回RxJava2CallAdapter的结果。


public class AnnotationAdapter implements CallAdapter {
    private final CallAdapter callAdapter;
    private final Annotation[] annotations;

    public AnnotationAdapter(CallAdapter callAdapter, Annotation[] annotations) {
        this.callAdapter = callAdapter;
        this.annotations = annotations;
    }

    @Override
    public Type responseType() {
        return callAdapter.responseType();
    }


    @Override
    public Object adapt(Call call) {
        for (Annotation annotation : annotations) {
            if (annotation instanceof Login) {
                if (!UserStatus.Companion.getINSTANCE().isLogin()) {
                    call.cancel();
                    return new Observable>() {
                        @Override
                        protected void subscribeActual(Observer> 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>() {
                @Override
                protected void subscribeActual(Observer> observer) {
                    observer.onError(e);
                }
            };
        }
    }
}

再次运行测试,点赞->登录->点赞,这个流程终于正常了,大功告成。 这里只是提供了一种小小的思路,也许大家还有别的更好的方法,欢迎在下面留言。感谢大家的观看,再见。

你可能感兴趣的:(Android OKHttp3+Retrofit2自定义注解的一种方法)