Для объявления кэширования абстракция кэширования в Spring предоставляет набор аннотаций Java:

  • @Cacheable: Запускает наполнение кэша.

  • @CacheEvict: Запускает выгрузку кэша.

  • @CachePut: Обновляет кэш, не вмешиваясь в выполнение метода.

  • @Caching: Перегруппировывает несколько операций кэширования для применения к методу.

  • @CacheConfig: Разделяет некоторые общие настройки, связанные с кэшем, на уровне класса.

Аннотация @Cacheable

Как следует из названия, аннотацию @Cacheable можно использовать для выделения методов, которые можно кэшировать – то есть методов, для которых результат сохраняется в кэше, чтобы при последующих вызовах (с теми же аргументами) возвращалось значение из кэша без необходимости снова вызывать метод. В своей простейшей форме объявление аннотации требует указания имени кэша, связанного с аннотируемым методом, как показано в следующем примере:

@Cacheable("books")
public Book findBook(ISBN isbn) {...}

В предыдущем фрагменте метод findBook связан с кэшем под именем books. Каждый раз, когда вызывается метод, кэш проверяется на предмет того, был ли уже выполнен вызов и не нужно ли его повторять. Хотя в большинстве случаев объявляется только один кэш, аннотация позволяет указать несколько имен, чтобы можно было использовать более одного кэша. В этом случае перед вызовом метода проверяется каждый из кэшей – если хотя бы один кэш совпадает, возвращается соответствующее значение.

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

В следующем примере аннотация @Cacheable используется в методе findBook с несколькими кэшами:

@Cacheable({"books", "isbns"})
public Book findBook(ISBN isbn) {...}

Генерация ключей по умолчанию

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

  • Если параметры не заданы, возвращается SimpleKey.EMPTY.

  • Если задан только один параметр, возвращается этот экземпляр.

  • Если задано более одного параметра, возвращается SimpleKey, содержащий все параметры.

Этот подход хорошо работает для большинства случаев использования, если параметры имеют естественные ключи и реализуют допустимые методы hashCode() и equals(). Если это не так, то необходимо менять стратегию.

Чтобы указать другой генератор ключей по умолчанию, необходимо реализовать интерфейс org.springframework.cache.interceptor.KeyGenerator.

Стратегия генерации ключей по умолчанию изменилась с выходом Spring 4.0. Более ранние версии Spring использовали стратегию генерации ключей, которая для нескольких ключевых параметров учитывала только hashCode() параметров, а не equals(). Это может приводить к неожиданным конфликтам ключей. Новый SimpleKeyGenerator использует составной ключ для таких сценариев.

Если необходимо продолжать использовать предыдущую стратегию ключей, то можно сконфигурировать устаревший класс org.springframework.cache.interceptor.DefaultKeyGenerator или создать собственную реализацию KeyGenerator на основе хэша.

Кастомное объявление генерации ключей

Поскольку кэширование является общим, целевые методы, скорее всего, будут иметь различные сигнатуры, которые не получится легко отображать поверх структуры кэша. Это становится очевидным, если целевой метод имеет несколько аргументов, из которых только некоторые подходят для кэширования (остальные используются только логикой метода). Рассмотрим следующий пример:

@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

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

Для таких случаев аннотация @Cacheable позволяет указать способ генерации ключа через его атрибут key. Вы можете использовать язык выражений SpE для выбора интересующих вас аргументов (или их вложенных свойств), выполнения операций или даже вызова произвольных методов без необходимости писать какой-либо код или реализовывать какой-либо интерфейс. Этот подход более предпочтителен, чем генератор по умолчанию, так как методы имеют тенденцию сильно отличаться по сигнатурам по мере роста кодовой базы. Хотя стратегия по умолчанию может сработать для некоторых методов, она редко срабатывает для всех методов.

В следующих примерах используются различные объявления на языке SpEL:

@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(cacheNames="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(cacheNames="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

В предыдущих фрагментах показано, как можно с легкостью выбрать определенный аргумент, одно из его свойств или даже произвольный (статический) метод.

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

@Cacheable(cacheNames="books", keyGenerator="myKeyGenerator")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
Параметры key и keyGenerator являются взаимоисключающими, поэтому операция, в которой указаны оба параметра, приводит к появлению исключения.

Разрешение кэша по умолчанию

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

Чтобы указать другой распознаватель кэша по умолчанию, необходимо реализовать интерфейс org.springframework.cache.interceptor.CacheResolver.

Кастомное разрешение кэша

Разрешение кэша по умолчанию хорошо подходит для приложений, которые работают с одним CacheManager и не имеют сложных требований к разрешению кэша.

Для приложений, которые работают с несколькими диспетчерами кэша, можно установить cacheManager, который будет использоваться для каждой операции, как показано в следующем примере:

@Cacheable(cacheNames="books", cacheManager="anotherCacheManager") (1)
public Book findBook(ISBN isbn) {...}
  1. Задание anotherCacheManager.

Вы также можете полностью заменить CacheResolver аналогично замене генерации ключей. Разрешение запрашивается для каждой операции с кэшем, позволяя реализации фактически решать, какие кэши использовать, основываясь на аргументах среды выполнения. В следующем примере показано, как задать CacheResolver:

@Cacheable(cacheResolver="runtimeCacheResolver") (1)
public Book findBook(ISBN isbn) {...}
  1. Задание CacheResolver.

Начиная с версии Spring 4.1, атрибут value аннотации кэша больше не являются обязательными, поскольку эта конкретная информация может быть передана CacheResolver независимо от содержимого аннотации.

Аналогично key и keyGenerator, параметры cacheManager и cacheResolver являются взаимоисключающими, а операция, задающая оба параметра, приводит к появлению исключения, поскольку кастомный CacheManager игнорируется реализацией CacheResolver. Вероятно, это не то, чего вы ожидаете.

Синхронизированное кэширование

В многопоточном окружении некоторые операции могут быть вызваны одновременно для одного и того же аргумента (обычно при запуске). По умолчанию абстракция кэша ничего не блокирует, а одно и то же значение может вычисляться несколько раз, что сводит на нет цель кэширования.

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

@Cacheable(cacheNames="foos", sync=true) (1)
public Foo executeExpensiveOperation(String id) {...}
  1. Использование атрибута sync.
Это опциональная функция, поэтому ваша любимая кэш-библиотека может ее не поддерживать. Все реализации CacheManager, передаваемые основным фреймворком, поддерживают её. Более подробную информацию см. в документации поставщика кэша.

Условное кэширование

Иногда метод может не подходить для постоянного кэширования (например, он может зависеть от заданных аргументов). Аннотации кэша поддерживают такие случаи использования благодаря параметру condition, который принимает выражение SpEL, вычисленное как true или false. Если true, то метод кэшируется. Если нет, то метод ведет себя так, как будто он не кэширован (то есть метод вызывается каждый раз, независимо от того, какие значения находятся в кэше или какие аргументы используются). Например, следующий метод кэшируется только в том случае, если name аргумента имеет длину меньше 32:

@Cacheable(cacheNames="book", condition="#name.length() < 32") (1)
public Book findBook(String name)
  1. Задание условия для аннотации @Cacheable.

В дополнение к параметру condition можно использовать параметр unless, чтобы запретить добавление значения в кэш. В отличие от condition, выражения с unless вычисляются после вызова метода. Развивая предыдущий пример, возможно, нам понадобится кэшировать только книги в мягкой обложке, как это сделано в следующем примере:

@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback") (1)
public Book findBook(String name)
  1. Использование атрибута unless для блокировки книг с твёрдым переплётом.

Абстракция кэша поддерживает возвращаемые типы java.util.Optional. Если имеется значение Optional, оно будет сохранено в соответствующем кэше. Если значения Optional нет, то в связанном кэше будет сохранен null. #result всегда ссылается на бизнес-сущность и никогда на поддерживаемую функцию-обёртку, поэтому предыдущий пример можно переписать следующим образом:

@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback")
public Optional<Book> findBook(String name)

Обратите внимание, что #result по-прежнему ссылается на Book, а не Optional<Book>. Поскольку он может быть null, мы используем оператор безопасной навигации на языке SpEL.

Доступный контекст определения кэширования на языке SpEL

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

Таблица 9. Доступные для кэширования метаданные SpEL
Имя Местонахождение Описание Пример

methodName

Корневой объект

Имя вызываемого метода

#root.methodName

method

Корневой объект

Вызываемый метод

#root.method.name

target

Корневой объект

Вызываемый целевой объект

#root.target

targetClass

Корневой объект

Класс вызываемой цели

#root.targetClass

args

Корневой объект

Аргументы (в виде массива), используемые для вызова цели

#root.args[0]

caches

Корневой объект

Коллекция кэшей, для которых выполняется текущий метод

#root.caches[0].name

Argument name

Контекст вычислений

Имя любого из аргументов метода. Если эти имена недоступны (вероятно, из-за отсутствия отладочной информации), имена аргументов будут также доступны под #a<#arg>, где #arg означает индекс аргумента (начиная с 0).

#iban или #a0 (в качестве псевдонима можно также использовать нотацию #p0 или #p<#arg>).

result

Контекст вычислений

Результат вызова метода (значение, подлежащее кэшированию). Доступен только в выражениях c unless, выражениях c cache put (для вычисления key) или выражениях c cache evict (когда beforeInvocation равно false). Для поддерживаемых функций-обёрток (таких как Optional) #result относится к фактическому объекту, а не к функции-обёртке.

#result

Аннотация @CachePut

Если кэш необходимо обновить, не вмешиваясь в выполнение метода, можно использовать аннотацию @CachePut. То есть метод будет вызываться всегда, а его результат помещаться в кэш (в соответствии с параметрами аннотации @CachePut). Она поддерживает те же параметры, что и аннотация @Cacheable, но используется для наполнения кэша, а не для оптимизации потока методов. В следующем примере используется аннотация @CachePut:

@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)
Использовать аннотации @CachePut и @Cacheable в одном и том же методе, как правило, настоятельно не рекомендуется, поскольку они имеют отличающуюся логику работы. В то время как в последнем случае вызов метода пропускается при использовании кэша, в первом происходит форсированный вызов метода для выполнения обновления кэша. Это приводит к непредвиденной логике работы, и, за исключением особых тупиковых ситуаций (например, аннотаций с условиями, исключающими их друг из друга), таких объявлений следует избегать. Обратите внимание, что такие условия не должны использовать результирующий объект (то есть переменную #result), так как они валидируются заранее для подтверждения исключения.

Аннотация @CacheEvict

Абстракция кэша позволяет не только создавать хранилище кэша, но и вытеснять из него данные. Этот процесс полезен для удаления устаревших или неиспользуемых данных из кэша. В отличие от аннотации @Cacheable, аннотация @CacheEvict разграничивает методы, которые выполняют вытеснение из кэша (то есть методы, которые действуют как триггеры для удаления данных из кэша). Как и её родственница, аннотация @CacheEvict требует задания одного или нескольких кэшей, на которые распространяется действие, позволяет задать кастомный кэш и разрешение ключа или условие, а также имеет дополнительный параметр (allEntries), который указывает, нужно ли выполнять вытеснение данных из всего кэша, а не только вытеснение записи (на основе ключа). В следующем примере вытесняются все записи из кэша books:

@CacheEvict(cacheNames="books", allEntries=true) (1)
public void loadBooks(InputStream batch)
  1. Использование атрибута allEntries для вытеснения всех записей из кэша.

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

Также можно указать, должно ли вытеснение происходить после (по умолчанию) или перед вызовом метода, используя атрибут beforeInvocation. Первый вариант обеспечивает ту же семантику, что и остальные аннотации: После успешного завершения метода выполняется действие (в данном случае вытеснение) над кэшем. Если метод не выполняется (поскольку он может быть кэширован) или генерируется исключение, вытеснение не происходит. Последний вариант (beforeInvocation=true) приводит к тому, что вытеснение всегда происходить до вызова метода. Это полезно в тех случаях, когда вытеснение не обязательно должно быть связано с результатом выполнения метода.

Обратите внимание, что методы void можно использовать с аннотацией @CacheEvict – поскольку методы действуют как триггер, возвращаемые значения игнорируются (так как они не взаимодействуют с кэшем). Это не относится к аннотации @Cacheable, которая привносит данные в кэш или обновляет данные в кэше и, таким образом, требует получения результата.

Аннотация @Caching

Иногда требуется задать несколько аннотаций одного типа (например, @CacheEvict или @CachePut) – например, потому что условие или ключевое выражение в разных кэшах разное. Аннотация @Caching позволяет использовать несколько вложенных аннотаций @Cacheable, @CachePut и @CacheEvict для одного и того же метода. В следующем примере используются две аннотации @CacheEvict:

@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)

Аннотация @CacheConfig

До сих пор мы видели, что операции кэширования предполагают множество параметров настройки и что можно задавать эти параметры для каждой операции. Однако некоторые параметры настройки могут быть громоздкими, если применять их ко всем операциям класса. Например, задание имени кэша, который должен использоваться для каждой операции кэширования класса, можно заменить одним определением на уровне класса. Именно здесь в игру вступает аннотация @CacheConfig. В следующих примерах @CacheConfig используется для установки имени кэша:

@CacheConfig("books") (1)
public class BookRepositoryImpl implements BookRepository {
    @Cacheable
    public Book findBook(ISBN isbn) {...}
}
  1. Использование аннотации @CacheConfig для установки имени кэша.

@CacheConfig – это аннотация на уровне класса, которая позволяет совместно использовать имена кэша, кастомный KeyGenerator, кастомный CacheManager и кастомный CacheResolver . Размещение этой аннотации на классе не активирует никаких операций кэширования.

Настройка на уровне операции всегда переопределяет настройку, установленную для аннотации @CacheConfig. Следовательно, это дает нам три уровня настройки для каждой операции кэширования:

  • KeyGenerator, конфигурируемый глобально и доступный для CacheManager

  • На уровне класса, используя аннотацию @CacheConfig.

  • На уровне операций.

Активируем аннотации кэширования

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

Чтобы активировать аннотации кэширования, добавьте аннотацию @EnableCaching к одному из ваших классов, помеченных аннотацией @Configuration:

@Configuration
@EnableCaching
public class AppConfig {
}

Как вариант, для XML-конфигурации можно использовать элемент cache:annotation-driven:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:cache="http://www.springframework.org/schema/cache"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/cache https://www.springframework.org/schema/cache/spring-cache.xsd">
        <cache:annotation-driven/>
</beans>

И элемент cache:annotation-driven, и аннотация @EnableCaching позволяют задавать различные параметры, которые влияют на то, как логика работы кэширования будет добавлена в приложение посредством АОП. Конфигурация намеренно схожа с конфигурацией аннотации @Transactional.

Режим снабжения Advice-ми по умолчанию для обработки аннотаций @Transactionalproxy, что позволяет перехватывать вызовы исключительно через прокси. Локальные вызовы в пределах одного класса перехватить таким же образом нельзя. Для более расширенного режима перехвата рассмотрите возможность перехода на режим aspectj в сочетании со связыванием во время компиляции или во время загрузки.
Более подробно о расширенных настройках (с использованием Java-конфигурации), которые необходимы для реализации CachingConfigurer, см. в javadoc.
Таблица 10. Параметры кэширования аннотаций
Атрибут XML Атрибут аннотации По умолчанию Описание

cache-manager

Н/Д (см. javadoc по CachingConfigurer)

cacheManager

Имя используемого диспетчера кэша. CacheResolver по умолчанию инициализируется "за кулисами" с этим диспетчером кэша (или cacheManager, если он не установлен). Для более тонкого управления разрешением кэша следует установить атрибут "cache-resolver".

cache-resolver

Н/Д (см. javadoc по CachingConfigurer)

SimpleCacheResolver, использующий сконфигурированный cacheManager.

Имя бина для CacheResolver, который будет использоваться для разрешения резервных кэшей. Этот атрибут не является обязательным и его необходимо задавать только в качестве альтернативы атрибуту "cache-manager".

key-generator

Н/AД (см. javadoc по CachingConfigurer)

SimpleKeyGenerator

Имя используемого генератора кастомных ключей.

error-handler

Н/Д (см. javadoc по CachingConfigurer)

SimpleCacheErrorHandler

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

mode

mode

proxy

Режим по умолчанию (proxy) обрабатывает аннотированные бины для проксирования с помощью АОП-фреймворка Spring (следуя семантике прокси (которая была описана ранее), применяемой только к вызовам методов, поступающим через прокси). Альтернативный режим (aspectj) вместо этого привязывает затронутые классы с помощью аспекта транзакции на основе AspectJ из Spring, изменяя байт-код целевого класса для применения к любому виду вызова метода. Привязывание на основе AspectJ требует наличия spring-aspects.jar в classpath, а также активации привязывания во время загрузки (или привязывания во время компиляции).

proxy-target-class

proxyTargetClass

false

Применяется только в режиме прокси. Управляет тем, какой тип кэширующих прокси создается для классов, аннотированных @Cacheable или @CacheEvict. Если атрибут proxy-target-class имеет значение true, создаются прокси на основе классов. Если proxy-target-class имеет значение false или атрибут опущен, создаются стандартные прокси на основе интерфейса JDK.

order

order

Ordered.LOWEST_PRECEDENCE

Определяет порядок снабжения кэша Advice-ами, которые применяются к бинам, помеченным аннотациями @Cacheable или @CacheEvict. Отсутствие заданного порядка означает, что подсистема АОП будет определять порядок Advice.

<cache:annotation-driven/> ищет @Cacheable/@CachePut/@CacheEvict/@Caching только для бинов в том же контексте приложения, в котором он определен. Это означает, что если вы поместите <cache:annotation-driven/> в WebApplicationContext для DispatcherServlet, он будет проверять наличие бинов только в ваших контроллерах, а не в службах.
Видимость метода и аннотации кэша

При использовании прокси следует применять аннотации кэша только к методам с публичной видимостью. Если вы пометите protected, private методы или методы с областью доступности в пределах пакета аннотацией @Transactional, ошибки не возникнет, но аннотированный метод не выдаст сконфигурированных транзакционных параметров. Подумайте об использовании AspectJ (см. остальную часть этого раздела), если требуется аннотировать непубличные методы, поскольку это изменит сам байт-код.

Команда Spring рекомендует аннотировать конкретные классы (и методы конкретных классов) исключительно аннотациями @Cache*, а не аннотировать интерфейсы. Вы, конечно, можете пометить аннотацией @Cache* интерфейс (или метод интерфейса), но это сработает только в том случае, если вы используете режим прокси (mode="proxy"). Если вы используете аспект на основе привязки (mode="aspectj"), параметры кэширования не будут распознаваться инфраструктурой привязки в объявлениях на уровне интерфейса.
В режиме прокси (который используется по умолчанию) перехватываются только внешние вызовы методов, поступающие через прокси. Это означает, что самовызов (по сути, метод внутри целевого объекта вызывает другой метод целевого объекта) не приводит к появлению фактической транзакции во время выполнения, даже если вызываемый метод помечен аннотацией @Transactional. В этом случае используйте режим aspectj. Кроме того, прокси должен быть полностью инициализирован, чтобы обеспечить предусмотренную логику работы, поэтому не стоит прибегать к этой функции в коде инициализации – например, в методе, аннотированном @PostConstruct.

Использование кастомных аннотаций

Кастомная аннотация и AspectJ

Данная функция работает только в случае применения подхода на основе прокси, но её можно также активировать, приложив некоторые дополнительные усилия, и при использовании AspectJ.

Модуль spring-aspects определяет аспект только для стандартных аннотаций. Если были определены собственные аннотации, то также необходимо определить аспект и для них. См. пример AnnotationCacheAspect.

Абстракция кэширования позволяет использовать собственные аннотации, чтобы определять, какой метод будет вызывать наполнение кэша или вытеснение из кэша. Её довольно удобно использовать в качестве шаблонного механизма, поскольку избавляет от необходимости дублировать объявления аннотаций кэша, что особенно полезно, если задан ключ или условие или если импортируемый извне элемент (org.springframework) недопустим в кодовой базе. Аналогично остальным стереотипным аннотациям, можно использовать @Cacheable, @CachePut, @CacheEvict и @CacheConfig в качестве мета-аннотаций (то есть аннотаций, которые могут аннотировать другие аннотации). В следующем примере мы заменяем обычное объявление аннотации @Cacheable нашей собственной аннотацией:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Cacheable(cacheNames="books", key="#isbn")
public @interface SlowService {
}

В предыдущем примере мы определили нашу собственную аннотацию SlowService, которая сама аннотирована @Cacheable. Теперь можно заменить следующий код:

@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

В следующем примере показана кастомная аннотация, которой можно заменить предыдущий код:

@SlowService
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

Несмотря на то, что аннотация @SlowService не является аннотацией Spring, контейнер автоматически подхватывает её объявление во время выполнения и распознает её значение. Обратите внимание, что логика работы, управляемая аннотациями, должна быть активирована.