一. 背景
RxCache 是一个支持 Java 和 Android 的 Local Cache 。
之前的文章给 Java 和 Android 构建一个简单的响应式Local Cache曾详细介绍过它。
RxCache 包含了两级缓存: Memory 和 Persistence 。
下图是 rxcache-core 模块的 uml 类图
rxcache_uml.png
二. 持久层
RxCache 的持久层包括 Disk、DB,分别单独抽象了 Disk、DB 接口并继承 Persistence。
DB 接口:
package com.safframework.rxcache.persistence.db; import com.safframework.rxcache.persistence.Persistence; /** * Created by tony on 2018/10/14. */ public interface DB extends Persistence { }
在 RxCache 的持久层,尝试集成 Android 常用的持久层框架。
2.1 集成 greenDAO
greenDAO 是一款开源的面向 Android 的轻便、快捷的 ORM 框架,将 Java 对象映射到 SQLite 数据库。
首先,创建一个缓存实体 CacheEntity ,它包含 id、key、data、timestamp、expireTime。其中 data 是待缓存的对象并转换成 json 字符串。
@Entity public class CacheEntity { @Id(autoincrement = true) private Long id; public String key; public String data;// 对象转换的 json 字符串 public Long timestamp; public Long expireTime; ...... // getter 、setter }
创建一个单例的 DBService ,并提供返回 CacheEntityDao 的方法。其实,crud 的逻辑也可以放在此处。
public class DBService { private static final String DB_NAME = "cache.db"; private static volatile DBService defaultInstance; private DaoSession daoSession; private DBService(Context context) { DaoMaster.DevOpenHelper helper = new DaoMaster.DevOpenHelper(context, DB_NAME); DaoMaster daoMaster = new DaoMaster(helper.getWritableDatabase()); daoSession = daoMaster.newSession(); } public static DBService getInstance(Context context) { if (defaultInstance == null) { synchronized (DBService.class) { if (defaultInstance == null) { defaultInstance = new DBService(context.getApplicationContext()); } } } return defaultInstance; } public CacheEntityDao getCacheEntityDao(){ return daoSession.getCacheEntityDao(); } }
创建 GreenDAOImpl 实现 DB 接口,实现真正的缓存逻辑。
import com.safframework.rxcache.config.Constant; import com.safframework.rxcache.domain.Record; import com.safframework.rxcache.domain.Source; import com.safframework.rxcache.persistence.converter.Converter; import com.safframework.rxcache.persistence.converter.GsonConverter; import com.safframework.rxcache.persistence.db.DB; import com.safframework.tony.common.utils.Preconditions; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; /** * @FileName: com.safframework.rxcache4a.persistence.db.greendao.GreenDAOImpl * @author: Tony Shen * @date: 2018-10-15 11:50 * @version: V1.0 <描述当前版本功能> */ public class GreenDAOImpl implements DB { private CacheEntityDao dao; private Converter converter; public GreenDAOImpl(CacheEntityDao dao) { this(dao,new GsonConverter()); } public GreenDAOImpl(CacheEntityDao dao, Converter converter) { this.dao = dao; this.converter = converter; } @Override public <T> Record<T> retrieve(String key, Type type) { CacheEntity entity = dao.queryBuilder().where(CacheEntityDao.Properties.Key.eq(key)).unique(); if (entity==null) return null; long timestamp = entity.timestamp; long expireTime = entity.expireTime; T result = null; if (expireTime<0) { // 缓存的数据从不过期 String json = entity.data; result = converter.fromJson(json,type); } else { if (timestamp + expireTime > System.currentTimeMillis()) { // 缓存的数据还没有过期 String json = entity.data; result = converter.fromJson(json,type); } else { // 缓存的数据已经过期 evict(key); } } return result != null ? new Record<>(Source.PERSISTENCE, key, result, timestamp, expireTime) : null; } @Override public <T> void save(String key, T value) { save(key,value, Constant.NEVER_EXPIRE); } @Override public <T> void save(String key, T value, long expireTime) { if (Preconditions.isNotBlanks(key,value)) { CacheEntity entity = new CacheEntity(); entity.setKey(key); entity.setTimestamp(System.currentTimeMillis()); entity.setExpireTime(expireTime); entity.setData(converter.toJson(value)); dao.save(entity); } } @Override public List<String> allKeys() { List<CacheEntity> list = dao.loadAll(); List<String> result = new ArrayList<>(); for (CacheEntity entity:list) { result.add(entity.key); } return result; } @Override public boolean containsKey(String key) { List<String> keys = allKeys(); return Preconditions.isNotBlank(keys) ? keys.contains(key) : false; } @Override public void evict(String key) { CacheEntity entity = dao.queryBuilder().where(CacheEntityDao.Properties.Key.eq(key)).unique(); if (entity!=null) { dao.delete(entity); } } @Override public void evictAll() { dao.deleteAll(); } }
2.2 集成 Room
Room 是 Google 开发的一个 SQLite 对象映射库。 使用它来避免样板代码并轻松地将 SQLite 数据转换为 Java 对象。 Room 提供 SQLite 语句的编译时检查,可以返回 RxJava 和 LiveData Observable。
同样,需要先创建一个 CacheEntity,但是不能共用之前的 CacheEntity。因为 Room、greenDAO 使用的 @Entity
不同。
@Entity public class CacheEntity { @PrimaryKey(autoGenerate = true) private Long id; public String key; public String data;// 对象转换的 json 字符串 public Long timestamp; public Long expireTime; ...... // getter 、setter }
创建一个 CacheEntityDao 用于 crud 的实现。
import java.util.List; import androidx.room.Dao; import androidx.room.Delete; import androidx.room.Insert; import androidx.room.Query; import static androidx.room.OnConflictStrategy.IGNORE; /** * @FileName: com.safframework.rxcache4a.persistence.db.room.CacheEntityDao * @author: Tony Shen * @date: 2018-10-15 16:44 * @version: V1.0 <描述当前版本功能> */ @Dao public interface CacheEntityDao { @Query("SELECT * FROM cacheentity") List<CacheEntity> getAll(); @Query("SELECT * FROM cacheentity WHERE `key` = :key LIMIT 0,1") CacheEntity findByKey(String key); @Insert(onConflict = IGNORE) void insert(CacheEntity entity); @Delete void delete(CacheEntity entity); @Query("DELETE FROM cacheentity") void deleteAll(); }
创建一个 AppDatabase 表示一个数据库的持有者。
import androidx.room.Database; import androidx.room.RoomDatabase; /** * @FileName: com.safframework.rxcache4a.persistence.db.room.AppDatabase * @author: Tony Shen * @date: 2018-10-15 16:40 * @version: V1.0 <描述当前版本功能> */ @Database(entities = {CacheEntity.class}, version = 1) public abstract class AppDatabase extends RoomDatabase { public abstract CacheEntityDao cacheEntityDao(); }
最后,创建 RoomImpl 实现 DB 接口,实现真正的缓存逻辑。
import android.content.Context; import com.safframework.rxcache.config.Constant; import com.safframework.rxcache.domain.Record; import com.safframework.rxcache.domain.Source; import com.safframework.rxcache.persistence.converter.Converter; import com.safframework.rxcache.persistence.converter.GsonConverter; import com.safframework.rxcache.persistence.db.DB; import com.safframework.tony.common.utils.Preconditions; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; import androidx.room.Room; /** * @FileName: com.safframework.rxcache4a.persistence.db.room.RoomImpl * @author: Tony Shen * @date: 2018-10-15 16:46 * @version: V1.0 <描述当前版本功能> */ public class RoomImpl implements DB { private AppDatabase db; private Converter converter; private static final String DB_NAME = "cache"; public RoomImpl(Context context) { this(context,new GsonConverter()); } public RoomImpl(Context context, Converter converter) { this.db = Room.databaseBuilder(context, AppDatabase.class, DB_NAME).build(); this.converter = converter; } @Override public <T> Record<T> retrieve(String key, Type type) { CacheEntity entity = db.cacheEntityDao().findByKey(key); if (entity==null) return null; long timestamp = entity.timestamp; long expireTime = entity.expireTime; T result = null; if (expireTime<0) { // 缓存的数据从不过期 String json = entity.data; result = converter.fromJson(json,type); } else { if (timestamp + expireTime > System.currentTimeMillis()) { // 缓存的数据还没有过期 String json = entity.data; result = converter.fromJson(json,type); } else { // 缓存的数据已经过期 evict(key); } } return result != null ? new Record<>(Source.PERSISTENCE, key, result, timestamp, expireTime) : null; } @Override public <T> void save(String key, T value) { save(key,value, Constant.NEVER_EXPIRE); } @Override public <T> void save(String key, T value, long expireTime) { if (Preconditions.isNotBlanks(key,value)) { CacheEntity entity = new CacheEntity(); entity.setKey(key); entity.setTimestamp(System.currentTimeMillis()); entity.setExpireTime(expireTime); entity.setData(converter.toJson(value)); db.cacheEntityDao().insert(entity); } } @Override public List<String> allKeys() { List<CacheEntity> list = db.cacheEntityDao().getAll(); List<String> result = new ArrayList<>(); for (CacheEntity entity:list) { result.add(entity.key); } return result; } @Override public boolean containsKey(String key) { List<String> keys = allKeys(); return Preconditions.isNotBlank(keys) ? keys.contains(key) : false; } @Override public void evict(String key) { CacheEntity entity = db.cacheEntityDao().findByKey(key); if (entity!=null) { db.cacheEntityDao().delete(entity); } } @Override public void evictAll() { db.cacheEntityDao().deleteAll(); } }
这两种集成方式,都使用 CacheEntity 的 data 来存储对象转换后的 json 字符串。使用这种方式,可以替换成任何的持久层框架。使得 DB 也可以成为 RxCache 的其中一级缓存。
三. 使用
编写单元测试,看一下集成 greenDAO 的效果。
分别测试多种对象的存储、带 ExpireTime 的存储。
import android.content.Context; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import com.safframework.rxcache.RxCache; import com.safframework.rxcache.domain.Record; import com.safframework.rxcache4a.persistence.db.greendao.CacheEntityDao; import com.safframework.rxcache4a.persistence.db.greendao.DBService; import com.safframework.rxcache4a.persistence.db.greendao.GreenDAOImpl; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; /** * @FileName: com.safframework.rxcache4a.GreenDAOImplTest * @author: Tony Shen * @date: 2018-10-15 18:51 * @version: V1.0 <描述当前版本功能> */ @RunWith(AndroidJUnit4.class) public class GreenDAOImplTest { Context appContext; DBService dbService; @Before public void setUp() { appContext = InstrumentationRegistry.getTargetContext(); dbService = DBService.getInstance(appContext); } @Test public void testWithObject() { CacheEntityDao dao = dbService.getCacheEntityDao(); GreenDAOImpl impl = new GreenDAOImpl(dao); impl.evictAll(); RxCache.config(new RxCache.Builder().persistence(impl)); RxCache rxCache = RxCache.getRxCache(); Address address = new Address(); address.province = "Jiangsu"; address.city = "Suzhou"; address.area = "Gusu"; address.street = "ren ming road"; User u = new User(); u.name = "tony"; u.password = "123456"; u.address = address; rxCache.save("user",u); Record<User> record = rxCache.get("user", User.class); assertEquals(u.name, record.getData().name); assertEquals(u.password, record.getData().password); assertEquals(address.city, record.getData().address.city); rxCache.save("address",address); Record<Address> record2 = rxCache.get("address", Address.class); assertEquals(address.city, record2.getData().city); } @Test public void testWithExpireTime() { CacheEntityDao dao = dbService.getCacheEntityDao(); GreenDAOImpl impl = new GreenDAOImpl(dao); impl.evictAll(); RxCache.config(new RxCache.Builder().persistence(impl)); 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); assertNull(record); } }
两个 test case 都顺利通过,表示集成 greenDAO 没有问题。当然,集成 Room 也是一样。
四. 总结
我单独创建了一个项目 RxCache4a 用于整合的 greenDAO、Room 等。
Github 地址: https://github.com/fengzhizi715/RxCache4a
未来,可能对框架增加一些 Annotation,以及增加 Cache 清除的算法。