Кэширование в приложении / виджете GWT с помощью HTML5 localStorage - PullRequest
6 голосов
/ 14 февраля 2012

Я пытаюсь включить кэш данных для одного из моих виджетов GWT.
У меня есть интерфейс / класс источника данных, который извлекает некоторые данные из моего бэкэнда через RequestBuilder и JSON.Поскольку я отображаю виджет несколько раз, я хочу получить данные только один раз.

Итак, я попытался зайти с кешем приложений.Наивный подход заключается в использовании HashMap в одноэлементном объекте для хранения данных.Однако я также хочу использовать HTML5 localStorage / sessionStorage, если поддерживается.
HTML5 localStorage поддерживает только строковые значения.Поэтому я должен преобразовать свой объект в JSON и сохранить в виде строки.Однако, так или иначе, я не могу придумать хороший чистый способ сделать это.вот что у меня есть.

Я определяю интерфейс с двумя функциями: fetchStatsList() выбирает список статистики, которая может отображаться в виджете, а fetchStatsData() выбирает фактические данные.

public interface DataSource {
    public void fetchStatsData(Stat stat,FetchStatsDataCallback callback);
    public void fetchStatsList(FetchStatsListCallback callback);
}

Класс Stat - это простой класс наложения Javascript (JavaScriptObject) с некоторыми получателями (getName () и т. Д.). У меня есть обычная не кэшируемая реализация RequestBuilderDataSource моего DataSource, котораявыглядит следующим образом:

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 и две реализации 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 и, если он не найден, он будет вызывать функцию выборки вродительский класс и перехватить обратный вызов, чтобы поместить его в кеш, а затем вызвать реальный обратный вызов.
Итак, для поддержки как локального хранилища HTML5, так и обычного хранилища JS HashMap, я создал две реализации моего Cache интерфейса:

JS HashMap storage :

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

Сообщение получилось немного длинным, но, надеюсь, прояснилосьчего я пытаюсь достичь.Он сводится к двум вопросам:

  1. Отзывы о дизайне / архитектуре этого подхода (например, создание подкласса RequestBilderDataSource для функции кэширования и т. Д.).Может ли это быть улучшено (это, вероятно, больше связано с общим дизайном, чем конкретно с GWT).
  2. С DefaultCacheImpl действительно легко хранить и извлекать любые произвольные объекты.Как я могу добиться того же с помощью localStorage, где мне нужно конвертировать и анализировать JSON?Я использую DataTable, который требует вызова функции DataTable.create(JavaScriptObject jso) для работы.Как я могу решить эту проблему без многих if / else и экземпляров проверок?

1 Ответ

6 голосов
/ 14 февраля 2012

Мои первые мысли: создать два слоя кеша, а не два разных кеша.Начните с карты в памяти, поэтому для считывания заданного объекта не требуется сериализация / десериализация, и поэтому изменение объекта в одном месте изменяет его во всех.Затем положитесь на локальное хранилище, чтобы хранить данные для загрузки следующей страницы, избегая необходимости извлечения данных с сервера.

Я бы сказал пропустить сеанс хранения, так как это не долго, но у него есть свои преимущества.

Для хранения / чтения данных я бы рекомендовал проверить AutoBeansвместо использования JSO.Таким образом, вы можете поддерживать любой тип данных (который может быть сохранен как autobean) и можете передать параметр Class в сборщик, чтобы указать, какой тип данных вы будете читать с сервера / кеша, и декодировать json в bean-компонент.таким же образом.В качестве дополнительного бонуса автобин легче определить - JSNI не требуется.Метод может выглядеть примерно так (обратите внимание, что в DataSource и его значении сигнатура отличается).

public <T> void fetch(Class<T> type, List<Stat> stats, Callback<T, Throwable> callback);

Тем не менее, что такое DataTable.create?Если это уже JSO, вы можете просто привести к DataTable, как вы (вероятно) обычно делаете при чтении из данных RequestBuilder.

Я бы также рекомендовал не возвращать массив JSON непосредственно с сервера, а заключать его в оболочкув объекте, как лучшая практика для защиты данных ваших пользователей от чтения другими сайтами.(Хорошо, при перечитывании вопросов, объекты тоже не очень хороши).Вместо того чтобы обсуждать это здесь, ознакомьтесь с рекомендациями по обеспечению безопасности JSON?

Итак, все это говорит, сначала определите данные (не совсем уверены, как эти данные предназначены для работы, поэтому простосоставляя как я)

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?
}

Для автобинов мы определяем фабрику, которая может создавать любые наши данные при наличии экземпляра Class и некоторых данных.Каждый из этих методов может использоваться как своего рода конструктор для создания нового экземпляра на клиенте, а фабрика может быть передана в AutoBeanCodex для декодирования данных.

interface DataABF extends AutoBeanFactory {
    AutoBean<DataTable> dataTable();
    AutoBean<Stat> stat();
    AutoBean<TableCollection> tableCollection();
}

Делегировать всю работу String <=>Возьмите объект в 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();
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...