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

夕陽.JPG

一. 為何要創(chuàng)建這個庫

首先遗嗽,Local Cache 不是類似于 Redis秸仙、Couchbase、Memcached 這樣的分布式 Cache滤港。Local Cache 適用于在單機環(huán)境下鼠冕,對訪問頻率高添寺、更新次數(shù)少的數(shù)據(jù)進行存放。因此懈费,Local Cache 不適合存放大量的數(shù)據(jù)计露。

Local Cache 特別適合于 App,也適合在 Java 的某些場景下使用。

我們的 App 使用 Retrofit 作為網(wǎng)絡框架票罐,并且大量使用 RxJava叉趣,因此我考慮創(chuàng)建一個 RxCache 來緩存一些必要的數(shù)據(jù)。

RxCache 地址:https://github.com/fengzhizi715/RxCache

二. 如何構建 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() 方法包含了失效時間的參數(shù)expireTime,這對于 Local Cache 是比較重要的一個方法蚕礼,超過這個時間烟具,這個數(shù)據(jù)將會失效。

既然是 RxCache奠蹬,對于獲取數(shù)據(jù)肯定需要類似這樣的方法:

<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 時,可以考慮結合這些策略來緩存數(shù)據(jù)狸演。

以 CacheFirstStrategy 為例:

/**
 * 緩存優(yōu)先的策略言蛇,緩存取不到時取接口的數(shù)據(jù)。
 * 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 严沥。

RxCache.png

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();
}

它的默認實現(xiàn) DefaultMemoryImpl 使用 ConcurrentHashMap 來緩存數(shù)據(jù)。

在 extra 模塊還有 Guava Cache中姜、Caffeine 的實現(xiàn)消玄。它們都是成熟的 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 的實現(xiàn)類 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 的抽象實現(xiàn)類 AbstractConverter 的構造方法注入了 Encryptor 接口:

public abstract class AbstractConverter implements Converter {

    private Encryptor encryptor;

    public AbstractConverter() {
    }

    public AbstractConverter(Encryptor encryptor) {

        this.encryptor = encryptor;
    }

    ......
}

Encryptor 接口用于將存儲到 Disk 上的數(shù)據(jù)進行加密和解密蕊蝗,目前 RxCache 支持 AES128 和 DES 兩種加密方式仅乓。不使用 Encryptor 接口,則存儲到 Disk 上的數(shù)據(jù)是明文蓬戚,也就是一串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 的實現(xiàn)使用 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石洗,我還單獨創(chuàng)建了一個項目 RxCache4a: https://github.com/fengzhizi715/RxCache4a

它包含了一個基于 LruCache 的 Memory 實現(xiàn),以及一個基于 MMKV(騰訊開源的key
-value存儲框架) 的 Persistence 實現(xiàn)紧显。

我們目前 App 采用了如下的 MVVM 架構來傳輸數(shù)據(jù):


MVVM.png

未來讲衫,希望能夠通過 RxCache 來整合 Repository 這一層。

五. 總結

目前鸟妙,RxCache 完成了大體的框架焦人,初步可用,接下來打算增加一些 Annotation重父,方便其使用花椭。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市房午,隨后出現(xiàn)的幾起案子矿辽,更是在濱河造成了極大的恐慌,老刑警劉巖郭厌,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件袋倔,死亡現(xiàn)場離奇詭異,居然都是意外死亡折柠,警方通過查閱死者的電腦和手機宾娜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來扇售,“玉大人前塔,你說我怎么就攤上這事〕斜” “怎么了华弓?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長困乒。 經(jīng)常有香客問我寂屏,道長,這世上最難降的妖魔是什么娜搂? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任迁霎,我火速辦了婚禮,結果婚禮上百宇,老公的妹妹穿的比我還像新娘欧引。我一直安慰自己,他們只是感情好恳谎,可當我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布芝此。 她就那樣靜靜地躺著憋肖,像睡著了一般。 火紅的嫁衣襯著肌膚如雪婚苹。 梳的紋絲不亂的頭發(fā)上岸更,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天,我揣著相機與錄音膊升,去河邊找鬼怎炊。 笑死,一個胖子當著我的面吹牛廓译,可吹牛的內(nèi)容都是我干的评肆。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼非区,長吁一口氣:“原來是場噩夢啊……” “哼瓜挽!你這毒婦竟也來了?” 一聲冷哼從身側響起征绸,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤久橙,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后管怠,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體淆衷,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年渤弛,在試婚紗的時候發(fā)現(xiàn)自己被綠了祝拯。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡她肯,死狀恐怖佳头,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情辕宏,我是刑警寧澤畜晰,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布砾莱,位于F島的核電站瑞筐,受9級特大地震影響,放射性物質發(fā)生泄漏腊瑟。R本人自食惡果不足惜聚假,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望闰非。 院中可真熱鬧膘格,春花似錦、人聲如沸财松。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至菜秦,卻和暖如春甜害,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背球昨。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工尔店, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人主慰。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓嚣州,卻偏偏與公主長得像,于是被迫代替她去往敵國和親共螺。 傳聞我的和親對象是個殘疾皇子该肴,可洞房花燭夜當晚...
    茶點故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 用兩張圖告訴你,為什么你的 App 會卡頓? - Android - 掘金 Cover 有什么料璃谨? 從這篇文章中你...
    hw1212閱讀 12,723評論 2 59
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理沙庐,服務發(fā)現(xiàn),斷路器佳吞,智...
    卡卡羅2017閱讀 134,654評論 18 139
  • 關于Mongodb的全面總結 MongoDB的內(nèi)部構造《MongoDB The Definitive Guide》...
    中v中閱讀 31,930評論 2 89
  • 1拱雏、通過CocoaPods安裝項目名稱項目信息 AFNetworking網(wǎng)絡請求組件 FMDB本地數(shù)據(jù)庫組件 SD...
    陽明先生_X自主閱讀 15,980評論 3 119
  • 大明帝國自開創(chuàng)后,朱元璋為了鞏固帝國底扳,加強君權铸抑,在全國各地分封自己的兒子們?yōu)橥酰@個王只有名衷模,沒有實力鹊汛。享受王的...
    煩人的昵稱閱讀 1,592評論 0 3