美文网首页
Android OKHttp3+Retrofit2自定义注解的一

Android OKHttp3+Retrofit2自定义注解的一

作者: 打野路过惩戒炮车 | 来源:发表于2021-11-27 14:48 被阅读0次

    在通常情况下,我在写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<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的构造过程


    image.png
    image.png

    最终返回的并不是参数中直接传递过来的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);
                    }
                };
            }
        }
    }
    

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

    相关文章

      网友评论

          本文标题:Android OKHttp3+Retrofit2自定义注解的一

          本文链接:https://www.haomeiwen.com/subject/jjnfxrtx.html