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<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);
                }
            };
        }
    }
}

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348

推荐阅读更多精彩内容