我们在做客户端的设计实现底层网络架构时候,常常不可避免的一个问题:token的有效验证,若是token过期,则需要先执行refresh token的操作,若是执行refresh token也无效,则需要用户再执行登陆的过程中;而这个refresh token的操作,按理来说,对用户是不可见的。这样的话,我们应该是怎么解决这个问题呢?
本文是采用RxJava + Retrofit来实现网络请求的封装的,则主要讨论这种情况的实现;一般的写法,则主要是在回调中,做一些拦截的判断,这里就不叙述了。
单个请求添加token失效的判断
再使用Rxjava的时候,针对单个API出错,再进行重试机制,这里应该使用的操作符是retryWhen
, 通过检测固定的错误信息,然后进行retryWhen
中的代码,执行重试机制。这里有个很好的例子,就是扔物线写的RxJavaSamples 中提到的非一次token的demo。接下来,主要以其中的demo为例,提一下retryWhen的用法。
在Demo中的TokenAdvancedFragment
中,可查到如下的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
做的判断是检测是否为NullPointerException
和IllegalArgumentException
,其中前者的抛出是在flatMap
的代码体中,当用户的token为空抛出的,而IllegalArgumentException
是在什么时候抛出来的呢?而retryWhen
中的代码体还有fakeApi.getFakeData
的调用,看来就是在它之中抛出的,来看一下他的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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代理类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
来实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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
来进行获取,主要的代码实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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篇