我正在尝试为我的GWT小部件之一合并数据缓存。
我有一个数据源接口(interface)/类,它通过RequestBuilder
和JSON从后端检索一些数据。因为我多次显示小部件,所以我只想检索一次数据。
因此,我尝试提供一个应用程序缓存。天真的方法是在单例对象中使用HashMap
来存储数据。但是,如果支持的话,我也想利用HTML5的localStorage/sessionStorage。
HTML5 localStorage仅支持字符串值。因此,我必须将对象转换为JSON并存储为字符串。但是,以某种方式,我无法提出一种很好的清洁方法。这是我到目前为止所拥有的。
我定义了一个具有两个功能的接口(interface):fetchStatsList()
获取可在小部件中显示的统计信息列表,而fetchStatsData()
获取实际数据。
public interface DataSource {
public void fetchStatsData(Stat stat,FetchStatsDataCallback callback);
public void fetchStatsList(FetchStatsListCallback callback);
}
Stat
类是一个简单的Javascript覆盖类(JavaScriptObject
),带有一些 setter/getter (getName()等)我的DataSource有一个普通的不可实现的实现
RequestBuilderDataSource
,如下所示:public class RequestBuilderDataSource implements DataSource {
@Override
public void fetchStatsList(final FetchStatsListCallback callback) {
// create RequestBuilderRequest, retrieve response and parse JSON
callback.onFetchStatsList(stats);
}
@Override
public void fetchStatsData(List<Stat> stats,final FetchStatsDataCallback callback) {
String url = getStatUrl(stats);
//create RequestBuilderRquest, retrieve response and parse JSON
callback.onFetchStats(dataTable); //dataTable is of type DataTable
}
}
我将大部分代码留给了RequestBuilder,因为它很简单。
这是开箱即用的,但是即使在每个小部件实例之间共享数据时,统计信息列表也会每次都检索到。
为了支持缓存,我添加了一个
Cache
接口(interface)和两个Cache实现(一个用于HTML5 localStorage,一个用于HashMap):public interface Cache {
void put(Object key, Object value);
Object get(Object key);
void remove(Object key);
void clear();
}
我添加了一个新的类
RequestBuilderCacheDataSource
,该类扩展了RequestBuilderDataSource
并在其构造函数中采用了Cache
实例。public class RequestBuilderCacheDataSource extends RequestBuilderDataSource {
private final Cache cache;
publlic RequestBuilderCacheDataSource(final Cache cache) {
this.cache = cache;
}
@Override
public void fetchStatsList(final FetchStatsListCallback callback) {
Object value = cache.get("list");
if (value != null) {
callback.fetchStatsList((List<Stat>)value);
}
else {
super.fetchStatsList(stats,new FetchStatsListCallback() {
@Override
public void onFetchStatsList(List<Stat>stats) {
cache.put("list",stats);
callback.onFetchStatsList(stats);
}
});
super.fetchStatsList(callback);
}
}
@Override
public void fetchStatsData(List<Stat> stats,final FetchStatsDataCallback callback) {
String url = getStatUrl(stats);
Object value = cache.get(url);
if (value != null) {
callback.onFetchStatsData((DataTable)value);
}
else {
super.fetchStatsData(stats,new FetchStatsDataCallback() {
@Override
public void onFetchStatsData(DataTable dataTable) {
cache.put(url,dataTable);
callback.onFetchStatsData(dataTable);
}
});
}
}
}
基本上,新类将在
Cache
中查找值,如果找不到该值,它将在父类中调用fetch函数并拦截回调以将其放入缓存中,然后调用实际的回调。因此,为了同时支持HTML5本地存储和普通的JS HashMap存储,我创建了
Cache
接口(interface)的两个实现:JS HashMap存储:
public class DefaultcacheImpl implements Cache {
private HashMap<Object, Object> map;
public DefaultCacheImpl() {
this.map = new HashMap<Object, Object>();
}
@Override
public void put(Object key, Object value) {
if (key == null) {
throw new NullPointerException("key is null");
}
if (value == null) {
throw new NullPointerException("value is null");
}
map.put(key, value);
}
@Override
public Object get(Object key) {
// Check for null as Cache should not store null values / keys
if (key == null) {
throw new NullPointerException("key is null");
}
return map.get(key);
}
@Override
public void remove(Object key) {
map.remove(key);
}
@Override
public void clear() {
map.clear();
}
}
HTML5 localStorage :
public class LocalStorageImpl implements Cache{
public static enum TYPE {LOCAL,SESSION}
private TYPE type;
private Storage cacheStorage = null;
public LocalStorageImpl(TYPE type) throws Exception {
this.type = type;
if (type == TYPE.LOCAL) {
cacheStorage = Storage.getLocalStorageIfSupported();
}
else {
cacheStorage = Storage.getSessionStorageIfSupported();
}
if (cacheStorage == null) {
throw new Exception("LocalStorage not supported");
}
}
@Override
public void put(Object key, Object value) {
//Convert Object (could be any arbitrary object) into JSON
String jsonData = null;
if (value instanceof List) { // in case it is a list of Stat objects
JSONArray array = new JSONArray();
int index = 0;
for (Object val:(List)value) {
array.set(index,new JSONObject((JavaScriptObject)val));
index = index +1;
}
jsonData = array.toString();
}
else // in case it is a DataTable
{
jsonData = new JSONObject((JavaScriptObject) value).toString();
}
cacheStorage.setItem(key.toString(), jsonData);
}
@Override
public Object get(Object key) {
if (key == null) {
throw new NullPointerException("key is null");
}
String jsonDataString = cacheStorage.getItem(key.toString());
if (jsonDataString == null) {
return null;
}
Object data = null;
Object jsonData = JsonUtils.safeEval(jsonDataString);
if (!key.equals("list"))
data = DataTable.create((JavaScriptObject)data);
else if (jsonData instanceof JsArray){
JsArray<GenomeStat> jsonStats = (JsArray<GenomeStat>)jsonData;
List<GenomeStat> stats = new ArrayList<GenomeStat>();
for (int i = 0;i<jsonStats.length();i++) {
stats.add(jsonStats.get(i));
}
data = (Object)stats;
}
return data;
}
@Override
public void remove(Object key) {
cacheStorage.removeItem(key.toString());
}
@Override
public void clear() {
cacheStorage.clear();
}
public TYPE getType() {
return type;
}
}
帖子有点长,但希望可以澄清我尝试达到的目标。它归结为两个问题:
DefaultCacheImpl
确实很容易存储和检索任何对象。如何在必须转换和解析JSON的localStorage上实现同一目的?我使用的是DataTable
,它需要调用DataTable.create(JavaScriptObject jso)
函数才能正常工作。没有太多的if/else和检查实例,如何解决这个问题? 最佳答案
我的第一个想法是:使其成为两层缓存,而不是两个不同的缓存。从内存中映射开始,因此不需要序列化/反序列化来读取给定的对象,因此在一个位置更改一个对象就可以完全更改它。然后,依靠本地存储为下一页加载保留数据,而无需从服务器提取数据。
我倾向于说跳过 session 存储,因为它不会持续很长时间,但是它确实有其好处。
对于存储/读取数据,我建议您 checkout AutoBeans而不是使用JSO。这样,您可以支持任何类型的数据(可以将其存储为autobean),并且可以将Class参数传递到访存程序中,以指定将从服务器/缓存中读取的数据类型,并将json解码为bean以同样的方式。另外,autobeans更易于定义-无需JSNI。方法可能看起来像这样(请注意,在DataSource及其暗示中,签名是不同的)。
public <T> void fetch(Class<T> type, List<Stat> stats, Callback<T, Throwable> callback);
就是说,
DataTable.create
是什么?如果它已经是JSO,则可以像从RequestBuilder数据读取时一样(可能)像通常那样强制转换为DataTable。我也鼓励不要直接从服务器返回JSON数组,而是将其包装在一个对象中,这是保护您的用户数据不被其他站点读取的最佳实践。 (好的,在重新阅读问题时,对象也不是很好)。与其在这里讨论,不如查看JSON security best practices?
因此,所有这些,首先定义数据(不确定该数据的工作方式,因此请按我的意愿进行整理)
public interface DataTable {
String getTableName();
void setTableName(String tableName);
}
public interface Stat {// not really clear on what this is supposed to offer
String getKey();
void setKey(String key);
String getValue();
String setValue(String value);
}
public interface TableCollection {
List<DataTable> getTables();
void setTables(List<DataTable> tables);
int getRemaining();//useful for not sending all if you have too much?
}
对于autobean,我们定义了一个工厂,当给定
Class
实例和一些数据时,该工厂可以创建我们的任何数据。这些方法中的每一个都可以用作一种构造函数,以在客户端上创建新实例,并且可以将工厂传递给AutoBeanCodex来解码数据。interface DataABF extends AutoBeanFactory {
AutoBean<DataTable> dataTable();
AutoBean<Stat> stat();
AutoBean<TableCollection> tableCollection();
}
将String Object的所有工作委托(delegate)给AutoBeanCodex,但您可能希望在它周围进行一些简单的包装,以使其易于从html5缓存和RequestBuilder结果中进行调用。这里的简单示例:
public class AutoBeanSerializer {
private final AutoBeanFactory factory;
public AutoBeanSerializer(AutoBeanFactory factory) {
this.factory = factory;
}
public String <T> encodeData(T data) {
//first, get the autobean mapped to the data
//probably throw something if we can't find it
AutoBean<T> autoBean = AutoBeanUtils.getAutoBean(data);
//then, encode it
//no factory or type needed here since the AutoBean has those details
return AutoBeanCodex.encode(autoBean);
}
public <T> T decodeData(Class<T> dataType, String json) {
AutoBean<T> bean = AutoBeanCodex.decode(factory, dataType, json);
//unwrap the bean, and return the actual data
return bean.as();
}
}