则需求先实施refresh,Android代码点我365体育网投

我们在拜访户端的安顿实现底层网络架构时候,平日不可幸免的一个难点:token的立竿见影认证,假使token过期,则需求先实施refresh
token的操作,就算执行refresh
token也对事情没有啥益处,则需求用户再实践登陆的进程中;而这几个refresh
token的操作,按理来说,对用户是不可知的。那样的话,大家相应是怎么消除这一个难点吧?

在上篇小说索罗德xjava+Retrofit 达成全局过期 Token
自动刷新
中,主讲了贯彻的想想,发表之后,有个别小伙伴抱怨没有完整的
德姆o,所以在此地再度补上了贰个姗姗来迟的大概的实例。Android代码点我

本文是选拔奥迪Q7xJava +
Retrofit来实现网络请求的卷入的,则主要研商那种情形的完结;一般的写法,则第叁是在回调中,做一些阻止的论断,那里就不叙述了。

适用情况

三个利用的超过十分之五伸手都必要一个包蕴 token
的参数,来代表近来的用户消息;其它 token 是含有有效时间的,当 token
过期时,必要履行刷新 token
的操作。这几个消除方案正是本着那种情状而发出的,当一个伸手收到 token
过期的错误新闻,大家会在尾部执行刷新 token
的操作(这个操作是晶莹操作,对用户不可知),当 token
刷新成功以往,则再次履行在此以前发生的乞请。

其它,不适用的便是 token 是放在 http 请求的 header
中的请求,那种情景的内需经过行使 okhttp
的拦截器来完成,可活动查阅其他的小说。

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

再使用Lacrossexjava的时候,针对单个API出错,再开始展览重试机制,那里应该利用的操作符是retryWhen,
通过检测固定的错误音信,然后举行retryWhen中的代码,执行重试机制。这里有个很好的例子,正是扔物线写的瑞鹰xJava萨姆ples中涉及的非叁回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实现

为了确定保证 德姆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,简单起见,它俩不需求参数,并且再次来到的结果同样;其余1个常规请求的
    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 是
一千,那时大家兴许必要做的操作正是重新登录的操作等等。

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

还有2个生死攸关的地方正是,当token刷新成功现在,大家将旧的token替换掉吧?小编查了一下,java第88中学的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 类型,做以包装,让其重新生成2个档次为 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: 更新了完全的德姆o,
地址:TucsonxJava+Retrofit兑现全局过期token自动刷新德姆o篇

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)

刚产生的多少个请求都回来了 token 过期的 error,之后看到三个重新刷新 token
的哀求,它成功未来,原先的陆个请求又开始展览了重试,并都回到了成功的新闻。一切圆满。

最终,三个完完全全而又简便的德姆o就成功了,若是还有啥样不亮堂的同伴可以加
QQ 群:289926871
来交换。完整的代码这里的包为
token 的结构下,server 代码则是根目录下的 server
文件夹中,测试的时候绝不忘启动 server 哦。

相关文章