美文网首页
解决 Retrofit 无法缓存 POST 请求

解决 Retrofit 无法缓存 POST 请求

作者: artzok | 来源:发表于2017-08-31 17:15 被阅读0次

由于 Retrofit 设计之初就是不支持 post 缓存的,关于这点请看 retrofit issues 471,以下是 JakeWharton 关于这点的解释:

Retrofit has nothing to do with this process.It's sole responsibility is the serialization and deserialization of objects to request bodies and vice-versa.

OkHttp does support standard HTTP caching semantics but not for POST. If you want to cache POST bodies you will either have to do it at the application layer above Retrofit or inside of a custom HTTP client below it.

大概意思是说,如果你要缓存 POST 请求就只能通过对 Retrofit 的上层封装或者自定义 HTTP client 实现相应需求。

所以要是你碰上 API 全部使用 POST 请求的话,估计就要抓狂了。

方案1:利用代理模式实现 API 接口类


首先看一下 API 接口:

public interface ApiService {
    @POST("api/category/app/")
    Observable<ResultBean<ListBean<AppMeta>>
    getAppCategory(@Query("category_id") long categoryId,
                   @Query("number") int pageSize,
                   @Query("page") int pageNow);
}

接口并没有全部贴出来,只拿一个来举例说明。为了实现缓存,首先我需要创建一个代理类,并且该类需要实现所有 API 接口,并且每个实现方法需要控制访问是从本地还是网络,下面贴出实现代码:

class ProxyApiService implements ApiService {

   // TypeToken
   private static final Type RESULT_LIST_APP =
           new TypeToken<ResultBean<ListBean<AppMeta>>>() {
           }.getType();

   private Gson mGson;
   private SpUtils mRequestTimestamp;
   private ApiService mRemoteService;
   private DiskLruCacheHelper mDiskCacheHelper;

   ProxyApiService(Context ctx, Retrofit retro) {
       mGson = new Gson();
       mRemoteService = retro.create(ApiService.class);
       mRequestTimestamp = SpUtils.getInstance(ctx, "api_fetch_time");
       mDiskCacheHelper = createDiskLruCacheHelper(ctx);
   }

   // create lru cache helper 
   private synchronized DiskLruCacheHelper
   createDiskLruCacheHelper(Context ctx) {
       //.....
       return null;
   }

   // determine whether from remote access data
   private boolean requestShouldFromRemote(String key) {
       Long preTime = mRequestTimestamp.getLong(key);
       if (NetworkUtils.isAvailable(AppUtils.getAppContext())) {
           long currentTimeMillis = System.currentTimeMillis();
           boolean outTime = currentTimeMillis - preTime > DEFAULT_TIME_INTERVAL;
           DiskLruCacheHelper helper = mDiskCacheHelper;
           String cache = helper == null ? null : helper.getAsString(key);
           if (outTime || cache == null) {
               mRequestTimestamp.putLong(key, currentTimeMillis);
               return true;
           }
       }
       return false;
   }


   public Observable<ResultBean<ListBean<AppMeta>>>
   getAppCategory(long categoryId, int pageSize, int pageNow) {

       final String key = StringUtils.MD5("api/category/app/?category_id="
               + categoryId + "&number=" + pageSize + "&page=" + pageNow);

       if (requestShouldFromRemote(key)) {
            return mRemoteService.getAppCategory(categoryId, pageSize, pageNow)
                    .doOnNext(new Action1<ResultBean<ListBean<AppMeta>>>() {
                        @Override
                        public void call(ResultBean<ListBean<AppMeta>> result) {
                            mDiskCacheHelper.put(key, mGson.toJson(result, RESULT_LIST_APP));
                        }
                    });
        } else {
            String cacheJson = mDiskCacheHelper.getAsString(key);
            if (!TextUtils.isEmpty(cacheJson)) {
                ResultBean<ListBean<AppMeta>> result = mGson.fromJson(cacheJson, RESULT_LIST_APP);
                return Observable.just(result);
            } else return Observable.error(new RuntimeException("can't find cache!"));
        }
    }
}

其实上面代码理解起来也挺简单,只要抓住以下几点即可:

  1. 利用 DiskLruCache 保存缓存内容。
  2. 利用 Gson 将 Response 序列化为字符串以及反序列化为 Java Bean 对象。
  3. 利用 SharedPreferences 实例保存每个接口最近一次从网络访问的时间戳。
  4. 利用 RxJava 的 doOnNext 方法实现将 Response 内容反序列化为字符串,并存储到本地。

所以上述代码实现的整个请求流程如下:

request flow

从实际使用来说上述方案还是可用的,但是存在以下几个问题是无法解决的:

  1. 所有接口返回值都必须是 Observable,不能是 Call。虽然我们一般都是这样用,但是这样的实现总是不利于扩展的。
  2. Token 类型太多,每增加一种 Java Bean 可能就需要添加多种 Token,难以维护。
  3. 扩展性差,如果增加新的 API,即使新增 API 不需要缓存响应,也必须修改代码,添加新的方法实现。
  4. 由于每个接口包含的参数都不一样,而缓存对应的 key 值需要由哪些参数组成必须重点关注,无形中增加了维护成本。
  5. 缓存超时也是一个瓶颈,如果需要对不同接口设置不同的缓存时间,上述方案就很难实现了。

基于上述几个问题,我觉得有必要重构一下代码,下决心找到一种优雅的解决方案 。(主要是最近没那么忙,总想找点事虐自己)

方案2:还是代理模式


最开始使用 Retrofit 时,第一次看到下面代码就意淫以为是利用反射实现:

ApiService service = Retrofit.Builder()
                .baseUrl(baseUrl)
                ....
                .build()
                .create(ApiService.class);

后来不知道什么时候一不小心点开了 create 方法,看到实现后直接傻眼了。那时候才明白当初的自己是多么的 无脑,给一个接口的 Class 对象怎么可能利用反射创建对象呢!知道自己犯了错之后又发现一个相当牛逼的新大陆Proxy,学习动力倍加充足,然后就突发奇想,能不能在套一层 Proxy。

什么是 Proxy

Java 中的 Proxy 简称动态代理,实际上也是利用反射技术实现的。该技术可以在运行时创建一个实现了某些给定接口的新类并创建相应的实例对象,然后将该实例的所有接口方法的访问回调给同一个方法进行统一处理。

结合 Retrofit 的实现,Java 的动态代理还是非常容易理解的。本质就是我们可以在运行时创建一个接口的实现类,并让所有实现方法回调到同一个方法,根据反射信息,在该方法中我们可以知道调用了哪个方法,传入了什么参数等等。

利用 Proxy 和运行时注解实现 GET/POST 缓存

实际上利用 Proxy 只能解决前三个问题,而后面两个问题需要使用运行时注解来解决,接下来通过下面几个步骤完成实现。

  1. 创建代理对象
  public <T> T create(final Context context, final T api, Class<T> clzz) {
        mResponseCache = createDiskLruCacheHelper(context);
        mResponseSaveTpSharedPref = context.getSharedPreferences(
                "response_save_tp_shared_pref_" + cache.getDirName(), Context.MODE_PRIVATE);
        return (T) Proxy.newProxyInstance(clzz.getClassLoader(), new Class[]{clzz},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        if (method.getDeclaringClass() == Object.class) {
                            return method.invoke(this, args);
                        }
                        try {
                            return handleMethodInvoke(context, api, method, args);
                        } catch (Exception e) {
                            return Observable.error(e);
                        }
                    }
                });
    }

同样,还是使用 DiskLruCache 保存缓存内容。
参数 api 表示由用户通过 Retrofit.create(API.class) 创建的对象,用于正常访问服务端。
mResponseSaveTpSharedPref和方案1的mRequestTimestamp意义基本一样,这里指的是最近一次请求的响应 (Response) 被缓存到本地的时间戳。
InvocationHandler 就是上面说的回调对象,而方法 invoke 最终会处理代理对象的所有接口方法的调用。关于代理的更详细内容参考 API 文档。
invoke 实现可以看到最终调用了 handleMethodInvoke 方法,接下来详细分析该方法的实现过程。

  1. 实现处理方法
public <T> Object handleMethodInvoke(Context context, T api, final Method method, Object[] args) {
    final String key = generateKey(method, args);
    final int duration = getCacheDuration(method);
    final Type returnType = method.getGenericReturnType();
    Type responseType = null;
    try {
        responseType = getResponseType(returnType);
        if (responseType == null)
            throw new RuntimeException("can't find type token!");
    } catch (Exception e) {
        return Observable.error(e);
    }

    if (duration != -1 && shouldFromCache(context, key, duration)) {
        String cacheJson = mResponseCache.getCache(key);
        Object obj = null;
        try {
            if (TextUtils.isEmpty(cacheJson))
                throw new RuntimeException("can't find local cache!");
            obj = mGson.fromJson(cacheJson, responseType);
        } catch (Exception e) {
            mResponseCache.removeCache(key);
            return Observable.error(e);
        }
        return Observable.just(obj);
    }
    try {
        Observable observable = null;
        if (args == null || args.length == 0) {
            observable = (Observable) method.invoke(api);
        } else {
            observable = (Observable) method.invoke(api, args);
        }
        final Type finalResponseType = responseType;
        return observable.doOnNext(new Action1() {
            public void call(Object o) {
                if (duration != -1) {
                    mResponseCache.saveCache(key, mGson.toJson(o, finalResponseType));
                }
            }
        });
    } catch (Exception e) {
        return Observable.error(e);
    }
}

generateKey 方法用于生成缓存对象的 key 值,该方法依赖运行时注解,在下一节内容讲解。
getCacheDuration 方法根据注解和环境配置最终返回方法对应 API 的缓存时长,其中 -1 表示不缓存,其他值表示缓存的分钟数。
shouldFromCache 用于决定是否可以从 lru cache 中获得缓存内容,如果可以就从缓存中获取缓存并返回,如果不行则继续执行下面代码,通过 method.invoke 调用 api 对象的方法,然后将返回结果强转为 Observable ( 这里暂不考虑返回值为 Call 的情况 ),最后利用 doOnNext 实现保存响应内容。

  1. 定义运行时注解

使用运行时注解主要是为了解决 key 值难管理,缓存时长等问题,所以最终的注解定义如下:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface CACHE {
    int duration() default 0;                   // 由配置决定是否缓存以及时长
    String key() default "";                    // 自定义键值,默认为空
    int[] paramIndexOfKeys() default {-1};      // 默认全部参数参与key值计算
}

而处理注解的两个关键方法 generateKeygetCacheDuration 定义如下:

  1. 重构方法

相关文章

网友评论

      本文标题:解决 Retrofit 无法缓存 POST 请求

      本文链接:https://www.haomeiwen.com/subject/pjkqjxtx.html