Rxjava+Retrofit贯彻全局过期token自动刷新。RxJava+Retrofit实现全局过期token自动刷新Demo篇。

我们以做客户端的设计实现底层网络架构下,常常不可避免的一个题材:token的有效认证,若是token过期,则用先实行refresh
token的操作,若是执行refresh
token也无济于事,则需用户还实施登陆的经过中;而以此refresh
token的操作,按理来说,对用户是不可见的。这样的话,我们当是怎么化解这问题吧?

于达到篇稿子Rxjava+Retrofit 实现全局过期 Token
自动刷新面临,主讲了实现之思量,发布后,有若干稍伙伴抱怨没有完好的
Demo,所以在此间更补充上了一个姗姗来迟的略的实例。Android代码点我

正文是使RxJava +
Retrofit来落实网络要的卷入的,则要讨论这种情景的实现;一般的写法,则着重是于回调中,做有拦的判定,这里就未叙述了。

适用情形

一个施用之大部要都亟待一个包含 token
的参数,来代表目前的用户信息;另外 token 是带有有效时间之,当 token
过期时,需要实践刷新 token
的操作。这个解决方案虽是对准这种状态只要有的,当一个央收到 token
过期的错误信息,我们会于底部执行刷新 token
的操作(这个操作是晶莹剔透操作,对用户不可见),当 token
刷新成功以后,则又履行前起之乞求。

此外,不适用的哪怕是 token 是身处 http 请求的 header
中之要,这种气象的待通过采取 okhttp
的拦截器来贯彻,可自动查阅其他的章。

单个请求加加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的款式抛出来,不然她继续执行之前的代码体,陷入一个死循环。

Demo 实现

大多单请求token失效的拍卖逻辑

当集成了Retrofit之后,我们的网要接口则成为了一个个单独的法子,这时我们用添加一个大局的token错误抛来,之后还得得针对有的接口做一个合并之retryWhen的操作,来避免每个接口都所要之token验证处理。

1.实现思想

用 Observale 的 retryWhen 的艺术,识别 token
过期失效的错误信息,此时生刷新 token 请求的代码块,完成后更新
token,这时之前的呼吁会又履行,但以它的 token
更新也流行的。另外通过代理类对具备的要都进行拍卖,完成之后,我们只有待关注单个
API 的实现,而不用每个都考虑 token 过期,大大地落实解耦操作。

token失效错误抛出

在Retrofit中的Builder中,是通过GsonConvertFactory来开json转成model数据处理的,这里我们便需重实现一个协调之GsonConvertFactory,这里根本是因为三单文本GsonConvertFactory,GsonRequestBodyConverter,GsonResponseBodyConverter,它们三只由源码中以过来新建即可。主要我们重写GsonResponseBodyConverter这看似中之convert的办法,这个办法主要以ResponseBody转换我们用的Object,这里我们由此将到我们的token失效的错误信息,然后以那为一个指定的Exception的消息抛出。

2.API实现

为保证 Demo 的完整性,API
这个环节是必要的,这里允许自己偷个小懒,没有运用远程的 API
服务来促成。而是使用 NodeJs
在地头写了单简单的服务,所以小地劳动读者多动一下手指,先启动咱们的
API
服务。NodeJs代码点我

  • 启航服务
    成就的服务器代码在列之绝望目录下之 server 文件被,里面包含一个名
    refresh_token 的 js 文件。我们切到 server 目录下,在命令行下执行
    node refresh_token.js,就足以启动一个监听端口号为 8888 的劳动。
    另外,如果当微机上看的口舌,执行 http://127.0.0.1:8888
    即可访问;如果经过模拟器访问的言语,需要拿到计算机的当地
    IP,这里我取到之是 192.168.56.1。

  • API 介绍
    此地为模仿真实的 token 原理,我用日戳来作为 token
    的平种实现。客户端向服务器请求 token, 服务器返回当前的时戳来作为
    token;之后用户每次的求虽需携这个 token
    作为参数,服务器将到客户端发送过来的
    token,来与眼前之时刻开展比较,这里我下的时日间隔也30秒,若小于30秒,服务器认为
    token 合法,返回正确结果;若超过30秒,则认为 token 失效。

  • 实现
    此间自己计划了三独 API,获取 token 的 get_token 及刷新 token 的
    refresh_token,简单起见,它俩非需参数,并且返回的结果一致;另外一个正常化请求的
    API 是 request,它需要传递一个称也 token
    的参数。代码很简单,如下:

var http = require('http');
var url = require('url');
var querystring = require('querystring');

http.createServer(function (request, response) {

   // 发送 HTTP 头部 
   // HTTP 状态值: 200 : OK
   // 内容类型: text/plain
   response.writeHead(200, {'Content-Type': 'text/plain'});

   var pathname = url.parse(request.url).pathname;
   if (pathname == "/get_token" || pathname == "/refresh_token"){
      // get a new token or refresh the token
      var result = {
         "success" : true,
         "data" : {
            "token" : new Date().getTime().toString()
         }
      }
      response.end(JSON.stringify(result));
   }else if (pathname == "/request"){
      // Normal request
      var token_str = querystring.parse(url.parse(request.url).query)['token'];
      if (token_str){
         var token_time = parseFloat(token_str);
         var cur_time = new Date().getTime();
         if(cur_time - token_time < 30 * 1000){
            var result = {
               "success" : true,
               "data" : {
                  "result" : true
               }
            }
            response.end(JSON.stringify(result)); 
         }else{
            response.end(JSON.stringify({"success": false, "error_code" : 1001})); 
         }
      } else {
         response.end(JSON.stringify({"success": false, "error_code" : 1000})); 
      }
   }

}).listen(8888);

代码很简短,需要提及的凡当 token 超过限定的30秒,返回的 error_code 是
1001;而 token 不存在则赶回的 error_code 是
1000,这时我们或许要做的操作就重新登录的操作等等。

差不多要的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.

3.错误抛出

当服务器错误信息的时段,同样也是一个 model,不同的凡 success 为
false,并且带有 error_code的信息。所以我们要对 model
处理的当儿,做为咬定。主要修改的地方便是 retrofit 的
GsonConvertFactory,这里不再通过 gradle
引入,直接拿其源码中之老三个文件上加到咱们的品类面临。

率先提及的刹那凡是针对统一 model 的卷入,如下:

public class ApiModel<T> {
    public boolean success;
    @SerializedName("error_code") public int errorCode;

    public T data;
}

当是返回的时,我们获得到 data,直接给上层;当出错的下,可以针对
errorCode的音讯,做片甩卖,让其活动最上层调用的 onError 方法。

吓了,说说咱们这边而改的地方:

  • 1.修改 GsonConverterFactory 中,生成 GsonResponseBodyConverter
    的方法:

@Override
public Converter<ResponseBody, ?> responseBodyConverter(final Type type, Annotation[] annotations, Retrofit retrofit) {
  Type newType = new ParameterizedType() {
      @Override
      public Type[] getActualTypeArguments() {
          return new Type[] { type };
      }

      @Override
      public Type getOwnerType() {
          return null;
      }

      @Override
      public Type getRawType() {
          return ApiModel.class;
      }
  };
  TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(newType));
  return new GsonResponseBodyConverter<>(adapter);
}

得看到我们这里针对 type 类型,做盖包装,让那再次生成一个型为 ApiModel
的初路。因为咱们以形容接口代码的时候,都为真的种 type
来作返回值的,而未是 ApiModel。

  • 2.GsonResponseBodyConverter的处理
    她的修改,则是要对准返回结果,做盖好的判定连丢掉来,主要看那个的
    convert方法:

@Override
public Object convert(ResponseBody value) throws IOException {
  try {
      ApiModel apiModel = (ApiModel) adapter.fromJson(value.charStream());
      if (apiModel.errorCode == ErrorCode.TOKEN_NOT_EXIST) {
          throw new TokenNotExistException();
      } else if (apiModel.errorCode == ErrorCode.TOKEN_INVALID) {
          throw new TokenInvalidException();
      } else if (!apiModel.success) {
          // TODO: 16/8/21 handle the other error.
          return null;
      } else if (apiModel.success) {
          return apiModel.data;
      }
  } finally {
      value.close();
  }
  return null;
}

后记

这里,整个完整的代码没有吃起,但是思路走下来还是颇鲜明的。笔者这里的代码是构成了Dagger2一起来成功的,不过代码是一步步圆之。另外,我们还是发生不少沾可扩大的,例如,将刷新token的代码变成同步块,只同意单线程的走访,这就是付读者们去一步步到位了。

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

PS:
转载请注明原文链接

4.添加代理

在以 Retrofit 的时节,我们都得针对每个 API
编写相应的接口代码,最后通过 Retrofit 的 create
方法来兑现调用,而之方法就是经采用代理,根据此接口方法的各种注解参数,最后一个个独门的完好的
API 调用。

因咱们吧用对每个 API 做拍卖,所以我们啊针对它们的 create
方法做一个代理的兑现,主要利用的代码是 Proxy类的
newProxyInstance方法。

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

骨干的代办实现则是其一 ProxyHandler,它是对接口 InvocationHandler
的一个贯彻类似。思想便是指向 method 的调用,做盖 retryWhen
的包,在retryWhen 中取相应的不胜信息来做拍卖,看 retryWhen 的代码:

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 TokenInvalidException) {
                            return refreshTokenWhenTokenInvalid();
                        } else if (throwable instanceof TokenNotExistException) {
                            Toast.makeText(BaseApplication.getContext(), "Token is not existed!!", Toast.LENGTH_SHORT).show();
                            return Observable.error(throwable);
                        }
                        return Observable.error(throwable);
                    }
                });
            }
        })

这里针对 token 过期的 TokenInvalidException 的不行,执行刷新 token
的操作,刷新 token 的操作则是直调用 Retrofit
的章程,而非需走代理了。另外她必须是独联合的代码块,主要的代码就不在这里贴了,具体的代码见
这里。

5.代码说明

极上层之代码调用中,添加了点滴单按钮:

  • 按钮1:获取token

@OnClick(R.id.btn_token_get)
public void onGetTokenClick(View v) {
  RetrofitUtil.getInstance()
      .get(IApiService.class)
      .getToken()
      .subscribeOn(Schedulers.io())
      .observeOn(AndroidSchedulers.mainThread())
      .subscribe(new Subscriber<TokenModel>() {
          @Override
          public void onCompleted() {

          }

          @Override
          public void onError(Throwable e) {

          }

          @Override
          public void onNext(TokenModel model) {
              if (model != null && !TextUtils.isEmpty(model.token)) {
                  GlobalToken.updateToken(model.token);
              }
          }
      });
}

token 获取成功以后,仅仅更新一下大局的token即可。

  • 健康的呼吁
    此地为模仿多请,这里我直接调正常的要5赖:

@OnClick(R.id.btn_request)
public void onRequestClick(View v) {
  for (int i = 0; i < 5; i++) {
      RetrofitUtil.getInstance()
          .getProxy(IApiService.class)
          .getResult(GlobalToken.getToken())
          .subscribeOn(Schedulers.io())
          .observeOn(AndroidSchedulers.mainThread())
          .subscribe(new Subscriber<ResultModel>() {
              @Override
              public void onCompleted() {

              }

              @Override
              public void onError(Throwable e) {

              }

              @Override
              public void onNext(ResultModel model) {

              }
          });
  }
}

为了查看输出,另外对 Okhttp 添加了 HttpLoggingInterceptor 并设置 Body
的 level 输出,用来监测 http 请求的输出。

全体完成之后,先点击获取 token
的按钮,等待30秒以后,再点击正常请求按钮。可以观看如下的出口:

 --> GET http://192.168.56.1:8888/request?token=1471774119164 http/1.1
 --> END GET
 --> GET http://192.168.56.1:8888/request?token=1471774119164 http/1.1
 --> END GET
 --> GET http://192.168.56.1:8888/request?token=1471774119164 http/1.1
 --> END GET
 --> GET http://192.168.56.1:8888/request?token=1471774119164 http/1.1
 --> END GET
 --> GET http://192.168.56.1:8888/request?token=1471774119164 http/1.1
 --> END GET
 <-- 200 OK http://192.168.56.1:8888/request?token=1471774119164 (8ms)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Transfer-Encoding: chunked
 {"success":false,"error_code":1001}
 <-- END HTTP (35-byte body)
 <-- 200 OK http://192.168.56.1:8888/request?token=1471774119164 (5ms)
 <-- 200 OK http://192.168.56.1:8888/request?token=1471774119164 (4ms)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Transfer-Encoding: chunked
 --> GET http://192.168.56.1:8888/refresh_token http/1.1
 --> END GET
 {"success":false,"error_code":1001}
 <-- END HTTP (35-byte body)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Transfer-Encoding: chunked
 <-- 200 OK http://192.168.56.1:8888/request?token=1471774119164 (7ms)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 {"success":false,"error_code":1001}
 Transfer-Encoding: chunked
 <-- END HTTP (35-byte body)
 {"success":false,"error_code":1001}
 <-- END HTTP (35-byte body)
 <-- 200 OK http://192.168.56.1:8888/refresh_token (2ms)
 Content-Type: text/plain
 <-- 200 OK http://192.168.56.1:8888/request?token=1471774119164 (6ms)
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Connection: keep-alive
 Transfer-Encoding: chunked
 Transfer-Encoding: chunked
 {"success":true,"data":{"token":"1471826289336"}}
 <-- END HTTP (49-byte body)
 {"success":false,"error_code":1001}
 <-- END HTTP (35-byte body)
roxy: Refresh token success, time = 1471790019657
 --> GET http://192.168.56.1:8888/request?token=1471826289336 http/1.1
 --> GET http://192.168.56.1:8888/request?token=1471826289336 http/1.1
 --> END GET
 --> END GET
 --> GET http://192.168.56.1:8888/request?token=1471826289336 http/1.1
 --> GET http://192.168.56.1:8888/request?token=1471826289336 http/1.1
 --> END GET
 --> END GET
 --> GET http://192.168.56.1:8888/request?token=1471826289336 http/1.1
 --> END GET
 <-- 200 OK http://192.168.56.1:8888/request?token=1471826289336 (2ms)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Transfer-Encoding: chunked
 {"success":true,"data":{"result":true}}
 <-- END HTTP (39-byte body)
 <-- 200 OK http://192.168.56.1:8888/request?token=1471826289336 (4ms)
 <-- 200 OK http://192.168.56.1:8888/request?token=1471826289336 (6ms)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Transfer-Encoding: chunked
 {"success":true,"data":{"result":true}}
 <-- END HTTP (39-byte body)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Transfer-Encoding: chunked
 <-- 200 OK http://192.168.56.1:8888/request?token=1471826289336 (4ms)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Transfer-Encoding: chunked
 {"success":true,"data":{"result":true}}
 <-- END HTTP (39-byte body)
 <-- 200 OK http://192.168.56.1:8888/request?token=1471826289336 (7ms)
 Content-Type: text/plain
 Date: Mon, 22 Aug 2016 00:38:09 GMT
 Connection: keep-alive
 Transfer-Encoding: chunked
 {"success":true,"data":{"result":true}}
 <-- END HTTP (39-byte body)
 {"success":true,"data":{"result":true}}
 <-- END HTTP (39-byte body)

赶巧生的5独请求都归了 token 过期的 error,之后看到一个双重刷新 token
的伸手,它成之后,原先的5独请求而展开了重试,并都回去了成功的信。一切圆满。

末,一个完好无损而同时简约的Demo就得了,若是还有呀不知底的伴儿可以加
QQ 群:289926871
来交流。完整的代码这里的包为
token 的结构下,server 代码则是彻底目录下之 server
文件夹着,测试的时刻绝不忘记启动 server 哦。