App网络请求实战二:继续封装以及Interceptor拦截器的使用场景分析
我一猛龙撞击,加一手回笼望月,完美,叫你皮!
老规矩,先上图
OkHttp的配置
如果你还没有看上一篇,你可以先看一看上一篇 App网络请求实战一:Rxjava+Retrofit的初步封装
上一篇中遗留了一个问题就是:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(ApiService.baseUrl)
//这里的client当然可以自己配置
.client(new OkHttpClient())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build();
apiService = retrofit.create(ApiService.class);
就是我每次调用网络请求的时候都要写这么一大段,感觉有点zz有木有,而且还没有配置okhttp。所以我们还要再继续封装下。关于为什么retrofit.create(ApiService.class);就可以调用网络,这里面用到了动态代理的技术,不是实战主题,不展开。retrofit采用一个baseUrl对应一个retrofit原则,所以最好采用单例模式进行封装,如下所示:
/**
* <pre>
* 作者 : 肖坤
* 时间 : 2018/04/20
* 描述 :
* 版本 : 1.0
* </pre>
*/
public class RetrofitHelper
{
//假设这个是默认的retrofit,因为一个App可能有多个baseUrl
//所以可能有多个retrofit
private static Retrofit retrofit1;
private static Retrofit retrofit2;
public static Retrofit getRetrofit1()
{
//设置gson解析不严格模式,防止一些解析错误,比如double数据出现NaN时
Gson gson = new GsonBuilder()
.setLenient()
.create();
OkHttpClient client = OkhttpHelper.initOkHttp1();
if (retrofit1 == null)
{
retrofit1 = new Retrofit.Builder()
.client(client)
.baseUrl(ApiService.baseUrl)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
}
return retrofit1;
}
public static Retrofit getRetrofit2()
{
//设置gson解析不严格模式,防止一些解析错误,比如double数据出现NaN时
Gson gson = new GsonBuilder()
.setLenient()
.create();
OkHttpClient client = OkhttpHelper.initOkHttp2();
if (retrofit2 == null)
{
retrofit2 = new Retrofit.Builder()
.client(client)
.baseUrl(ApiService.baseUrl)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
}
return retrofit2;
}
public static <S> S createService(Class<S> serviceClass)
{
return createService(serviceClass, getRetrofit1());
}
public static <S> S createService(Class<S> serviceClass, Retrofit retrofit)
{
if (retrofit == null)
{
throw new NullPointerException("retrofit 不能为null");
}
return retrofit.create(serviceClass);
}
}
一般来说,中小型App都是只有一个baseUrl的。所以在创建ApiService时,默认retrofit1是主Retrofit。
关于OkHttpClinet的配置如下:
/**
* <pre>
* 作者 : 肖坤
* 时间 : 2018/04/20
* 描述 : okhttp配置
* 版本 : 1.0
* </pre>
*/
public class OkhttpHelper
{
private static int CONNECT_TIME = 10;
private static int READ_TIME = 20;
private static int WRITE_TIME = 20;
public static OkHttpClient initOkHttp1()
{
OkHttpClient.Builder builder = new OkHttpClient.Builder();
if (BuildConfig.DEBUG)
{
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(BODY);
//打印拦截器
builder.addInterceptor(loggingInterceptor);
//调试拦截器
builder.addInterceptor(new StethoInterceptor());
}
File cacheFile = new File(Constants.PATH_CACHE);
//最大50M,缓存太大领导有意见!为何你App占这么多内存?
Cache cache = new Cache(cacheFile, 1024 * 1024 * 50);
//这里用到okhttp的拦截器知识
builder.addInterceptor(appCacheInterceptor)
// .addNetworkInterceptor(netCacheInterceptor)
.cache(cache)
//下面3个超时,不设置默认就是10s
.connectTimeout(CONNECT_TIME, TimeUnit.SECONDS)
.readTimeout(READ_TIME, TimeUnit.SECONDS)
.writeTimeout(WRITE_TIME, TimeUnit.SECONDS)
//失败重试
.retryOnConnectionFailure(true)
.build();
return builder.build();
}
}
封装之后在项目中我们就直接这么写了:
apiService = RetrofitHelper.createService(ApiService.class);
一行代码搞定,呜呜呜,感觉有点小委屈呢。
有两个比较有意思的拦截器一个是HttpLoggingInterceptor,这个拦截器可以打印出返回的数据,在debug期间很有用;第二个是StethoInterceptor调试拦截器,这个是facebook出的调试神器。OkHttp提供了拦截器机制,Interceptor又分为客户端拦截器和云端拦截器。如下所示:
通过addInterceptor添加的就是客户端拦截器,而通过addNetworkInterceptor添加的就是云端拦截器。客户端拦截器的好处就是:1.总是调用一次;2.允许重试和多次调用Chain.proceed()方法;云端拦截器的好处:不太清楚,没怎么用, 皮的我就不谈了!所以利用客户端拦截器的优势,我们可以用来做缓存处理和token刷新,
场景一:缓存的处理方式
其实逻辑很简单啦,所以我们可以根据上面的逻辑来编写代码。首先在拦截器中判断是否有网络, 没有网络的情况可以修改请求头中CacheControl属性,将其设定为只从缓存中获取数据。既然要有缓存,那必然需要缓存文件,不然丫的从哪里获取数据。所以需要给okhttp指定一个缓存文件夹通过cache来操作。如果有网络,那么不修改request请求头,直接走你!如下所示:
static Interceptor appCacheInterceptor = new Interceptor()
{
@Override
public Response intercept(Chain chain) throws IOException
{
Request request = chain.request();
if (!SystemUtils.isNetworkConnected())
{
//强制使用缓存
request = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build();
}
int tryCount = 0;
Response response = chain.proceed(request);
while (!response.isSuccessful() && tryCount < 3)
{
tryCount++;
// 重试
response = chain.proceed(request);
}
return response;
}
};
场景二:token刷新
其实现在几乎所有的App都通过登录来拿token,然后app里除了登录接口外,其他的接口都需要在header里面验证token。有的App的token是不过期的,有的App的token是过期的。比如QQ一段时间不登陆,就会提示登录过期。所以,有过期token的App是有刷新token的需求的。
有时候我们需要根据业务来判断选择什么技术,脱离业务的技术就是扯淡。从上图中,我们可以得到当token过期的时候,我们需要执行两次请求。第一次请求后发现过期了,拿到新token,又进行了一次请求。所以这符合客户端本地拦截器的优势,直接选择本地拦截器来实现。
static Interceptor tokenInterceptor = new Interceptor()
{
@Override
public Response intercept(Chain chain) throws IOException
{
Request request = chain.request();
Response response = chain.proceed(request);
//判断token是否过期
if (isTokenExpired(response))
{
//同步请求方式,获取新token
ApiService service = RetrofitHelper.createService(ApiService.class);
Call<BaseResponse<ResEntity1.DataBean>> call = service.getNewToken();
retrofit2.Response<BaseResponse<ResEntity1.DataBean>> tokenRes = call.execute();
String newToken = tokenRes.body().getData().getRes();
//然后把这个新token存到sp中
App.getSp().edit().putString("token", newToken).commit();
Request newRequest = chain.request()
.newBuilder()
.header("token", newToken)
.build();
response.body().close();//释放资源
//重新请求
return chain.proceed(newRequest);
}
//若没有过期,直接返回response
return response;
}
};
注意哦,这里面有一个坑坑的地方。那就是如何判断token是否过期这个点,一般是通过服务端返回的code来判断。所以我们需要拿到这个code,如下所示:
/**
* 判断token是否过期
*
* @param response
* @return
*/
private static boolean isTokenExpired(Response response)
{
try
{
String bodyString = getBodyString(response);
BaseResponse tokenExpiredData = new Gson().fromJson(bodyString, BaseResponse.class);
int retCode = tokenExpiredData.getCode();
if (retCode == Constants.EXPIRED_TOKEN)
{
return true;
}
} catch (IOException e)
{
e.printStackTrace();
}
return false;
}
/**
* 将response转换为json字符串
*
* @param response
* @return
* @throws IOException
*/
public static String getBodyString(Response response) throws IOException
{
ResponseBody responseBody = response.body();
BufferedSource source = responseBody.source();
source.request(Long.MAX_VALUE);
Buffer buffer = source.buffer();
Charset charset = Charset.forName("UTF-8");
MediaType contentType = responseBody.contentType();
if (contentType != null)
{
contentType.charset(charset);
}
//注意这里的方式,是仿写的HttpLoggingInterceptor
//在okhttp中buffer只能被read一次,所以只能先clone然后在read
//否则会报错
return buffer.clone().readString(charset);
}
注意上面获取bodyString的时候不能直接使用response.body().string();方法,这样会报错。retrofit中规定那个buffer流只能read一次哦,所以需要先进行clone,然后再进行读取。HttpLoggingInterceptor打印拦截器中,有这方面的实现。
if (logHeaders) {
Headers headers = response.headers();
for (int i = 0, count = headers.size(); i < count; i++) {
logger.log(headers.name(i) + ": " + headers.value(i));
}
if (!logBody || !HttpHeaders.hasBody(response)) {
logger.log("<-- END HTTP");
} else if (bodyEncoded(response.headers())) {
logger.log("<-- END HTTP (encoded body omitted)");
} else {
BufferedSource source = responseBody.source();
source.request(Long.MAX_VALUE); // Buffer the entire body.
Buffer buffer = source.buffer();
Charset charset = UTF8;
MediaType contentType = responseBody.contentType();
if (contentType != null) {
charset = contentType.charset(UTF8);
}
if (!isPlaintext(buffer)) {
logger.log("");
logger.log("<-- END HTTP (binary " + buffer.size() + "-byte body omitted)");
return response;
}
if (contentLength != 0) {
logger.log("");
//就是在这里,对吧,我没蒙你吧!
logger.log(buffer.clone().readString(charset));
}
logger.log("<-- END HTTP (" + buffer.size() + "-byte body)");
}
拦截器真是一个好东西,这上面介绍的还是冰山一角呀0-0。未完待续。
以上。
擦,差点又忘了github地址:https://github.com/xiaokun19931126/HttpExceptionDemo
以上。
上篇博客:App网络请求实战一:Rxjava+Retrofit的初步封装
下篇博客:App网络请求实战三:下载文件以及断点续载