任课了完毕的思索,则必要先举办refresh

大家在走访户端的设计完结底层网络架构时候,平时不可防止的一个标题:token的有效性认证,假诺token过期,则要求先进行refresh
token的操作,假诺执行refresh
token也不行,则需求用户再履行登陆的历程中;而那些refresh
token的操作,按理来说,对用户是不可知的。那样的话,大家应该是怎么消除那一个题目呢?

在上篇文章Havalxjava+Retrofit 已毕全局过期 Token
自动刷新
中,主讲了贯彻的想想,公布之后,有个别小伙伴抱怨没有完好的
德姆o,所以在这边再度补上了二个迟到的简约的实例。Android代码点作者

本文是使用锐界xJava +
Retrofit来贯彻互联网请求的包裹的,则首要商讨那种情况的贯彻;一般的写法,则第二是在回调中,做一些拦截的判断,那里就不叙述了。

适用情况

多个利用的绝超过八分之四伸手都亟需八个包蕴 token
的参数,来代表近年来的用户音讯;别的 token 是含有有效时间的,当 token
过期时,须求执行刷新 token
的操作。那些消除方案就是针对性那种情况而暴发的,当三个伸手收到 token
过期的错误新闻,大家会在底层执行刷新 token
的操作(这么些操作是晶莹操作,对用户不可知),当 token
刷新成功之后,则再度履行之前发生的呼吁。

别的,不适用的就是 token 是放在 http 请求的 header
中的请求,那种情景的要求通过行使 okhttp
的拦截器来已毕,可机关查阅其余的稿子。

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

再利用Evoquexjava的时候,针对单个API出错,再进行重试机制,那里应该使用的操作符是retryWhen,
通过检测固定的错误消息,然后开展retryWhen中的代码,执行重试机制。这里有个很好的例证,就是扔物线写的PRADOxJavaSamples中涉嫌的非1次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之后,大家的网络请求接口则成为了2个个独门的章程,那时大家须求加上3个大局的token错误抛出,之后还得须求对具备的接口做三个集合的retryWhen的操作,来幸免每一个接口都所必要的token验证处理。

1.已毕思想

行使 Observale 的 retryWhen 的艺术,识别 token
过期失效的错误消息,此时发出刷新 token 请求的代码块,达成之后更新
token,那时在此以前的请求会再也履行,但将它的 token
更新为流行的。其它通过代理类对拥有的伏乞都进行拍卖,完毕以往,大家只需关怀单个
API 的落到实处,而不用各种都考虑 token 过期,大大地贯彻解耦操作。

token失效错误抛出

在Retrofit中的Builder中,是通过GsonConvertFactory来做json转成model数据处理的,那里大家就须要再一次达成1个投机的GsonConvertFactory,那里首要由多少个公文GsonConvertFactory,GsonRequestBodyConverter,GsonResponseBodyConverter,它们几个从源码中拿过来新建即可。主要大家重写GsonResponseBodyConverter以此类中的convert的艺术,那个点子首要将ResponseBody转换大家须求的Object,那里我们因而得到大家的token失效的错误消息,然后将其以多少个点名的Exception的消息抛出。

2.API实现

为了保障 德姆o 的完整性,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,不难起见,它俩不必要参数,并且重返的结果一致;其余3个平常请求的
    API 是 request,它须要传递1个称呼为 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 是
一千,这时大家恐怕要求做的操作就是重复登录的操作等等。

多请求的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的操作,那段代码就很简短了。就不再那里细述了。

再有1个重中之重的地点就是,当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.不当抛出

当服务器错误消息的时候,同样也是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的处理
    它的改动,则是要针对性再次回到结果,做以12分的论断并抛出,首要看其的
    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: 更新了完整的德姆o,
地址:RubiconxJava+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即可。

  • 好端端的哀告
    此处为了模仿多请求,那里小编直接调不荒谬的请求4次:

@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)

威尼斯人官网,刚暴发的五个请求都回去了 token 过期的 error,之后看到三个再一次刷新 token
的哀求,它成功将来,原先的八个请求又开展了重试,并都回去了中标的新闻。一切圆满。

最终,三个总体而又简单的德姆o就形成了,借使还有何样不了解的小伙伴可以加
QQ 群:289926871
来沟通。完整的代码这里的包为
token 的布局下,server 代码则是根目录下的 server
文件夹中,测试的时候不要忘运营 server 哦。

相关文章