Учебные материалы

Кеширование данных с Spring

В этом уроке освещается процесс включения кеширования Spring бина.

Что вы создадите

Вы создадите приложение, которое позволяет кешировать простой репозиторий книг.

Что вам потребуется

  • Примерно 15 минут свободного времени
  • Любимый текстовый редактор или IDE
  • JDK 6 и выше
  • Gradle 1.11+ или Maven 3.0+
  • Вы также можете импортировать код этого урока, а также просматривать web-страницы прямо из Spring Tool Suite (STS), собственно как и работать дальше из него.

Как проходить этот урок

Как и большинство уроков по Spring, вы можете начать с нуля и выполнять каждый шаг, либо пропустить базовые шаги, которые вам уже знакомы. В любом случае, вы в конечном итоге получите рабочий код.

Чтобы начать с нуля, перейдите в Настройка проекта.

Чтобы пропустить базовые шаги, выполните следующее:

Когда вы закончите, можете сравнить получившийся результат с образцом в gs-caching/complete.

Настройка проекта

Для начала вам необходимо настроить базовый скрипт сборки. Вы можете использовать любую систему сборки, которая вам нравится для сборки проетов Spring, но в этом уроке рассмотрим код для работы с Gradle и Maven. Если вы не знакомы ни с одним из них, ознакомьтесь с соответсвующими уроками Сборка Java-проекта с использованием Gradle или Сборка Java-проекта с использованием Maven.

Создание структуры каталогов

В выбранном вами каталоге проекта создайте следующую структуру каталогов; к примеру, командой mkdir -p src/main/java/hello для *nix систем:

└── src
    └── main
        └── java
            └── hello

Создание файла сборки Gradle

Ниже представлен начальный файл сборки Gradle. Файл pom.xml находится здесь. Если вы используете Spring Tool Suite (STS), то можете импортировать урок прямо из него.

Если вы посмотрите на pom.xml, вы найдете, что указана версия для maven-compiler-plugin. В общем, это не рекомендуется делать. В данном случае он предназначен для решения проблем с нашей CI системы, которая по умолчанию имеет старую(до Java 5) версию этого плагина.

build.gradle

buildscript {
    repositories {
        mavenLocal()
        mavenCentral()
        maven { url "https://repo.spring.io/libs-release" }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.1.9.RELEASE")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'

jar {
    baseName = 'gs-caching'
    version =  '0.1.0'
}

repositories {
    mavenLocal()
    mavenCentral()
    maven { url "https://repo.spring.io/libs-release" }
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter")
    compile("org.springframework:spring-context")
}

task wrapper(type: Wrapper) {
    gradleVersion = '1.11'
}

Spring Boot gradle plugin предоставляет множество удобных возможностей:

  • Он собирает все jar'ы в classpath и собирает единое, исполняемое "über-jar", что делает более удобным выполнение и доставку вашего сервиса
  • Он ищет public static void main() метод, как признак исполняемого класса
  • Он предоставляет встроенное разрешение зависимостей, с определенными номерами версий для соответсвующих Spring Boot зависимостей. Вы можете переопределить на любые версии, какие захотите, но он будет по умолчанию для Boot выбранным набором версий

Создание репозитория

Для начала давайте создадим очень простую модель для вашей книги.

src/main/java/hello/Book.java

package hello;

public class Book {

    private String isbn;
    private String title;

    public Book(String isbn, String title) {
        this.isbn = isbn;
        this.title = title;
    }

    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    @Override
    public String toString() {
        return "Book{" + "isbn='" + isbn + '\'' + ", title='" + title + '\'' + '}';
    }
}

И репозиторий для этой модели:

src/main/java/hello/BookRepository.java

package hello;

public interface BookRepository {

    Book getByIsbn(String isbn);

}

Вы могли бы использовать Spring Data для предоставления реализации к вашему репозиторию для широкого выбора SQL или NoSQL хранилищ, но для наглядности данного урока вы будете использовать простейшую реализацию, которая симулирует некоторую задержку.

src/main/java/hello/SimpleBookRepository.java

package hello;

public class SimpleBookRepository implements BookRepository {

    @Override
    public Book getByIsbn(String isbn) {
        simulateSlowService();
        return new Book(isbn, "Some book");
    }

    // Don't do this at home
    private void simulateSlowService() {
        try {
            long time = (long) (5000L);
            Thread.sleep(time);
        } catch (InterruptedException e) {
            throw new IllegalStateException(e);
        }
    }

}

Метод simulateSlowService специально вставлен для задержки в пять секунд в каждом вызове getByIsbn.

Использование репозитория

Инициализируйте репозиторий и используйте его для доступа к некоторым книгам:

src/main/java/hello/Application.java

package hello;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableAutoConfiguration
public class Application {

    private final static Logger log = LoggerFactory.getLogger(Application.class);

    @Configuration
    static class Runner implements CommandLineRunner {
        @Autowired
        private BookRepository bookRepository;

        @Override
        public void run(String... args) throws Exception {
            log.info(".... Fetching books");
            log.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
            log.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
            log.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
        }
    }

    @Bean
    public BookRepository bookRepository() {
        return new SimpleBookRepository();
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Если вы попытаетесь запустить приложение на данном этапе, то вы заметите, как оно работает достаточно медленно, даже с учетом того, что вы получаете одну и ту же книгу.

2014-06-05 12:15:35.783  ... : .... Fetching books
2014-06-05 12:15:40.783  ... : isbn-1234 -->Book{isbn='isbn-1234', title='Some book'}
2014-06-05 12:15:45.784  ... : isbn-1234 -->Book{isbn='isbn-1234', title='Some book'}
2014-06-05 12:15:50.786  ... : isbn-1234 -->Book{isbn='isbn-1234', title='Some book'}

Как видно по времени, каждая книга возвращается через пять секунд, не смотря на то, что вы её уже получали.

Включение кеширования

Давайте включим кеширование в вашем SimpleBookRepository так, чтобы книги были закешированы в books кэше.

src/main/java/hello/SimpleBookRepository.java

package hello;

import org.springframework.cache.annotation.Cacheable;

public class SimpleBookRepository implements BookRepository {

    @Override
    @Cacheable("books")
    public Book getByIsbn(String isbn) {
        simulateSlowService();
        return new Book(isbn, "Some book");
    }

    // Don't do this at home
    private void simulateSlowService() {
        try {
            long time = (long) (5000L);
            Thread.sleep(time);
        } catch (InterruptedException e) {
            throw new IllegalStateException(e);
        }
    }

}

Теперь вам необходимо включить обработку аннотаций кеширования:

src/main/java/hello/Application.java

package hello;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableCaching
@EnableAutoConfiguration
public class Application {

    private static final Logger log = LoggerFactory.getLogger(Application.class);

    @Configuration
    static class Runner implements CommandLineRunner {
        @Autowired
        private BookRepository bookRepository;

        @Override
        public void run(String... args) throws Exception {
            log.info(".... Fetching books");
            log.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
            log.info("isbn-4567 -->" + bookRepository.getByIsbn("isbn-4567"));
            log.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
            log.info("isbn-4567 -->" + bookRepository.getByIsbn("isbn-4567"));
            log.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
            log.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
        }
    }

    @Bean
    public BookRepository bookRepository() {
        return new SimpleBookRepository();
    }

    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("books");
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Аннотация @EnableCaching запускает post processor, который проверяет каждый Spring бин на наличие аннотаций кеширования для public методов. Если такая аннотация найдена, то автоматически созданный прокси перехватает вызов метода и обрабатывает в соответствии споведением кеширования.

Cacheable, CachePut и CacheEvict являются аннотациями, которыми управляет вышеописанный post processor. Более подробную информацию смотрите в документации.

В основе своей, аннотации требуется CacheManager как поставщика соответствующего кэша. В данном примере вы используете реализацию, которая возвращает ConcurrentHashMap. Интерфейс CachingConfigurer предоставляет более расширенные варианты настройки.

Сборка исполняемого JAR

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

./gradlew build

Затем вы можете запустить JAR-файл:

java -jar build/libs/gs-caching-0.1.0.jar

Если вы используете Maven, вы можете запустить приложение, используя mvn spring-boot:run, либо вы можете собрать приложение с mvn clean package и запустить JAR примерно так:

java -jar target/gs-caching-0.1.0.jar
Процедура, описанная выше, создает исполняемый JAR. Вы также можете вместо него собрать классический WAR-файл.

Если вы используете Gradle, вы можете запустить ваш сервис из командной строки:

./gradlew clean build && java -jar build/libs/gs-caching-0.1.0.jar
Если вы используете Maven, то можете запустить ваш сервис таким образом: mvn clean package && java -jar target/gs-caching-0.1.0.jar.

Как вариант, вы можете запустить ваш сервис напрямую из Gradle примерно так:

./gradlew bootRun
С mvn - mvn spring-boot:run.

Тестирование приложения

Теперь, когда кеширование включено, вы можете снова запустить приложение и увидеть разницу с дополнительными вызовами или без с теми же isbn. Разница должна быть огромной:

2014-06-05 12:09:23.862 ... : .... Fetching books
2014-06-05 12:09:28.866 ... : isbn-1234 -->Book{isbn='isbn-1234', title='Some book'}
2014-06-05 12:09:33.867 ... : isbn-4567 -->Book{isbn='isbn-4567', title='Some book'}
2014-06-05 12:09:33.867 ... : isbn-1234 -->Book{isbn='isbn-1234', title='Some book'}
2014-06-05 12:09:33.867 ... : isbn-4567 -->Book{isbn='isbn-4567', title='Some book'}
2014-06-05 12:09:33.868 ... : isbn-1234 -->Book{isbn='isbn-1234', title='Some book'}
2014-06-05 12:09:33.868 ... : isbn-1234 -->Book{isbn='isbn-1234', title='Some book'}

Эти данные из консоли показывают, что первый запрос книги по каждому из isbn занимает пять секунд, а последующие вызовы выполняются почти мгновенно.

Итог

Поздравляем! Вы только что включили кеширование для Spring бина.

С оригинальным текстом урока вы можете ознакомиться на spring.io.

comments powered by Disqus