給 Java 和 Android 構建一個簡單的響應式Local Cache

一. 為何要建立這個庫
首先,Local Cache 不是類似於 Redis、Couchbase、Memcached 這樣的分散式 Cache。Local Cache 適用於在單機環境下,對訪問頻率高、更新次數少的資料進行存放。因此,Local Cache 不適合存放大量的資料。
Local Cache 特別適合於 App,也適合在 Java 的某些場景下使用。
我們的 App 使用 Retrofit 作為網路框架,並且大量使用 RxJava,因此我考慮建立一個 RxCache 來快取一些必要的資料。
RxCache 地址: ofollow,noindex">github.com/fengzhizi71…
二. 如何構建 RxCache
2.1 RxCache 的基本方法
對於 Local Cache,最重要是需要有以下的這些方法:
<T> Record<T> get(String key, Type type); <T> void save(String key, T value); <T> void save(String key, T value, long expireTime); boolean containsKey(String key); Set<String> getAllKeys(); void remove(String key); void clear(); 複製程式碼
其中,有一個 save() 方法包含了失效時間的引數expireTime,這對於 Local Cache 是比較重要的一個方法,超過這個時間,這個資料將會失效。
既然是 RxCache,對於獲取資料肯定需要類似這樣的方法:
<T> Observable<Record<T>> load2Observable(final String key, final Type type) ; <T> Flowable<Record<T>> load2Flowable(final String key, final Type type); <T> Single<Record<T>> load2Single(final String key, final Type type); <T> Maybe<Record<T>> load2Maybe(final String key, final Type type); 複製程式碼
也需要一些 Transformer 的方法,將 RxJava 的被觀察者進行轉換。在 RxCache 中,包含了一些預設的 Transformer 策略,特別是使用 Retrofit 和 RxJava 時,可以考慮結合這些策略來快取資料。
以 CacheFirstStrategy 為例:
/** * 快取優先的策略,快取取不到時取介面的資料。 * Created by tony on 2018/9/30. */ public class CacheFirstStrategy implements ObservableStrategy, FlowableStrategy, MaybeStrategy{ @Override public <T> Publisher<Record<T>> execute(RxCache rxCache, String key, Flowable<T> source, Type type) { Flowable<Record<T>> cache = rxCache.<T>load2Flowable(key, type); Flowable<Record<T>> remote = source .map(new Function<T, Record<T>>() { @Override public Record<T> apply(@NonNull T t) throws Exception { rxCache.save(key, t); return new Record<>(Source.CLOUD, key, t); } }); return cache.switchIfEmpty(remote); } @Override public <T> Maybe<Record<T>> execute(RxCache rxCache, String key, Maybe<T> source, Type type) { Maybe<Record<T>> cache = rxCache.<T>load2Maybe(key, type); Maybe<Record<T>> remote = source .map(new Function<T, Record<T>>() { @Override public Record<T> apply(@NonNull T t) throws Exception { rxCache.save(key, t); return new Record<>(Source.CLOUD, key, t); } }); return cache.switchIfEmpty(remote); } @Override public <T> Observable<Record<T>> execute(RxCache rxCache, String key, Observable<T> source, Type type) { Observable<Record<T>> cache = rxCache.<T>load2Observable(key, type); Observable<Record<T>> remote = source .map(new Function<T, Record<T>>() { @Override public Record<T> apply(@NonNull T t) throws Exception { rxCache.save(key, t); return new Record<>(Source.CLOUD, key, t); } }); return cache.switchIfEmpty(remote); } } 複製程式碼
2.2 Memory
RxCache 包含了兩級快取: Memory 和 Persistence 。

Memory:
package com.safframework.rxcache.memory; import com.safframework.rxcache.domain.Record; import java.util.Set; /** * Created by tony on 2018/9/29. */ public interface Memory { <T> Record<T> getIfPresent(String key); <T> void put(String key, T value); <T> void put(String key, T value, long expireTime); Set<String> keySet(); boolean containsKey(String key); void evict(String key); void evictAll(); } 複製程式碼
它的預設實現 DefaultMemoryImpl 使用 ConcurrentHashMap 來快取資料。
在 extra 模組還有 Guava Cache、Caffeine 的實現。它們都是成熟的 Local Cache,如果不想使用 DefaultMemoryImpl ,完全可以使用 extra 模組成熟的替代方案。
2.3 Persistence
Persistence 的介面跟 Memory 很類似:
package com.safframework.rxcache.persistence; import com.safframework.rxcache.domain.Record; import java.lang.reflect.Type; import java.util.List; /** * Created by tony on 2018/9/28. */ public interface Persistence { <T> Record<T> retrieve(String key, Type type); <T> void save(String key, T value); <T> void save(String key, T value, long expireTime); List<String> allKeys(); boolean containsKey(String key); void evict(String key); void evictAll(); } 複製程式碼
由於,考慮到持久層可能包括 Disk、DB。於是單獨抽象了一個 Disk 介面繼承 Persistence。
在 Disk 的實現類 DiskImpl 中,它的構造方法注入了 Converter 介面:
public class DiskImpl implements Disk { private File cacheDirectory; private Converter converter; public DiskImpl(File cacheDirectory,Converter converter) { this.cacheDirectory = cacheDirectory; this.converter = converter; } ...... } 複製程式碼
Converter 介面用於物件儲存到檔案的序列化和反序列化,目前支援 Gson 和 FastJSON。
Converter 的抽象實現類 AbstractConverter 的構造方法注入了 Encryptor 介面:
public abstract class AbstractConverter implements Converter { private Encryptor encryptor; public AbstractConverter() { } public AbstractConverter(Encryptor encryptor) { this.encryptor = encryptor; } ...... } 複製程式碼
Encryptor 介面用於將儲存到 Disk 上的資料進行加密和解密,目前 RxCache 支援 AES128 和 DES 兩種加密方式。不使用 Encryptor 介面,則儲存到 Disk 上的資料是明文,也就是一串json字串。
三. 支援 Java
在 example 模組下,包括了一些常見 Java 使用的例子。
例如,最簡單的使用:
import com.safframework.rxcache.RxCache; import com.safframework.rxcache.domain.Record; import domain.User; import io.reactivex.Observable; import io.reactivex.functions.Consumer; /** * Created by tony on 2018/9/29. */ public class Test { public static void main(String[] args) { RxCache.config(new RxCache.Builder()); RxCache rxCache = RxCache.getRxCache(); User u = new User(); u.name = "tony"; u.password = "123456"; rxCache.save("test",u); Observable<Record<User>> observable = rxCache.load2Observable("test", User.class); observable.subscribe(new Consumer<Record<User>>() { @Override public void accept(Record<User> record) throws Exception { User user = record.getData(); System.out.println(user.name); System.out.println(user.password); } }); } } 複製程式碼
帶 ExpireTime 的快取測試:
import com.safframework.rxcache.RxCache; import com.safframework.rxcache.domain.Record; import domain.User; /** * Created by tony on 2018/10/5. */ public class TestWithExpireTime { public static void main(String[] args) { RxCache.config(new RxCache.Builder()); RxCache rxCache = RxCache.getRxCache(); User u = new User(); u.name = "tony"; u.password = "123456"; rxCache.save("test",u,2000); try { Thread.sleep(2500); } catch (InterruptedException e) { e.printStackTrace(); } Record<User> record = rxCache.get("test", User.class); if (record==null) { System.out.println("record is null"); } } } 複製程式碼
跟 Spring 整合並且 Memory 的實現使用 GuavaCacheImpl:
import com.safframework.rxcache.RxCache; import com.safframework.rxcache.extra.memory.GuavaCacheImpl; import com.safframework.rxcache.memory.Memory; import org.springframework.beans.factory.annotation.Configurable; import org.springframework.context.annotation.Bean; /** * Created by tony on 2018/10/5. */ @Configurable public class ConfigWithGuava { @Bean public Memory guavaCache(){ return new GuavaCacheImpl(100); } @Bean public RxCache.Builder rxCacheBuilder(){ return new RxCache.Builder().memory(guavaCache()); } @Bean public RxCache rxCache() { RxCache.config(rxCacheBuilder()); return RxCache.getRxCache(); } } 複製程式碼
測試一下剛才的整合:
import com.safframework.rxcache.RxCache; import com.safframework.rxcache.domain.Record; import domain.User; import io.reactivex.Observable; import io.reactivex.functions.Consumer; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; /** * Created by tony on 2018/10/5. */ public class TestWithGuava { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithGuava.class); RxCache rxCache = ctx.getBean(RxCache.class); User u = new User(); u.name = "tony"; u.password = "123456"; rxCache.save("test",u); Observable<Record<User>> observable = rxCache.load2Observable("test", User.class); observable.subscribe(new Consumer<Record<User>>() { @Override public void accept(Record<User> record) throws Exception { User user = record.getData(); System.out.println(user.name); System.out.println(user.password); } }); } } 複製程式碼
四. 支援 Android
為了更好地支援 Android,我還單獨建立了一個專案 RxCache4a: github.com/fengzhizi71…
它包含了一個基於 LruCache 的 Memory 實現,以及一個基於 MMKV(騰訊開源的key -value儲存框架) 的 Persistence 實現。
我們目前 App 採用瞭如下的 MVVM 架構來傳輸資料:

未來,希望能夠通過 RxCache 來整合 Repository 這一層。
五. 總結
目前, RxCache 完成了大體的框架,初步可用,接下來打算增加一些 Annotation,方便其使用。