最近项目中遇到一个需求,token过期的自动处理。我们项目中网络请求模块使用了rxJava+Retrofit的组合,相信看到这个文章的人都对此比较了解了,不多赘述。
看到token过期处理首先想到的是在请求回调中获取到token过期的信息,然后进行刷新操作,但是由于项目中使用到的网络请求接口众多,而且大多数接口都有可能会出现token过期的情况,如此处理就显得比较麻烦了。
百度之,发现了使用代理来处理的方法,见大神博客:http://alighters.com/blog/2016/08/22/rxjava-plus-retrofitshi-xian-zhi-demo/?utm_source=tuicool&utm_medium=referral ,本篇文章主要讲一讲我在实践中遇到的问题以及解决的方案。
Token自动刷新实现
1.实现思想
与alighters的思想相同,摘录如下:利用 Observale 的 retryWhen 的方法,识别 token 过期失效的错误信息,此时发出刷新 token 请求的代码块,完成之后更新 token,这时之前的请求会重新执行,但将它的 token 更新为最新的。另外通过代理类对所有的请求都进行处理,完成之后,我们只需关注单个 API 的实现,而不用每个都考虑 token 过期,大大地实现解耦操作。
2.token过期处理相关类
token过期处理主要用到如下4个类
GsonConverterFactory //在Retrofit.builder().addConverterFactory中使用,直接拷贝原有的 retrofit2.converter.gson.GsonConverterFactory到项目目录下即可
GsonRequestBodyConverter //同上,和retrofit中的一样
GsonResponseBodyConverter //主要的过期异常抛出代码在其convert()方法中
ProxyHandler //代理类,token刷新代码都在这个类中
3.错误抛出
首先提及我们项目中统一Response的model封装:
public class MResponse<T> implements Serializable {
public String responseCode;//结果码
public T responseData;
public String responseToken;
}
当正确返回数据时,responseCode为success,当出现token过期时 responseCode为token_timeout.
alighters在其demo对 GsonConverterFactory类中的responseBodyConverter方法中的Type做了包装,其包装代码如下:
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);
}
而在实践中发现这样子包装
会造成一些问题 ,本着尽量少改源码的思想,我这里 没有做这些包装 ,维持原样,即:
public Converter<ResponseBody, ?> responseBodyConverter(final Type type, Annotation[] annotations, Retrofit retrofit) {
TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
return new GsonResponseBodyConverter<>(gson, adapter);
}
这样也避免了Type的类型锁死为我们的Model封装。
接下来就到了真正抛出token_timeout异常的地方了,在GsonResponseBodyConverter中,直接看代码:
public Object convert(ResponseBody value) throws IOException {
try {
JsonReader jsonReader = gson.newJsonReader(value.charStream());
Object obj = adapter.read(jsonReader);
if (obj instanceof MResponse) {//token过期则抛出异常
if (((MResponse) obj).responseCode.equals("token_timeout")) {
throw new IllegalArgumentException("token_timeout");//异常类型可以自己定义
}
}
return obj;
} finally {
value.close();
}
}
使用instanceof使得响应的Model封装不再固定为MResponse,如果有其他类型可以加上else if ,扩展性更好一些。
4.添加代理
添加代理的方式请参照alighters的文章,这里不再描述,贴上一段ProxyHandler中invoke()方法的代码:
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
final InvokeArguments invokeArguments = new InvokeArguments();//变量集合类,避免使用ProxyHandler的成员变量。
return Observable.just(null).flatMap(new Func1<Object, Observable<?>>() {
@Override
public Observable<?> call(Object o) {
try {
try {
if (invokeArguments.isTokenNeedRefresh) {//需要更新的时候更新方法里的token参数
updateMethodToken(invokeArguments, args);
}
return (Observable<?>) method.invoke(mProxyObject, args);
} catch (InvocationTargetException e) {
e.printStackTrace();
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
}).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.getMessage().equals("token_timeout")) {//可自定义过期异常
//token_过期处理
return refreshTokenWhenTokenInvalid(invokeArguments);//获取新的token
}
return Observable.error(throwable);//使用error避免进入死循环
}
});
}
});
}
refreshTokenWhenTokenInvalid代码:
private Observable<?> refreshTokenWhenTokenInvalid(final InvokeArguments invokeArguments) {
synchronized (ProxyHandler.class) {
invokeArguments.refreshTokenError = null;
// call the refresh token api.
ApiWrapper.getInstance().refreshToken()
.subscribe(new Subsciber<MResponse>() {
@Override
public void onNext(MResponse model) {
if (model != null) {
invokeArguments.isTokenNeedRefresh = true;
tokenChangedTime = new Date().getTime();
GlobalToken.updateToken(model.responseToken);//此处使用自己的token存放方式
System.out.println("Refresh token success, time = " + tokenChangedTime);
}
}
@Override
public void onError(Throwable e) {
invokeArguments.refreshTokenError = e;
}
});
if (invokeArguments.refreshTokenError != null) {
return Observable.error(invokeArguments.refreshTokenError);
} else {
return Observable.just(true);
}
}
}
updateMethodToken方法:
private void updateMethodToken(InvokeArguments invokeArguments, Object[] args) {
if (invokeArguments.isTokenNeedRefresh && !TextUtils.isEmpty(your new token)) {
for (int i = 0, len = args.length; i < len; i++) {
if (args[i] instanceof RequestParams) {
((RequestParams) args[i]).requestToken = your new token;
}
}
invokeArguments.isTokenNeedRefresh = false;
}
}
至此全局的token自动刷新功能就完成了,只需用Proxy.newProxyIntance生成的ApiService类来调用请求接口即可实现token过期自动刷新了。为测试可以使用Okhttp的Intercepter来拦截请求,使用Mock的数据来测试,不过要注意mock的数据如果responseCode一直是token_timeout会导致一直循环retry,可以添加 刷新token次数限制来避免。
后记:做完token刷新,本地测试ok之后 兴冲冲的使用服务端的数据来测试,结果居然失败了,罪魁祸首是什么?请看下集,gson解析同一位置不同类型json数据。