关于OkHttp缓存post请求的问题

现有这样一个要求,使用Retorfit+okhttp需要在有网的时候能够连接服务器,读取相关信息;在没网络断开的时候需要读取Okhttp的缓存来达到离线的效果。
基于上述的需求,可以使用Okhttp的拦截器来实现:

//设置缓存目录
File cacheFile = new File(BaseApplication.getContext().getCacheDir(), "cache");
Cache cache = new Cache(cacheFile, 1024 * 1024 * 100); //100Mb

//配置okhttp
OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .readTimeout(READ_TIME_OUT, TimeUnit.MILLISECONDS)
                .connectTimeout(CONNECT_TIME_OUT, TimeUnit.MILLISECONDS)
                .addNetworkInterceptor(mRewriteCacheControlInterceptor)
                .addInterceptor(headerInterceptor)
                .addInterceptor(logInterceptor)
                .cache(cache) //设置缓存
                .build();
        Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").serializeNulls().create();
retrofit = new Retrofit.Builder()
                .client(okHttpClient)
//                .addConverterFactory(ScalarsConverterFactory.create())
                .addConverterFactory(GsonConverterFactory.create(gson))
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .baseUrl(BASE_URL)
                .build();
//配置拦截器
private final Interceptor mRewriteCacheControlInterceptor = new Interceptor() {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            String cacheControl = request.cacheControl().toString();
            if (!NetWorkUtils.isNetConnected(BaseApplication.getContext())) {
                request = request.newBuilder()
                        .cacheControl(TextUtils.isEmpty(cacheControl)? CacheControl.FORCE_CACHE:CacheControl.FORCE_NETWORK)
                        .build();
            }
            Response originalResponse = chain.proceed(request);
            if (NetWorkUtils.isNetConnected(BaseApplication.getContext())) {
                //有网的时候连接服务器请求,缓存一天
                return originalResponse.newBuilder()
                        .header("Cache-Control", "public, max-age=" + MAX_AGE)
                        .removeHeader("Pragma")
                        .build();
            } else {
                //网络断开时读取缓存
                return originalResponse.newBuilder()
                        .header("Cache-Control", "public, only-if-cached, max-stale=" + CACHE_STALE_SEC)
                        .removeHeader("Pragma")
                        .build();
            }
        }
    };

通过以上就可以实现缓存。
但是,在使用该拦截器去执行POST请求的时候,会发现,即使在log中看到了读取缓存,但是实际上缓存目录里什么都没有。实际上是因为get请求一般较为持久,而post需要携带参数,会经常改动,所以没必要缓存,这个机制从Okhttp的源码里也可以看到:

//Cache类的put方法
private CacheRequest put(Response response) throws IOException {
    String requestMethod = response.request().method();

    if (HttpMethod.invalidatesCache(response.request().method())) {
      try {
        remove(response.request());
      } catch (IOException ignored) {
      }
      return null;
    }
    //如果请求方式不用get,就直接跳过了
    if (!requestMethod.equals("GET")) {
      return null;
    }
    //省略代码
  }

但是,有些情况下确实需要去缓存post请求,要怎么去实现呢?

  • Sqlite
    网络正常时缓存响应信息到数据库,在没有网络的时候读出数据。
  • DiskLruCache
    通过文件缓存到本地。

这里是通过Sqlite来实现,至于如果通过DiskLruCache可以参考手动缓存Retrofit+OkHttp响应体,不再局限于Get请求缓存 。
通过Sqlite,需要缓存的基本信息有URL,Params, Response(链接,参数,响应结果)。当然可能还会缓存一些期限时长之类的字段,可以用于清理过期的缓存等。这里以基本的信息来实现。

//创建数据库和表
public class MyDBHelper extends SQLiteOpenHelper{
    private static final String DB_NAME = "test.db";
    private static final int DB_VERSION = 1;
    static final String CACHE_TABLE = "cache";

    public MyDBHelper(Context context) {
        super(context, DB_NAME, null, DB_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        String sql = "create table if not exists " + CACHE_TABLE +
                " (url text, params text, response text)";
        db.execSQL(sql);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        String sql = "DROP TABLE IF EXISTS " + CACHE_TABLE;
        db.execSQL(sql);
        onCreate(db);
    }
}

另外,还需创建一个数据库管理类CacheDao,用于增删该改查:

public class CacheDao {
    private static volatile CacheDao cacheDao;

    private MyDBHelper helper;
    private SQLiteDatabase database;

    private CacheDao(Context context){
        helper = new MyDBHelper(context.getApplicationContext());
        database = helper.getWritableDatabase();
    }
    public static CacheDao getInstance(Context context) {
        if (cacheDao == null) {
            synchronized (CacheDao.class) {
                if (cacheDao == null) {
                    cacheDao = new CacheDao(context);
                }
            }
        }
        return cacheDao;
    }
    //查
    public String queryResponse(String urlKey, String params) {
        return null;
    }
    //增
    public void insertResponse(String urlKey, String params, String value) {       
    }
    //改
    public void updateResponse(String urlKey, String params, String value) {  
    }
    //删
    public void deleteResponse(String urlKey, String params) {
    }
}

数据库创建完成后,关键是要和Okhttp的请求拦截串联起来使用:

 private final Interceptor mRewriteCacheControlInterceptor = new Interceptor() {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();    
            String url = request.url().toString(); //获取请求URL
            Buffer buffer = new Buffer(); 
            request.body().writeTo(buffer); 
            String params = buffer.readString(Charset.forName("UTF-8")); //获取请求参数
            Response response;
            if (NetWorkUtils.isNetConnected(BaseApplication.getContext())) {
                int maxAge = 60 * 60*24; 
                //如果网络正常,执行请求。
                Response originalResponse = chain.proceed(request);
                //获取MediaType,用于重新构建ResponseBody
                MediaType type = originalResponse.body().contentType();
                //获取body字节即响应,用于存入数据库和重新构建ResponseBody
                byte[] bs = originalResponse.body().bytes();
                response = originalResponse.newBuilder()
                        .removeHeader("Pragma")
                        .removeHeader("Cache-Control")
                        .header("Cache-Control", "public, max-age=" + maxAge)
                        //重新构建body,原因在于body只能调用一次,之后就关闭了。
                        .body(ResponseBody.create(type, bs))
                        .build();
                 //将响应插入数据库
                cacheDao.insertResponse(url, params, new String(bs, "GB2312"));
            } else {
                //没有网络的时候,由于Okhttp没有缓存post请求,所以不要调用chain.proceed(request),会导致连接不上服务器而抛出异常(504)
                String b = cacheDao.queryResponse(url, params); //读出响应
                Log.d("OkHttp", "request:" + url);
                Log.d("OkHttp", "request method:" + request.method());
                Log.d("OkHttp", "response body:" + b);
                int maxStale = 60 * 60 * 24 * 28; 
                //构建一个新的response响应结果
                response = new Response.Builder()
                        .removeHeader("Pragma")
                        .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
                        .body(ResponseBody.create(MediaType.parse("application/json"), b.getBytes()))
                        .request(request)
                        .protocol(Protocol.HTTP_1_1)
                        .code(200)
                        .build();
            }
            return response;
        }
    };

由于在没有网络的时候不调用chain.proceed(request),所以拦截器就在此中断,直接将response结果返回。
通过上述方式,即可实现post请求结果的缓存。

你可能感兴趣的:(Android进阶)