美文网首页Android DevAndroid rxjava
Rxjava+Retrofit实现全局过期token自动刷新

Rxjava+Retrofit实现全局过期token自动刷新

作者: alighters | 来源:发表于2016-05-02 21:53 被阅读3972次

我们在做客户端的设计实现底层网络架构时候,常常不可避免的一个问题:token的有效验证,若是token过期,则需要先执行refresh token的操作,若是执行refresh token也无效,则需要用户再执行登陆的过程中;而这个refresh token的操作,按理来说,对用户是不可见的。这样的话,我们应该是怎么解决这个问题呢?

本文是采用RxJava + Retrofit来实现网络请求的封装的,则主要讨论这种情况的实现;一般的写法,则主要是在回调中,做一些拦截的判断,这里就不叙述了。

单个请求添加token失效的判断

再使用Rxjava的时候,针对单个API出错,再进行重试机制,这里应该使用的操作符是retryWhen, 通过检测固定的错误信息,然后进行retryWhen中的代码,执行重试机制。这里有个很好的例子,就是扔物线写的RxJavaSamples中提到的非一次token的demo。接下来,主要以其中的demo为例,提一下retryWhen的用法。

在Demo中的TokenAdvancedFragment中,可查到如下的代码:

Observable.just(null)
  .flatMap(new Func1<Object, Observable<FakeThing>>() {
      @Override
      public Observable<FakeThing> call(Object o) {
      return cachedFakeToken.token == null
      ? Observable.<FakeThing>error(new NullPointerException("Token is null!"))
      : fakeApi.getFakeData(cachedFakeToken);
      }
      })
.retryWhen(new Func1<Observable<? extends Throwable>, Observable<?>>() {
    @Override
    public Observable<?> call(Observable<? extends Throwable> observable) {
    return observable.flatMap(new Func1<Throwable, Observable<?>>() {
        @Override
        public Observable<?> call(Throwable throwable) {
        if (throwable instanceof IllegalArgumentException || throwable instanceof NullPointerException) {
        return fakeApi.getFakeToken("fake_auth_code")
        .doOnNext(new Action1<FakeToken>() {
            @Override
            public void call(FakeToken fakeToken) {
            tokenUpdated = true;
            cachedFakeToken.token = fakeToken.token;
            cachedFakeToken.expired = fakeToken.expired;
            }
            });
        }
        return Observable.just(throwable);
        }
        });
    }
})

代码中retryWhen执行体中,主要对throwable做的判断是检测是否为NullPointerExceptionIllegalArgumentException,其中前者的抛出是在flatMap的代码体中,当用户的token为空抛出的,而IllegalArgumentException是在什么时候抛出来的呢?而retryWhen中的代码体还有fakeApi.getFakeData的调用,看来就是在它之中抛出的,来看一下他的代码:

public Observable<FakeThing> getFakeData(FakeToken fakeToken) {
  return Observable.just(fakeToken)
    .map(new Func1<FakeToken, FakeThing>() {
        @Override
        public FakeThing call(FakeToken fakeToken) {
        ...
        if (fakeToken.expired) {
        throw new IllegalArgumentException("Token expired!");
        }

        FakeThing fakeData = new FakeThing();
        fakeData.id = (int) (System.currentTimeMillis() % 1000);
        fakeData.name = "FAKE_USER_" + fakeData.id;
        return fakeData;
        }
        });
}

这里的代码示例中可以看出,当fakeToken失效的时候,则抛出了之前提到的异常。

所以,对token失效的错误信息,我们需要把它以固定的error跑出来,然后在retryWhen中进行处理,针对token失效的错误,执行token重新刷新的逻辑,而其他的错误,必须以Observable.error的形式抛出来,不然它继续执行之前的代码体,陷入一个死循环。

多个请求token失效的处理逻辑

当集成了Retrofit之后,我们的网络请求接口则变成了一个个单独的方法,这时我们需要添加一个全局的token错误抛出,之后还得需要对所有的接口做一个统一的retryWhen的操作,来避免每个接口都所需要的token验证处理。

token失效错误抛出

在Retrofit中的Builder中,是通过GsonConvertFactory来做json转成model数据处理的,这里我们就需要重新实现一个自己的GsonConvertFactory,这里主要由三个文件GsonConvertFactory,GsonRequestBodyConverter,GsonResponseBodyConverter,它们三个从源码中拿过来新建即可。主要我们重写GsonResponseBodyConverter这个类中的convert的方法,这个方法主要将ResponseBody转换我们需要的Object,这里我们通过拿到我们的token失效的错误信息,然后将其以一个指定的Exception的信息抛出。

多请求的API代理

为所有的请求都添加Token的错误验证,还要做统一的处理。借鉴Retrofit创建接口的api,我们也采用代理类,来对Retrofit的API做统一的代理处理。

  • 建立API代理类
public class ApiServiceProxy {

    Retrofit mRetrofit;

    ProxyHandler mProxyHandler;

    public ApiServiceProxy(Retrofit retrofit, ProxyHandler proxyHandler) {
        mRetrofit = retrofit;
        mProxyHandler = proxyHandler;
    }

    public <T> T getProxy(Class<T> tClass) {
        T t = mRetrofit.create(tClass);
        mProxyHandler.setObject(t);
        return (T) Proxy.newProxyInstance(tClass.getClassLoader(), new Class<?>[] { tClass }, mProxyHandler);
    }
}

这样,我们就需要通过ApiServiceProxy中的getProxy方法来创建API请求。另外,其中的ProxyHandler则是实现InvocationHandler来实现。

public class ProxyHandler implements InvocationHandler {

    private Object mObject;

    public void setObject(Object obj) {
        this.mObject = obj;
    }

    @Override
    public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable {
        Object result = null;
        result = Observable.just(null)
            .flatMap(new Func1<Object, Observable<?>>() {
                @Override
                public Observable<?> call(Object o) {
                    try {
                        checkTokenValid(method, args);
                        return (Observable<?>) method.invoke(mObject, args);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                    return Observable.just(new APIException(-100, "method call error"));
                }
            }).retryWhen(new Func1<Observable<? extends Throwable>, Observable<?>>() {
                             @Override
                             public Observable<?> call(Observable<? extends Throwable> observable) {
                                 return observable.
                                     flatMap(new Func1<Throwable, Observable<?>>() {
                                                 @Override
                                                 public Observable<?> call(Throwable throwable) {
                                                     Observable<?> x = checkApiError(throwable);
                                                     if (x != null) return x;
                                                     return Observable.error(throwable);
                                                 }
                                             }

                                     );
                             }
                         }

                , Schedulers.trampoline());
        return result;
        }
  }
    

这里的invoke方法则是我们的重头戏,在其中通过将method.invoke方法包装在Observable中,并添加retryWhen的方法,在retryWhen方法中,则对我们在GsonResponseBodyConverter中暴露出来的错误,做一判断,然后执行重新获取token的操作,这段代码就很简单了。就不再这里细述了。

还有一个重要的地方就是,当token刷新成功之后,我们将旧的token替换掉呢?笔者查了一下,java8中的method类,已经支持了动态获取方法名称,而之前的Java版本则是不支持的。那这里怎么办呢?通过看retrofit的调用,可以知道retrofit是可以将接口中的方法转换成API请求,并需要封装参数的。那就需要看一下Retrofit是如何实现的呢?最后发现重头戏是在Retrofit对每个方法添加的@interface的注解,通过Method类中的getParameterAnnotations来进行获取,主要的代码实现如下:

Annotation[][] annotationsArray = method.getParameterAnnotations();
Annotation[] annotations = null;
Annotation annotation = null;
if (annotationsArray != null && annotationsArray.length > 0) {
  for (int i = 0; i < annotationsArray.length; i++) {
    annotations = annotationsArray[i];
    for (int j = 0; j < annotations.length; j++) {
      annotation = annotations[j];
      if (annotation instanceof Query) {
        if (ACCESS_TOKEN_KEY.equals(((Query) annotation).value())) {
          args[i] = newToken;
        }
      }
    }
  }
}

这里,则遍历我们所使用的token字段,然后将其替换成新的token.

后记

这里,整个完整的代码没有给出,但是思路走下来还是很清晰的。笔者这里的代码是结合了Dagger2一起来完成的,不过代码是一步步完善的。另外,我们还是有许多点可以扩展的,例如,将刷新token的代码变成同步块,只允许单线程的访问,这就交给读者们去一步步完成了。

PS: 更新了完整的Demo, 地址:RxJava+Retrofit实现全局过期token自动刷新Demo篇

PS: 转载请注明原文链接

相关文章

网友评论

  • 陈晨XX:好好学习下
  • 陈晨XX:楼主源码放一个?
    alighters:@陈晨XX 加我qq 528127139聊吧
  • d81398a62868:兄弟,现在越来越给力了
    alighters:@kevin徐鑫 好啊,听你安排。
    d81398a62868:@alighters 大哥,啥时候有空,请你们吃饭,我要去深圳了
    alighters:@kevin徐鑫 你才是大神,向你学习。。
  • cf2684cf16b1:请教一下楼主,我的需求也是遇到特定错误需要处理并重发请求,但不是刷新 token ,而是弹出对话框让用户输入密码,密码正确之后重发请求,这种情况下应该怎么办?在网上搜索了很久,都没有找到合适的方法.希望楼主赐教,谢谢
    alighters:@一树1916 没问题的,做一下线程同步就好
    cf2684cf16b1:@alighters 但是要等待用户输入密码,只能在对话框的点击事件里面重发请求.
    alighters:@一树1916 重发请求可以用rxjava的retrywhen。弹出对话框是在主线程做处理,这里要注意下线程同步的问题。
  • bogerLiu:学习学习
  • 满天丿星:打听一下,楼主是怎么解决Rxjava+Retrofit下网络状态判断的,无网络情况下统一抛出一个自定义网络异常??
    我想要有你在的未来:@满天丿星 是的,我是这样做的
  • 满天丿星:本来扔物线的例子看不懂的,经过你这么一讲解再加上”token失效错误抛出”的方案完美的解决了我的token刷新和注册问题,不过”多请求的API代理”没怎么看懂,可能是是我暂时想不到怎么用
    alighters:@满天丿星 谢谢壕的打赏。 :smile:
    alighters:@满天丿星 :blush: 多请求是指如果每个请求都要进行token失效验证还要刷新,就太麻烦了,所以实现了一个代理类,通过代理类来去做这些事情,这样我们API接口还是干净的(没有蛋疼的token验证的问题)。
    满天丿星:@满天丿星 写的比较好就打赏一下吧
  • 风怜目:上源码吧
  • 747a945a4501:而且Retrofit和rxjava整合,他支持几种返回足够你用了,Obserable<实体> Obserable<Response<实体>> Obserable<Result<实体>>,Single<???> 功能还是很强大。可以看看adapter-rxjava的代码
  • 747a945a4501:其实我觉得用OkHttp的拦截器就能做的事情,楼主饶了一大堆路。而且token失效也是服务器校验,回给客户端也是校验异常的信息,你的业务层拦截到做相应的动作。如果你是想拦截这种现象,response都抛给业务层了,直接用通用的Ui handler处理一次不就行了。
    747a945a4501:@江上夜泊人5728 http://www.jianshu.com/p/62ab11ddacc8
    江上夜泊人5728:求在okhttp拦截器里面处理的例子,最近也在烦恼这个问题
    alighters:@维少 那这个之前的请求token失效,需要自动刷新token,再执行之前错误的请求。。业务层处理太麻烦了吧?你有好方法的话,让我围观一下代码。。
  • 遇见67:你好,想问一下,如果不是使用rxjava该如何实现这个功能呢
    alighters:@遇见67 HttpException的errorBody这个你要看一下你的网络请求框架有这个方面的说明么,我用的是retrofit,是在gson解析的时候对错误进行统一处理的。
    遇见67:我也是觉得这样做好麻烦,然后前几天使用rxjava实现了这个功能,但是,现在有一个情况就是有些网络请求失败后返回的HttpException的errorBody无法获得,不知道是后台数据返回错误还是我这里的错误,真是郁闷。
    alighters:@遇见67 底层网络处理的时候,需要统一回调,而在token失效的时候,不调用网络请求的回调,先执行刷新token的操作,成功则再进行上次的网络请求。。具体代码还是比较麻烦,毕竟要嵌套进行网络请求的回调
  • 键盘男:其实好文章,标题不够给力。叫“Retrofit处理api token过期并刷新”是不是更好?
    alighters:@苦逼键盘男kkmike999 可以有

本文标题:Rxjava+Retrofit实现全局过期token自动刷新

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