关于OkHttp缓存post请求的问题

Posted this.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于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请求结果的缓存。

以上是关于关于OkHttp缓存post请求的问题的主要内容,如果未能解决你的问题,请参考以下文章

使用 OkHttp 缓存 POST 请求

搞定Android Post请求缓存(不能缓存你顺着网线过来打我)!

okhttp3 post请求基本使用以及超时重连

Android:OkHttp的理解和使用

java okhttp发送post请求

网络请求框架OkHttp3全解系列:OkHttp的基本使用