Для объявления кэширования абстракция кэширования в 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) {...}
- Задание
anotherCacheManager
.
Вы также можете полностью заменить CacheResolver
аналогично замене генерации ключей. Разрешение запрашивается для каждой операции с кэшем, позволяя реализации фактически решать, какие кэши использовать, основываясь на аргументах среды выполнения. В следующем примере показано, как задать CacheResolver
:
@Cacheable(cacheResolver="runtimeCacheResolver") (1)
public Book findBook(ISBN isbn) {...}
- Задание
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) {...}
- Использование атрибута
sync
.
CacheManager
, передаваемые основным фреймворком, поддерживают её. Более подробную информацию см. в документации поставщика кэша.Условное кэширование
Иногда метод может не подходить для постоянного кэширования (например, он может зависеть от заданных аргументов). Аннотации кэша поддерживают такие случаи использования благодаря параметру condition
, который принимает выражение SpEL
, вычисленное как true
или false
. Если true
, то метод кэшируется. Если нет, то метод ведет себя так, как будто он не кэширован (то есть метод вызывается каждый раз, независимо от того, какие значения находятся в кэше или какие аргументы используются). Например, следующий метод кэшируется только в том случае, если name
аргумента имеет длину меньше 32:
@Cacheable(cacheNames="book", condition="#name.length() < 32") (1)
public Book findBook(String name)
- Задание условия для аннотации
@Cacheable
.
В дополнение к параметру condition
можно использовать параметр unless
, чтобы запретить добавление значения в кэш. В отличие от condition
, выражения с unless
вычисляются после вызова метода. Развивая предыдущий пример, возможно, нам понадобится кэшировать только книги в мягкой обложке, как это сделано в следующем примере:
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback") (1)
public Book findBook(String name)
- Использование атрибута
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
. В дополнение к предопределенным параметрам фреймворк предусматривает специальные метаданные, связанные с кэшированием, такие как имена аргументов. В следующей таблице описаны элементы, доступные для контекста, чтобы можно было использовать их для ключевых и условных вычислений:
Имя | Местонахождение | Описание | Пример |
---|---|---|---|
|
Корневой объект |
Имя вызываемого метода |
|
|
Корневой объект |
Вызываемый метод |
|
|
Корневой объект |
Вызываемый целевой объект |
|
|
Корневой объект |
Класс вызываемой цели |
|
|
Корневой объект |
Аргументы (в виде массива), используемые для вызова цели |
|
|
Корневой объект |
Коллекция кэшей, для которых выполняется текущий метод |
|
Argument name |
Контекст вычислений |
Имя любого из аргументов метода. Если эти имена недоступны (вероятно, из-за отсутствия отладочной информации), имена аргументов будут также доступны под |
|
|
Контекст вычислений |
Результат вызова метода (значение, подлежащее кэшированию). Доступен только в выражениях c |
|
Аннотация @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)
- Использование атрибута
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) {...}
}
- Использование аннотации
@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
.
@Transactional
– proxy
, что позволяет перехватывать вызовы исключительно через прокси. Локальные вызовы в пределах одного класса перехватить таким же образом нельзя. Для более расширенного режима перехвата рассмотрите возможность перехода на режим aspectj
в сочетании со связыванием во время компиляции или во время загрузки.CachingConfigurer
, см. в javadoc.Атрибут XML | Атрибут аннотации | По умолчанию | Описание |
---|---|---|---|
|
Н/Д (см. javadoc по |
|
Имя используемого диспетчера кэша. |
|
Н/Д (см. javadoc по |
|
Имя бина для CacheResolver, который будет использоваться для разрешения резервных кэшей. Этот атрибут не является обязательным и его необходимо задавать только в качестве альтернативы атрибуту "cache-manager". |
|
Н/AД (см. javadoc по |
|
Имя используемого генератора кастомных ключей. |
|
Н/Д (см. javadoc по |
|
Имя используемого пользовательского обработчика ошибок кэша. По умолчанию любое исключение, генерируемое во время операции, связанной с кэшем, возвращается обратно на стороне клиента. |
|
|
|
Режим по умолчанию ( |
|
|
|
Применяется только в режиме прокси. Управляет тем, какой тип кэширующих прокси создается для классов, аннотированных |
|
|
Ordered.LOWEST_PRECEDENCE |
Определяет порядок снабжения кэша Advice-ами, которые применяются к бинам, помеченным аннотациями |
<cache:annotation-driven/>
ищет @Cacheable/@CachePut/@CacheEvict/@Caching
только для бинов в том же контексте приложения, в котором он определен. Это означает, что если вы поместите <cache:annotation-driven/>
в WebApplicationContext
для DispatcherServlet
, он будет проверять наличие бинов только в ваших контроллерах, а не в службах. @Cache*
, а не аннотировать интерфейсы. Вы, конечно, можете пометить аннотацией @Cache*
интерфейс (или метод интерфейса), но это сработает только в том случае, если вы используете режим прокси (mode="proxy"
). Если вы используете аспект на основе привязки (mode="aspectj"
), параметры кэширования не будут распознаваться инфраструктурой привязки в объявлениях на уровне интерфейса.@Transactional
. В этом случае используйте режим aspectj
. Кроме того, прокси должен быть полностью инициализирован, чтобы обеспечить предусмотренную логику работы, поэтому не стоит прибегать к этой функции в коде инициализации – например, в методе, аннотированном @PostConstruct
.Использование кастомных аннотаций
Абстракция кэширования позволяет использовать собственные аннотации, чтобы определять, какой метод будет вызывать наполнение кэша или вытеснение из кэша. Её довольно удобно использовать в качестве шаблонного механизма, поскольку избавляет от необходимости дублировать объявления аннотаций кэша, что особенно полезно, если задан ключ или условие или если импортируемый извне элемент (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, контейнер автоматически подхватывает её объявление во время выполнения и распознает её значение. Обратите внимание, что логика работы, управляемая аннотациями, должна быть активирована.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ