前言
按照HTTP缓存机制和REST API设计规范,我们不应该缓存POST请求结果, 所以Okhttp官方也没有实现对POST请求结果进行缓存,以下是Okhttp源码注释
// 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.
但是现实社会很残酷,由于各种原因, 我们身边有很多用 POST请求当作GET使用请求的API. 对于这种情况,我们就要自己实现POST请求缓存链了.
本文将讲述Okhttp3+Retrofit实现POST请求缓存链过程, 当然也可以用于GET请求,但是不建议那么做,因为Okhttp对缓存GET请求支持的很完美.
特点
如果缓存有数据,并且数据没有过期,那么直接取缓存数据;
如果缓存过期,则直接从网络获取数据;
支持直接从缓存中读数据
支持忽略缓存,直接从网络获取数据
支持自由精确地配置缓存有效时间
内存缓存
很简单,直接封装android.support.v4.util.LruCache类, 外加上过期时间判断即可
public class MemoryCache {
private final LruCache<String, Entry> cache;
private final List<String> keys = new ArrayList<>();
public MemoryCache(int maxSize) {
this.cache = new LruCache<>(maxSize);
}
private void lookupExpired() {
Completable.fromAction(
() -> {
String key;
for (int i = 0; i < keys.size(); i++) {
key = keys.get(i);
Entry value = cache.get(key);
if (value != null && value.isExpired()) {
remove(key);
}
}
})
.subscribeOn(Schedulers.single())
.subscribe();
}
@CheckForNull
public synchronized Entry get(String key) {
Entry value = cache.get(key);
if (value != null && value.isExpired()) {
remove(key);
lookupExpired();
return null;
}
lookupExpired();
return value;
}
public synchronized Entry put(String key, Entry value) {
if (!keys.contains(key)) {
keys.add(key);
}
Entry oldValue = cache.put(key, value);
lookupExpired();
return oldValue;
}
public Entry remove(String key) {
keys.remove(key);
return cache.remove(key);
}
public Map<String, Entry> snapshot() {
return cache.snapshot();
}
public void trimToSize(int maxSize) {
cache.trimToSize(maxSize);
}
public int createCount() {
return cache.createCount();
}
public void evictAll() {
cache.evictAll();
}
public int evictionCount() {
return cache.evictionCount();
}
public int hitCount() {
return cache.hitCount();
}
public int maxSize() {
return cache.maxSize();
}
public int missCount() {
return cache.missCount();
}
public int putCount() {
return cache.putCount();
}
public int size() {
return cache.size();
}
@Immutable
public static final class Entry {
@SerializedName("data")
public final Object data;
@SerializedName("ttl")
public final long ttl;
}
}
硬盘缓存
同样很简单,直接参照Okhttp的Cache类逻辑, 直接封装DiskLruCache类, 外加上过期时间判断即可.
public final class DiskCache implements Closeable, Flushable {
/**
* Unlike {@link okhttp3.Cache} ENTRY_COUNT = 2
* We don't save the CacheHeader and Respond in two separate files
* Instead, we wrap them in {@link Entry}
*/
private static final int ENTRY_COUNT = 1;
private static final int VERSION = 201105;
private static final int ENTRY_METADATA = 0;
private final DiskLruCache cache;
public DiskCache(File directory, long maxSize) {
cache = DiskLruCache.create(FileSystem.SYSTEM, directory, VERSION, ENTRY_COUNT, maxSize);
}
public Entry get(String key) {
DiskLruCache.Snapshot snapshot;
try {
snapshot = cache.get(key);
if (snapshot == null) {
return null;
}
} catch (IOException e) {
return null;
}
try {
BufferedSource source = Okio.buffer(snapshot.getSource(0));
String json = source.readUtf8();
source.close();
Util.closeQuietly(snapshot);
return DataLayerUtil.fromJson(json, null, Entry.class);
} catch (IOException e) {
Util.closeQuietly(snapshot);
return null;
}
}
public void put(String key, Entry entry) {
DiskLruCache.Editor editor = null;
try {
editor = cache.edit(key);
if (editor != null) {
BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));
sink.writeUtf8(entry.toString());//Entry.toString() is json String
sink.close();
editor.commit();
}
} catch (IOException e) {
abortQuietly(editor);
}
}
public void remove(String key) throws IOException {
cache.remove(key);
}
private void abortQuietly(DiskLruCache.Editor editor) {
try {
if (editor != null) {
editor.abort();
}
} catch (IOException ignored) {
}
}
public void initialize() throws IOException {
cache.initialize();
}
public void delete() throws IOException {
cache.delete();
}
public void evictAll() throws IOException {
cache.evictAll();
}
public long size() throws IOException {
return cache.size();
}
public long maxSize() {
return cache.getMaxSize();
}
public File directory() {
return cache.getDirectory();
}
public boolean isClosed() {
return cache.isClosed();
}
@Override
public void flush() throws IOException {
cache.flush();
}
@Override
public void close() throws IOException {
cache.close();
}
/**
* Data and metadata for an entry returned by the cache.
* It's extracted from android Volley library.
* See {@code https://github.com/google/volley}
*/
@Immutable
public static final class Entry {
/**
* The data returned from cache.
* Use {@link com.thepacific.data.common.DataLayerUtil#toJsonByteArray(Object, Gson)}
* to serialize a data object
*/
@SerializedName("data")
public final byte[] data;
/**
* Time to live(TTL) for this record
*/
@SerializedName("ttl")
public final long ttl;
/**
* Soft TTL for this record
*/
@SerializedName("softTtl")
public final long softTtl;
/**
* @return To a json String
*/
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("{")
.append("data=")
.append(Arrays.toString(data))
.append(", ttl=")
.append(ttl)
.append(", softTtl=")
.append(softTtl)
.append("}");
return builder.toString();
}
/**
* True if the entry is expired.
*/
public boolean isExpired() {
return this.ttl < System.currentTimeMillis();
}
/**
* True if a refresh is needed from the original data source.
*/
public boolean refreshNeeded() {
return this.softTtl < System.currentTimeMillis();
}
}
实现Repository
写一个Repository<T, R>,泛型T代表请求参数类型(如UserQuery),泛型R代表请求结果类型(如User)
/**
* A repository can get cached data {@link Repository#get(Object)}, or force
* a call to network(skipping cache) {@link Repository#fetch(Object, boolean)}
*/
public abstract class Repository<T, R> {
protected final Gson gson;
protected final DiskCache diskCache;
protected final MemoryCache memoryCache;
protected final OnAccessFailure onAccessFailure;
protected String key;
public Repository(Gson gson,
DiskCache diskCache,
MemoryCache memoryCache,
OnAccessFailure onAccessFailure) {
this.gson = gson;
this.diskCache = diskCache;
this.memoryCache = memoryCache;
this.onAccessFailure = onAccessFailure;
}
/**
* Return an Observable of {@link Source <R>} for request query
* Data will be returned from oldest non expired source
* Sources are memory cache, disk cache, finally network
*/
@Nonnull
public final Observable<Source<R>> get(@Nonnull final T query) {
ExecutorUtil.requireWorkThread();
return stream(query)
.flatMap(it -> {
if (it.status == Status.SUCCESS) {
return Observable.just(it);
}
return load(query);
})
.flatMap(it -> {
if (it.status == Status.SUCCESS) {
return Observable.just(it);
}
return fetch(query, true);
});
}
/***
* @param query query parameters
* @param persist true for persisting data to disk
* @return an Observable of R for requested query skipping Memory & Disk Cache
*/
@Nonnull
public final Observable<Source<R>> fetch(@Nonnull final T query, boolean persist) {
ExecutorUtil.requireWorkThread();
Preconditions.checkNotNull(query);
key = getKey(query);
return dispatchNetwork().flatMap(it -> {
if (it.isSuccess()) {
R newData = it.data();
if (isIrrelevant(newData)) {
return Observable.just(Source.irrelevant());
}
long ttl = DataLayerUtil.elapsedTimeMillis(ttl());
long softTtl = DataLayerUtil.elapsedTimeMillis(softTtl());
long now = System.currentTimeMillis();
Preconditions.checkState(ttl > now && softTtl > now && ttl >= softTtl);
if (persist) {
byte[] bytes = DataLayerUtil.toJsonByteArray(newData, gson);
diskCache.put(key, DiskCache.Entry.create(bytes, ttl, softTtl));
} else {
clearDiskCache();
}
memoryCache.put(key, MemoryCache.Entry.create(newData, ttl));
return Observable.just(Source.success(newData));
}
IoError ioError = new IoError(it.message(), it.code());
if (isAccessFailure(it.code())) {
diskCache.evictAll();
memoryCache.evictAll();
ExecutorUtil.postToMainThread(() -> onAccessFailure.run(ioError));
return Observable.empty();
}
memoryCache.remove(key);
clearDiskCache();
return Observable.just(Source.failure(ioError));
});
}
/***
* @param query query parameters
* @return an Observable of R for requested from Disk Cache
*/
@Nonnull
public final Observable<Source<R>> load(@Nonnull final T query) {
ExecutorUtil.requireWorkThread();
Preconditions.checkNotNull(query);
key = getKey(query);
return Observable.defer(() -> {
DiskCache.Entry diskEntry = diskCache.get(key);
if (diskEntry == null) {
return Observable.just(Source.irrelevant());
}
R newData = gson.fromJson(DataLayerUtil.byteArray2String(diskEntry.data), dataType());
if (diskEntry.isExpired() || isIrrelevant(newData)) {
memoryCache.remove(key);
clearDiskCache();
return Observable.just(Source.irrelevant());
}
memoryCache.put(key, MemoryCache.Entry.create(newData, diskEntry.ttl));
return Observable.just(Source.success(newData));
});
}
/***
* @param query query parameters
* @return an Observable of R for requested from Memory Cache with refreshing query
* It differs with {@link Repository#stream()}
*/
@Nonnull
public final Observable<Source<R>> stream(@Nonnull final T query) {
Preconditions.checkNotNull(query);
key = getKey(query);
return stream();
}
/***
* @return an Observable of R for requested from Memory Cache without refreshing query
* It differs with {@link Repository#stream(Object)}
*/
@Nonnull
public final Observable<Source<R>> stream() {
return Observable.defer(() -> {
MemoryCache.Entry memoryEntry = memoryCache.get(key);
//No need to check isExpired(), MemoryCache.get(key) has already done
if (memoryEntry == null) {
return Observable.just(Source.irrelevant());
}
R newData = (R) memoryEntry.data;
if (isIrrelevant(newData)) {
return Observable.just(Source.irrelevant());
}
return Observable.just(Source.success(newData));
});
}
/***
* @return an R from Memory Cache
*/
@Nonnull
public final R memory() {
MemoryCache.Entry memoryEntry = memoryCache.get(key);
if (memoryEntry == null) {
throw new IllegalStateException("Not supported");
}
R newData = (R) memoryEntry.data;
if (isIrrelevant((R) memoryEntry.data)) {
throw new IllegalStateException("Not supported");
}
return newData;
}
public final void clearMemoryCache() {
memoryCache.remove(key);
}
public final void clearDiskCache() {
ExecutorUtil.requireWorkThread();
try {
diskCache.remove(key);
} catch (IOException ignored) {
}
}
/**
* @return default network cache time is 10. It must be {@code TimeUnit.MINUTES}
*/
protected int ttl() {
return 10;
}
/**
* @return default refresh cache time is 5. It must be {@code TimeUnit.MINUTES}
*/
protected final int softTtl() {
return 5;
}
/**
* @param code HTTP/HTTPS error code
* @return some server does't support standard authorize rules
*/
protected boolean isAccessFailure(final int code) {
return code == 403 || code == 405;
}
/**
* @return to make sure never returning empty or null data
*/
protected abstract boolean isIrrelevant(R data);
/**
* @return request HTTP/HTTPS API
*/
protected abstract Observable<Envelope<R>> dispatchNetwork();
/**
* @return cache key
*/
protected abstract String getKey(T query);
/**
* @return gson deserialize Class type for R {@code Type typeOfT = R.class} for List<R> {@code
* Type typeOfT = new TypeToken<List<R>>() { }.getType()}
*/
protected abstract Type dataType();
}
使用
@Test
public void testGet() {
userRepo.get(userQuery)
.onErrorReturn(e -> Source.failure(e))
.startWith(Source.inProgress())
.subscribe(it -> {
switch (it.status) {
case IN_PROGRESS:
System.out.println("Show Loading Dialog===============");
break;
case IRRELEVANT:
System.out.println("Empty Data===============");
break;
case ERROR:
System.out.println("Error Occur===============");
break;
case SUCCESS:
System.out.println("Update UI===============");
break;
default:
throw new UnsupportedOperationException();
}
});
assertEquals(2, userRepo.memory().size());
}
源码
完整源码请到点击,并查看data模块,具体使用请参照单元测试代码
此外,因为时间原因,现状态的源码属于雏形阶段的代码,代码多处地方存在不合理或者错误. 09月05日前会把生产线上的代码完整后上传到github