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

Posted Ever69

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了搞定Android Post请求缓存(不能缓存你顺着网线过来打我)!相关的知识,希望对你有一定的参考价值。

android Post请求缓存实践

*代码已上传github,需要源码的可以去这里看看NetCache

为什么要做网络缓存?

网络缓存可以提高接口的响应速度,节省服务器资源,因为有些数据比如用户信息之类的,很久都不会有什么修改,那么这种情况就没必要每次都从服务器拉取数据,完全可以使用本地的缓存,当用户信息有更新时,我们再将从服务器获取的数据覆盖本地缓存并使用。
期次对于某些需要用来做界面展示的数据,当手机没有网络的时候可以使用其缓存快速展示给用户,避免界面出现空屏无数据的尴尬。
相信以上两点足以证明网络缓存的必要性。

OkHttp的网络缓存

OKHttp应该都不陌生,现在大部分Android开发应该都在使用它或者封装了它的Retrofit做网络处理,OKHttp的功能非常强大,它可以说是Android目前最好的网络框架了,那么这么强大的框架,自然是支持网络缓存的,有关介绍OkHttp网络缓存的博客网上有很多,随便一篇都能教你怎么开启使用。

OkHttp的限制

那么既然已经有现成的方案可以使用了,我还写这篇博客干嘛,直接用现成的不香吗?不不不,虽然OKHttp的网络缓存很香,但是它也是有限制的,最大一点就是它只支持Get请求方式缓存,不支持Post缓存,这点它的源码里有注明。

if (!requestMethod.equals("GET")) 
      //不要缓存非GET响应。 从技术上讲,我们可以缓存
      //HEAD请求和一些POST请求,但是这样做很复杂
      //因此高,而收益低。
      // Don't cache non-GET responses. We're technically allowed to cache
      // HEAD requests and some POST requests, but the complexity of doing
      // so is high and the benefit is low.
      return null;
    

其实它不支持Post缓存也无可厚非,毕竟Post请求是表单操作,它是用户与服务器交互,向服务器提交数据的一种方式,既然是提交数据,那它返回的数据也是跟着用户提交的数据实时变化的,没必要进行缓存。
但是,就怕但是,遇到浪的,放着Get不用转而都去用Post方法请求接口的,比如我们公司的项目,。。好家伙,直接把路给你封的死死的,现有的OKHttp缓存是别想用了,除非把接口改成Get请求方式,emmm。。。还是算了。

如何实现Post缓存

既然OKHttp缓存这条路被堵死,那我们只能另辟蹊径了,喂喂,可不是让你去干改OKHttp源码,绕开判断这种事情的啊,写这篇博客之前我也在网上看了不少如何实现Post缓存这类文章,除了大部分挂羊头卖狗肉的文章外,其余的看完后我直接好家伙,真是神仙过海,各显神通,比如上面说的改源码、绕判断这种,尤其是绕判断这块,感觉没个十年脑栓根本想不出。
不过我没有去使用这些方案,总感觉不靠谱,而且放出的代码有限,博主自己也说方案有缺陷,并不能覆盖所有的缓存场景,也没有放出结果证实自己方案的可行性,所以就当个扩展思维的文章看吧~
思考一番过后,我选择仿照OKHttp通过拦截器实现自己的缓存方案。

我的缓存方案

在讲我的缓存方案之前,我先大致说一下OKHttp是如何实现缓存的,OKHttp的网络缓存是通过拦截器将request和response缓存在本地文件实现的,那么我也可以仿照它的思路在拦截器实现对接口返回数据Json字符串的缓存。
先上张我的缓存流程图

整个流程中最关键的就是红圈圈起来的地方,因为不是所有的接口请求都需要做缓存,同时就算是需要缓存的接口在有些情况下也不需要缓存,比如有些列表接口只需要缓存第一页的数据,又或者是同一个接口需要缓存不同参数下的数据,例如不同类型的新闻,如何判断一个请求的接口是否需要缓存是非常重要的,如果不能准确的识别,那么后面缓存流程中的存或取都将没有意义。

如何识别需要缓存的接口

因为我们项目用的是Retrofit做网络请求,Retrofit中的接口都是统一定义在一个接口类中的,所以我打算用注解的方式,标注需要缓存的接口,在程序运行时再遍历所有接口,把需要缓存的接口放到一个map集合中进行管理。当有接口请求时在拦截器里用当前请求接口的url去map集合里匹配,如果有则意味这个接口需要缓存。

注解标记

@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NetCache 
	//接口为请求列表时标记需缓存的页码,默认为0时不作处理
    String cachePageIndex() default "0";
    //对指定字段的不同参数都做缓存,默认为空字符串时不作处理
    String multipleCacheIdentificationParameter() default "";

上边就是我定义的注解,目前包含两个参数,如果有其他需要,后期可以再向里加参数。

筛选需要缓存的接口

接下来就是写一个注解解析器,去遍历所有接口筛选出我们需要缓存的部分了。

public class NetCacheProcess 

    /**
     * 已Http请求url的md5值为key
     */
    public static HashMap<String, NetCacheModel> cacheModels = new HashMap<>();

    public static void init(Application context, String baseUrl) 
        if (context == null || baseUrl == null) 
            Log.e(NetCacheUtil.NET_CACHE_TAG, "NetCacheProcess初始化失败,context和baseUrl不能为Null!");
            return;
        
        try 
            cacheModels.clear();
            Class<YSApi> objectClass = YSApi.class;
            Method[] methods = objectClass.getMethods();
            for (Method method : methods) 
                NetCache netCache = method.getAnnotation(NetCache.class);
                if (netCache != null) 
                    POST post = method.getAnnotation(POST.class);
                    if (post != null) 
                        Log.e(NetCacheUtil.NET_CACHE_TAG, "POST:" + post.value());
                        if (!TextUtils.isEmpty(post.value())) 
                            addNetCacheModel(baseUrl + post.value(), netCache);
                        
                     else 
                        GET get = method.getAnnotation(GET.class);
                        if (get != null) 
                            Log.e(NetCacheUtil.NET_CACHE_TAG, "GET:" + get.value());
                            if (!TextUtils.isEmpty(get.value())) 
                                addNetCacheModel(baseUrl + get.value(), netCache);
                            
                        
                    
                
            
         catch (Exception e) 
            e.printStackTrace();
            CrashHelper.respData(e);
        
    

    private static void addNetCacheModel(String key, NetCache netCache) 
        NetCacheModel model = new NetCacheModel();
        model.cacheUrl = key;
        model.cachePageIndex = netCache.cachePageIndex();
        model.identificationParameter = netCache.multipleCacheIdentificationParameter();
        cacheModels.put(MD5Util.encodeBy32BitMD5(key), model);
    


public class NetCacheModel 

    public String cacheUrl;
    public String cachePageIndex;
    public String identificationParameter;

这个类干的事就是利用反射把接口类里的所有方法取出来遍历,将含有NetCache注解的接口转换成对应的数据模型并以接口url为key保存到map集合中。
接着找几个接口添加一下注解,测试一下。

ok,需要缓存的几个接口都添加进来了,接下来就是在拦截器里判断请求的接口是否需要缓存了。

判断请求是否需要缓存

public static NetCacheModel checkCacheEnable(Request request) 
        NetCacheModel model = null;
        try 
            String key = "";
            String url = request.url().toString();
            if ("GET".equalsIgnoreCase(request.method())) 
                String[] split = url.split("\\\\?");
                key = MD5Util.encodeBy32BitMD5(split[0]);
             else if ("POST".equalsIgnoreCase(request.method()))
                key = MD5Util.encodeBy32BitMD5(url);
            
            if (NetCacheProcess.cacheModels.containsKey(key)) 
                model = getNewModel(NetCacheProcess.cacheModels.get(key));
                //需要接口标识请求参数时,直接将需要区别的参数加到缓存url标识后面
                if (!TextUtils.isEmpty(model.identificationParameter)) 
                    String value = getRequestParameter(request, model.identificationParameter);
                    model.cacheUrl = model.cacheUrl + value;
                
                //列表请求时过滤不需要缓存的页码数据
                if (!model.cachePageIndex.equals("0")) 
                    String value = getRequestParameter(request, "page");
                    if (!model.cachePageIndex.equals(value)) 
                        model = null;
                    
                
            
         catch (Exception e) 
            e.printStackTrace();
        
        return model;
    

这里判断的条件就是request的url 是否存在map集合中,如果存在则返回对应的数据模型,否则返回null,表示此请求不需要缓存,在判断之前需要对Post和Get请求的url做些处理,Post直接获取request的url就可以,但是为Get方式时需要把url附带的参数截掉,因为我们map集合中的url都是纯url,不带任何参数的。接下来就是之前讲的对同一接口多份缓存和过滤非指定页码缓存的处理。

在拦截器对数据进行缓存与读取

请求是否需要缓存的判断有了后,再加上对Json数据的缓存和读取就完成了,缓存和读取都是一些流的操作,存的时候在子线程操作并且对对Json字符串做了压缩,取的时候再解压就行了。
下面是拦截器完整的代码

public class NetCacheInterceptor implements Interceptor 
    @Override
    public Response intercept(Chain chain) throws IOException 
        Request request = chain.request();
        Response response = null;
        NetCacheModel cacheModel = HttpInterceptorUtil.checkCacheEnable(request);
        //无网络时并且此接口可缓存时尝试获取缓存返回
        if (!NetUtil.checkNet(MyApplication.sApplication) && cacheModel != null) 
            String cache = NetCacheUtil.loadCache(cacheModel.cacheUrl, new NetCacheLoadListener());
            //缓存不为空时创建response返回
            if (!TextUtils.isEmpty(cache)) 
                Response.Builder builder = new Response.Builder()
                        .request(request)
                        .protocol(Protocol.HTTP_1_1)
                        .message("use net cache")
                        .code(200)
                        .body(ResponseBody.create(MediaType.parse("application/json"), cache));
                response = builder.build();
                return response;
            
        
        response = chain.proceed(request);
        //此接口可缓存并且请求成功时尝试将数据缓存本地
        if (cacheModel != null && response != null && response.code() == 200) 
            String key = cacheModel.cacheUrl;
            String responseInfo = HttpInterceptorUtil.getResponseInfo(response);
            if (TextUtils.isEmpty(responseInfo)) 
                Log.e(NetCacheUtil.NET_CACHE_TAG, "缓存失败,responseInfo为null");
             else 
                NetCacheUtil.saveCacheAsync(key, responseInfo, new NewCacheSaveListener());
            
        
        return response;
    

最后再把拦截器添加到OKhttp里,记得用addInterceptor(),千万别用addNetworkInterceptor(),要是用了后者,没网的时候,就直接返回没网络的response了,不会走我们的缓存拦截器。

是骡子是马?

写了这么多代码,到底能不能按预期产出(废话,不能产出我还写这篇博客干啥)?看到结果前我也不知道,是骡子是马还得牵出来溜溜,拿两个接口试试。

@NetCache(cachePageIndex = "1",multipleCacheIdentificationParameter = "business_type")
    @FormUrlEncoded
    @POST("study_plan_task")
    Observable<HttpResponse<StudyData>> study_plan_task();
    
 @NetCache
    @FormUrlEncoded
    @POST("study/head")
    Observable<HttpResponse<MyStudyHeadBean>> getMyStudyHeadData();

缓存本地

接口请求后打印的log

去手机文件夹里查看一下缓存文件

读取本地缓存

断了网请求缓存过的接口试试读取好不好使

大功告成

至此,Post请求的缓存就完成了,虽然自己动手麻烦了点,但是看到成果的那一刻还是挺值得的,其实写代码倒没什么,主要是整个缓存流程的梳理和设计思路的构想,办法有了,困难就自然而然解决了~

以上是关于搞定Android Post请求缓存(不能缓存你顺着网线过来打我)!的主要内容,如果未能解决你的问题,请参考以下文章

Nginx POST 请求缓存的使用

使用 OkHttp 缓存 POST 请求

http 请求 get 和post 区别

http协议中GET和POST的区别

服务工作者可以缓存 POST 请求吗?

大厂Java研发岗面试复盘,从基础到源码统统帮你搞定