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

lotus_ruan發表於2021-09-09

一. 為何要建立這個庫

首先,Local Cache 不是類似於 Redis、Couchbase、Memcached 這樣的分散式 Cache。Local Cache 適用於在單機環境下,對訪問頻率高、更新次數少的資料進行存放。因此,Local Cache 不適合存放大量的資料。

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

我們的 App 使用 Retrofit 作為網路框架,並且大量使用 RxJava,因此我考慮建立一個 RxCache 來快取一些必要的資料。

RxCache 地址:

二. 如何構建 RxCache

2.1 RxCache 的基本方法

對於 Local Cache,最重要是需要有以下的這些方法:

T> RecordT> 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);

SetString> getAllKeys();

void remove(String key);

void clear();

其中,有一個 save() 方法包含了失效時間的引數expireTime,這對於 Local Cache 是比較重要的一個方法,超過這個時間,這個資料將會失效。

既然是 RxCache,對於獲取資料肯定需要類似這樣的方法:

T> ObservableRecordT>> load2Observable(final String key, final Type type) ;

T> FlowableRecordT>> load2Flowable(final String key, final Type type);

T> SingleRecordT>> load2Single(final String key, final Type type);

T> MaybeRecordT>> 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> PublisherRecordT>> execute(RxCache rxCache, String key, FlowableT> source, Type type) {

        FlowableRecordT>> cache = rxCache.>load2Flowable(key, type);

        FlowableRecordT>> remote = source
                .map(new FunctionT, RecordT>>() {
                    @Override
                    public RecordT> apply(@NonNull T t) throws Exception {

                        rxCache.save(key, t);

                        return new Record>(Source.CLOUD, key, t);
                    }
                });

        return cache.switchIfEmpty(remote);
    }

    @Override
    public T> MaybeRecordT>> execute(RxCache rxCache, String key, MaybeT> source, Type type) {

        MaybeRecordT>> cache = rxCache.>load2Maybe(key, type);

        MaybeRecordT>> remote = source
                .map(new FunctionT, RecordT>>() {
                    @Override
                    public RecordT> apply(@NonNull T t) throws Exception {

                        rxCache.save(key, t);

                        return new Record>(Source.CLOUD, key, t);
                    }
                });

        return cache.switchIfEmpty(remote);
    }

    @Override
    public T> ObservableRecordT>> execute(RxCache rxCache, String key, ObservableT> source, Type type) {

        ObservableRecordT>> cache = rxCache.>load2Observable(key, type);

        ObservableRecordT>> remote = source
                .map(new FunctionT, RecordT>>() {
                    @Override
                    public RecordT> 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> RecordT> getIfPresent(String key);

    T> void put(String key, T value);

    T> void put(String key, T value, long expireTime);

    SetString> 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> RecordT> retrieve(String key, Type type);

    T> void save(String key, T value);

    T> void save(String key, T value, long expireTime);

    ListString> 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 上的資料進行加密和解密,目前 支援 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);

        ObservableRecordUser>> observable = rxCache.load2Observable("test", User.class);

        observable.subscribe(new ConsumerRecordUser>>() {

            @Override
            public void accept(RecordUser> 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();
        }

        RecordUser> 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);

        ObservableRecordUser>> observable = rxCache.load2Observable("test", User.class);

        observable.subscribe(new ConsumerRecordUser>>() {
            @Override
            public void accept(RecordUser> record) throws Exception {

                User user = record.getData();
                System.out.println(user.name);
                System.out.println(user.password);
            }
        });
    }
}

四. 支援 Android

為了更好地支援 Android,我還單獨建立了一個專案 RxCache4a:

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

我們目前 App 採用瞭如下的 MVVM 架構來傳輸資料:
圖片描述

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

五. 總結

目前, 完成了大體的框架,初步可用,接下來打算增加一些 Annotation,方便其使用。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1762/viewspace-2814791/,如需轉載,請註明出處,否則將追究法律責任。

相關文章