пятница, 2 октября 2020 г.

Apache Ignite cache: Создаём интерфейсы репозитория

 Решил сделать два интерфейса репозитория:

- с доступом на чтение;

- с полным доступом.

Доступ на чтение будем предоставлять пользователям кеша. Какой в этом смысл? Смысл такой, что наши пользователи не смогут писать в кеш. Смогут только читать из него. Такова идея этого проекта. Если в каком-то другом проекте понадобится доступ пользователям на запись в кеш, тогда разделять репозиторий на два интерфейса будет не нужно.

Вот такие два интерфейса получились:

public interface ReadCacheRepository<K, V> {

    Optional<V> findById(K id);

    boolean existsById(K id);

    long count();
}

public interface CacheRepository<K, V> extends ReadCacheRepository<K, V> {

    void save(V value);

    void deleteById(K key);
}

Как я уже писал ранее, кеш Apache Ignite имеет интерфейс почти такой же, как у HashMap.

Apache Ignite cache: Пишем парсер файлов

 Не успел я написать утилиту для работы с архивными файлами, как пришла пора отправить её на заслуженный отдых.


Когда писал парсер JSON-файла, вспомнил, что есть библиотека java.uti.zip и когда-то давно я с её помощью читал заархивированные в zip-формате файлы, без предварительного их разархивирования. Посмотрел в документацию и нашёл, что ровно то же самое можно делать и с gzip-файлами.


Отлично. Одним рефакторингом мы избавляемся:
- от утилиты,
- от необходимости придумывать, из какого класса её вызывать,
- от необходимости её тестировать и сопровождать,
- от распакованных файлов, которые будут занимать лишнее место на диске.

Теперь наш парсер будет читать файлы из архивов:

    public <T> void parse(String path, Function<? super String, T> create, Consumer<T> save) {
        log.info("Start parsing file: {}", path);
        try (FileInputStream fis = new FileInputStream(path);
             GZIPInputStream gzis = new GZIPInputStream(fis);
             InputStreamReader isr = new InputStreamReader(gzis, StandardCharsets.UTF_8);
             BufferedReader br = new BufferedReader(isr)) {
            br.lines()
                .parallel()
                .map(create)
                .forEach(save);
            log.info("Finish parsing file: {}", path);
        } catch (Exception e) {
            log.error("Error parsing file: {}", path, e);
        }
    }

На вход метод получает путь к файлу и две функции: функцию, которая будет преобразовывать строки из JSON-файла в нашу модель Review и функцию, которая будет сохранять распарсенные данные в кеш.

Как это работает, можно посмотреть в тесте:

    @Test
    public void parseFile() {
        Gson gson = new Gson();
        CacheRepository<String, Review> reviewRepository = new ReviewTestRepository();
        String path = Objects.requireNonNull(
                getClass().getClassLoader().getResource("reviews_Electronics_5_short.json.gz")
        ).getPath();
        FileParser parser = new FileParser();
        parser.parse(path, line -> gson.fromJson(line, Review.class), reviewRepository::save);

        assertEquals(26, reviewRepository.count());
    }

Apache Ignite cache: Пишем утилиту для работы с архивными файлами

 Так как датасет мы скачиваем заархивированным в формат gzip, для дальнейшей работы с ним нам необходимо будет разархивировать его.

Для этого я решил написать утилитный класс:

public static void decompressGzip(File input, File output) throws IOException {
        try (GZIPInputStream in = new GZIPInputStream(new FileInputStream(input))){
            try (FileOutputStream out = new FileOutputStream(output)){
                byte[] buffer = new byte[1024];
                int len;
                while((len = in.read(buffer)) != -1){
                    out.write(buffer, 0, len);
                }
            } catch (Exception e) {
                log.error("Error decompress file. Input: {} Output: {}",
                        input.getAbsolutePath(), output.getAbsolutePath(), e);
            }
        }
    }

Работает утилита шустро и свою задачу выполняет. Тест это подтверждает:

@Test
    public void testDecompressGzip() throws IOException {
        File input = new File(resources + "reviews_Electronics_5_short.json1.gz");
        File output = new File(resources + "reviews_Electronics_5_short1.json");

        GzipUtility.decompressGzip(input, output);
        Path path = Paths.get(resources + "reviews_Electronics_5_short1.json");

        assertTrue(Files.exists(path));
    }

Скорее всего, я помещу вызов этой утилиты в класс Downloader.

Apache Ignite cache: Пишем загрузчик датасета

 

Для написания загрузчика датасета я решил не использовать сторонние библиотеки, чтобы не добавлять лишних зависимостей в код проекта. Недолго думая, для реализации загрузчика выбрал java NIO, как более современный по сравнению с java IO, фрэймворк.

Загрузчик получился простым и прямым как палка. Для начала, нам этого хватит. Возможно, в будущем мы захотим сделать многопоточный загрузчик с возобновлением прерванной загрузки и прочими маленькими радостями.

Код загрузчик:


 Что в загрузчике интересного:

- захардкожены URL для загрузки и имя файла. Позже мы можем это исправить и перенести все константы во внешний файл с настройками;
- согласно заверениям, метод .transferFrom() более эффективен, чем простое чтение из потока, использующего буфер. В зависимости от операционной системы, данные могут быть перемещены напрямую из кеша файловой системы в наш файл, без копирования в память приложения. Что ж, надеюсь это так. В любом случае, позже у нас будет возможность изменить реализацию загрузчика.

Для проверки работоспособности загрузчика, пишем тест.
Для начала, это будет интеграционный тест, который будет скачивать реальный файл с реального сервера и проверять, что он загрузился. В дальнейшем, с развитием приложения, мы напишем юнит-тесты с моками и всем прекрасным, что позволит нам не ждать 5 минут загрузки датасета и, в то же время, будет проверять правильность написанного нами кода.


Весь код можно посмотреть на github.


Apache Ignite cache: Пишем модельку

 Совсем короткий пост о том, какой будет наша модель, в которую мы будем парсить JSON.

На основе анализа JSON файла решил сделать вот такую модель:

@Data
public class Review {

    private String id;
    private String reviewerID;
    private String asin;
    private String reviewerName;
    private int[] helpful;
    private String reviewText;
    private double overall;
    private String summary;
    private long unixReviewTime;
    private String reviewTime;
}

Что в ней интересного:

- аннотация @Data - это аннотация фреймворка  Lombok, которая добавляет в наш файл геттеры, сеттеры, equals(), hashcode(), toString();
- поле id - его нет в JSON файле, но оно нам понадобится для добавления наших данных в кеш, т.к. кеш Apache Ignite устроен по типу ключ - значение и имеет почти все те же методы, что есть в HashMap.
Заполнить это поле я решил простой конкатенацией строк reviewerID и asin. Первый из них - это ID пользователя, оставившего отзыв. Второй - ID товара, о котором был оставлен отзыв. Вроде бы это сочетание - reviewerID + asin - не повторяется в JSON файле.

На этом про модель всё.