«1. Абстракция кэша?

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

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

2. Начало работы

Основная абстракция кэширования, предоставляемая Spring, находится в модуле spring-context. Поэтому при использовании Maven наш pom.xml должен содержать следующую зависимость:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.3</version>
</dependency>

Интересно, что есть еще один модуль с именем spring-context-support, который находится поверх модуля spring-context и предоставляет еще несколько CacheManager. при поддержке подобных EhCache или Caffeine. Если мы хотим использовать их в качестве нашего кэш-хранилища, то вместо этого нам нужно использовать модуль spring-context-support:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>5.3.3</version>
</dependency>

Поскольку модуль поддержки spring-context транзитивно зависит от модуля spring-context, существует нет необходимости в отдельном объявлении зависимостей для spring-context.

2.1. Spring Boot

Если мы используем Spring Boot, то мы можем использовать стартовый пакет spring-boot-starter-cache, чтобы легко добавить зависимости кэширования:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
    <version>2.4.0</version>
</dependency>

Под капотом стартер приносит spring-context- модуль поддержки.

3. Включите кэширование

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

Мы можем включить функцию кэширования, просто добавив аннотацию @EnableCaching к любому из классов конфигурации:

@Configuration
@EnableCaching
public class CachingConfig {

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

Мы, конечно, можем включить управление кэшем с конфигурацией XML:

<beans>
    <cache:annotation-driven />

    <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
        <property name="caches">
            <set>
                <bean 
                  class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" 
                  name="addresses"/>
            </set>
        </property>
    </bean>
</beans>

Примечание. После того, как мы включим кеширование, для минимальной настройки мы должны зарегистрировать cacheManager.

3.1. Использование Spring Boot

При использовании Spring Boot простое присутствие начального пакета в пути к классам вместе с аннотацией EnableCaching регистрирует тот же самый ConcurrentMapCacheManager. Таким образом, нет необходимости в отдельном объявлении bean-компонента.

Кроме того, мы можем настроить автоматически настроенный CacheManager с помощью одного или нескольких bean-компонентов CacheManagerCustomizer\u003cT\u003e:

@Component
public class SimpleCacheCustomizer 
  implements CacheManagerCustomizer<ConcurrentMapCacheManager> {

    @Override
    public void customize(ConcurrentMapCacheManager cacheManager) {
        cacheManager.setCacheNames(asList("users", "transactions"));
    }
}

Автоконфигурация CacheAutoConfiguration подбирает эти настройщики и применяет их к текущему CacheManager перед его полной инициализацией.

4. Использование кэширования с аннотациями

После включения кэширования следующим шагом будет привязка поведения кэширования к методам с декларативными аннотациями.

4.1. @Cacheable

Самый простой способ включить поведение кэширования для метода — разграничить его с помощью @Cacheable и параметризовать его именем кэша, в котором будут храниться результаты:

@Cacheable("addresses")
public String getAddress(Customer customer) {...}

Вызов getAddress() сначала проверит адреса кеша, прежде чем фактически вызвать метод, а затем кэширует результат.

Хотя в большинстве случаев достаточно одного кеша, среда Spring также поддерживает передачу нескольких кешей в качестве параметров:

@Cacheable({"addresses", "directory"})
public String getAddress(Customer customer) {...}

В этом случае, если какой-либо из кешей содержит требуемый результат, результат возвращается и метод не вызывается.

4.2. @CacheEvict

Теперь, в чем проблема сделать все методы @Cacheable?

Проблема в размере. Мы не хотим заполнять кеш значениями, которые нам не нужны часто. Кэши могут разрастаться довольно быстро, и мы можем хранить много устаревших или неиспользуемых данных.

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

@CacheEvict(value="addresses", allEntries=true)
public String getAddress(Customer customer) {...}

Здесь мы используем дополнительный параметр allEntries в сочетании с очисткой кеша; это очистит все записи в адресах кеша и подготовит его для новых данных.

4.3. @CachePut

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

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

«С помощью аннотации @CachePut мы можем обновлять содержимое кеша, не мешая выполнению метода. То есть метод всегда будет выполняться, а результат кэшироваться:

@CachePut(value="addresses")
public String getAddress(Customer customer) {...}

Разница между @Cacheable и @CachePut заключается в том, что @Cacheable пропустит выполнение метода, тогда как @CachePut фактически запустит метод, а затем поместит его результаты в кэше.

4.4. @Caching

Что делать, если мы хотим использовать несколько аннотаций одного типа для кэширования метода? Давайте рассмотрим неправильный пример:

@CacheEvict("addresses")
@CacheEvict(value="directory", key=customer.name)
public String getAddress(Customer customer) {...}

Приведенный выше код не скомпилируется, поскольку Java не позволяет объявлять несколько аннотаций одного типа для данного метода.

Обходной путь для вышеуказанной проблемы может быть следующим:

@Caching(evict = { 
  @CacheEvict("addresses"), 
  @CacheEvict(value="directory", key="#customer.name") })
public String getAddress(Customer customer) {...}

Как показано в приведенном выше фрагменте кода, мы можем сгруппировать несколько аннотаций кэширования с помощью @Caching и использовать его для реализации нашей собственной настраиваемой логики кэширования.

4.5. @CacheConfig

С помощью аннотации @CacheConfig мы можем упорядочить некоторые настройки кеша в одном месте на уровне класса, чтобы нам не приходилось объявлять вещи несколько раз:

@CacheConfig(cacheNames={"addresses"})
public class CustomerDataService {

    @Cacheable
    public String getAddress(Customer customer) {...}

5. Условное Кэширование

Иногда кэширование может не работать для метода во всех ситуациях.

Используя наш пример из аннотации @CachePut, мы будем выполнять метод и каждый раз кэшировать результаты:

@CachePut(value="addresses")
public String getAddress(Customer customer) {...}

5.1. Параметр условия

Если нам нужен больший контроль над тем, когда аннотация активна, мы можем параметризовать @CachePut параметром условия, который принимает выражение SpEL и обеспечивает кэширование результатов на основе оценки этого выражения:

@CachePut(value="addresses", condition="#customer.name=='Tom'")
public String getAddress(Customer customer) {...}

5.2. Параметр Unless

Мы также можем управлять кэшированием на основе вывода метода, а не ввода с помощью параметра If:

@CachePut(value="addresses", unless="#result.length()<64")
public String getAddress(Customer customer) {...}

Приведенная выше аннотация будет кэшировать адреса, если они не короче 64 символов.

Важно знать, что параметры условия и исключения можно использовать вместе со всеми кэширующими аннотациями.

Этот вид условного кэширования может оказаться весьма эффективным для управления большими результатами. Это также полезно для настройки поведения на основе входных параметров вместо принудительного применения общего поведения для всех операций.

6. Декларативное кэширование на основе XML

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

Вот наша конфигурация XML:

<!-- the service that you wish to make cacheable -->
<bean id="customerDataService" 
  class="com.your.app.namespace.service.CustomerDataService"/>

<bean id="cacheManager" 
  class="org.springframework.cache.support.SimpleCacheManager"> 
    <property name="caches"> 
        <set> 
            <bean 
              class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" 
              name="directory"/> 
            <bean 
              class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" 
              name="addresses"/> 
        </set> 
    </property> 
</bean>
<!-- define caching behavior -->
<cache:advice id="cachingBehavior" cache-manager="cacheManager">
    <cache:caching cache="addresses">
        <cache:cacheable method="getAddress" key="#customer.name"/>
    </cache:caching>
</cache:advice>

<!-- apply the behavior to all the implementations of CustomerDataService interface->
<aop:config>
    <aop:advisor advice-ref="cachingBehavior"
      pointcut="execution(* com.your.app.namespace.service.CustomerDataService.*(..))"/>
</aop:config>

7. Кэширование на основе Java

Вот эквивалентная конфигурация Java:

@Configuration
@EnableCaching
public class CachingConfig {

    @Bean
    public CacheManager cacheManager() {
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        cacheManager.setCaches(Arrays.asList(
          new ConcurrentMapCache("directory"), 
          new ConcurrentMapCache("addresses")));
        return cacheManager;
    }
}

А вот наш CustomerDataService:

@Component
public class CustomerDataService {
 
    @Cacheable(value = "addresses", key = "#customer.name")
    public String getAddress(Customer customer) {
        return customer.getAddress();
    }
}

8 Резюме

В этой статье мы обсудили основы кэширования в Spring и как эффективно использовать эту абстракцию с аннотациями.

Полную реализацию этой статьи можно найти в проекте GitHub.