You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1070 lines
47 KiB
1070 lines
47 KiB
[[cache]] |
|
= Cache Abstraction |
|
|
|
Since version 3.1, the Spring Framework provides support for transparently adding caching to |
|
an existing Spring application. Similar to the <<data-access.adoc#transaction, transaction>> |
|
support, the caching abstraction allows consistent use of various caching solutions with |
|
minimal impact on the code. |
|
|
|
In Spring Framework 4.1, the cache abstraction was significantly extended with support |
|
for <<cache-jsr-107,JSR-107 annotations>> and more customization options. |
|
|
|
|
|
|
|
[[cache-strategies]] |
|
== Understanding the Cache Abstraction |
|
|
|
.Cache vs Buffer |
|
**** |
|
|
|
The terms, "`buffer`" and "`cache,`" tend to be used interchangeably. Note, however, |
|
that they represent different things. Traditionally, a buffer is used as an intermediate |
|
temporary store for data between a fast and a slow entity. As one party would have to wait |
|
for the other (which affects performance), the buffer alleviates this by allowing entire |
|
blocks of data to move at once rather than in small chunks. The data is written and read |
|
only once from the buffer. Furthermore, the buffers are visible to at least one party |
|
that is aware of it. |
|
|
|
A cache, on the other hand, is, by definition, hidden, and neither party is aware that |
|
caching occurs. It also improves performance but does so by letting the same data be |
|
read multiple times in a fast fashion. |
|
|
|
You can find a further explanation of the differences between a buffer and a cache |
|
https://en.wikipedia.org/wiki/Cache_(computing)#The_difference_between_buffer_and_cache[here]. |
|
**** |
|
|
|
At its core, the cache abstraction applies caching to Java methods, thus reducing the |
|
number of executions based on the information available in the cache. That is, each time |
|
a targeted method is invoked, the abstraction applies a caching behavior that checks |
|
whether the method has been already invoked for the given arguments. If it has been |
|
invoked, the cached result is returned without having to invoke the actual method. |
|
If the method has not been invoked, then it is invoked, and the result is cached and |
|
returned to the user so that, the next time the method is invoked, the cached result is |
|
returned. This way, expensive methods (whether CPU- or IO-bound) can be invoked only |
|
once for a given set of parameters and the result reused without having to actually |
|
invoke the method again. The caching logic is applied transparently without any |
|
interference to the invoker. |
|
|
|
IMPORTANT: This approach works only for methods that are guaranteed to return the same |
|
output (result) for a given input (or arguments) no matter how many times they are invoked. |
|
|
|
The caching abstraction provides other cache-related operations, such as the ability |
|
to update the content of the cache or to remove one or all entries. These are useful if |
|
the cache deals with data that can change during the course of the application. |
|
|
|
As with other services in the Spring Framework, the caching service is an abstraction |
|
(not a cache implementation) and requires the use of actual storage to store the cache data -- |
|
that is, the abstraction frees you from having to write the caching logic but does not |
|
provide the actual data store. This abstraction is materialized by the |
|
`org.springframework.cache.Cache` and `org.springframework.cache.CacheManager` interfaces. |
|
|
|
Spring provides <<cache-store-configuration, a few implementations>> of that abstraction: |
|
JDK `java.util.concurrent.ConcurrentMap` based caches, Gemfire cache, |
|
https://github.com/ben-manes/caffeine/wiki[Caffeine], and JSR-107 compliant caches (such |
|
as Ehcache 3.x). See <<cache-plug>> for more information on plugging in other cache |
|
stores and providers. |
|
|
|
IMPORTANT: The caching abstraction has no special handling for multi-threaded and |
|
multi-process environments, as such features are handled by the cache implementation. |
|
|
|
If you have a multi-process environment (that is, an application deployed on several nodes), |
|
you need to configure your cache provider accordingly. Depending on your use cases, a copy |
|
of the same data on several nodes can be enough. However, if you change the data during |
|
the course of the application, you may need to enable other propagation mechanisms. |
|
|
|
Caching a particular item is a direct equivalent of the typical |
|
get-if-not-found-then-proceed-and-put-eventually code blocks |
|
found with programmatic cache interaction. |
|
No locks are applied, and several threads may try to load the same item concurrently. |
|
The same applies to eviction. If several threads are trying to update or evict data |
|
concurrently, you may use stale data. Certain cache providers offer advanced features |
|
in that area. See the documentation of your cache provider for more details. |
|
|
|
To use the cache abstraction, you need to take care of two aspects: |
|
|
|
* Caching declaration: Identify the methods that need to be cached and their policies. |
|
* Cache configuration: The backing cache where the data is stored and from which it is read. |
|
|
|
|
|
|
|
[[cache-annotations]] |
|
== Declarative Annotation-based Caching |
|
|
|
For caching declaration, Spring's caching abstraction provides a set of Java annotations: |
|
|
|
* `@Cacheable`: Triggers cache population. |
|
* `@CacheEvict`: Triggers cache eviction. |
|
* `@CachePut`: Updates the cache without interfering with the method execution. |
|
* `@Caching`: Regroups multiple cache operations to be applied on a method. |
|
* `@CacheConfig`: Shares some common cache-related settings at class-level. |
|
|
|
|
|
[[cache-annotations-cacheable]] |
|
=== The `@Cacheable` Annotation |
|
|
|
As the name implies, you can use `@Cacheable` to demarcate methods that are cacheable -- |
|
that is, methods for which the result is stored in the cache so that, on subsequent |
|
invocations (with the same arguments), the value in the cache is returned without |
|
having to actually invoke the method. In its simplest form, the annotation declaration |
|
requires the name of the cache associated with the annotated method, as the following |
|
example shows: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
@Cacheable("books") |
|
public Book findBook(ISBN isbn) {...} |
|
---- |
|
|
|
In the preceding snippet, the `findBook` method is associated with the cache named `books`. |
|
Each time the method is called, the cache is checked to see whether the invocation has |
|
already been run and does not have to be repeated. While in most cases, only one |
|
cache is declared, the annotation lets multiple names be specified so that more than one |
|
cache is being used. In this case, each of the caches is checked before invoking the |
|
method -- if at least one cache is hit, the associated value is returned. |
|
|
|
NOTE: All the other caches that do not contain the value are also updated, even though |
|
the cached method was not actually invoked. |
|
|
|
The following example uses `@Cacheable` on the `findBook` method with multiple caches: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
@Cacheable({"books", "isbns"}) |
|
public Book findBook(ISBN isbn) {...} |
|
---- |
|
|
|
[[cache-annotations-cacheable-default-key]] |
|
==== Default Key Generation |
|
|
|
Since caches are essentially key-value stores, each invocation of a cached method |
|
needs to be translated into a suitable key for cache access. The caching abstraction |
|
uses a simple `KeyGenerator` based on the following algorithm: |
|
|
|
* If no parameters are given, return `SimpleKey.EMPTY`. |
|
* If only one parameter is given, return that instance. |
|
* If more than one parameter is given, return a `SimpleKey` that contains all parameters. |
|
|
|
This approach works well for most use-cases, as long as parameters have natural keys |
|
and implement valid `hashCode()` and `equals()` methods. If that is not the case, |
|
you need to change the strategy. |
|
|
|
To provide a different default key generator, you need to implement the |
|
`org.springframework.cache.interceptor.KeyGenerator` interface. |
|
|
|
[NOTE] |
|
==== |
|
The default key generation strategy changed with the release of Spring 4.0. Earlier |
|
versions of Spring used a key generation strategy that, for multiple key parameters, |
|
considered only the `hashCode()` of parameters and not `equals()`. This could cause |
|
unexpected key collisions (see https://jira.spring.io/browse/SPR-10237[SPR-10237] |
|
for background). The new `SimpleKeyGenerator` uses a compound key for such scenarios. |
|
|
|
If you want to keep using the previous key strategy, you can configure the deprecated |
|
`org.springframework.cache.interceptor.DefaultKeyGenerator` class or create a custom |
|
hash-based `KeyGenerator` implementation. |
|
==== |
|
|
|
[[cache-annotations-cacheable-key]] |
|
==== Custom Key Generation Declaration |
|
|
|
Since caching is generic, the target methods are quite likely to have various signatures |
|
that cannot be readily mapped on top of the cache structure. This tends to become obvious |
|
when the target method has multiple arguments out of which only some are suitable for |
|
caching (while the rest are used only by the method logic). Consider the following example: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
@Cacheable("books") |
|
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) |
|
---- |
|
|
|
At first glance, while the two `boolean` arguments influence the way the book is found, |
|
they are no use for the cache. Furthermore, what if only one of the two is important |
|
while the other is not? |
|
|
|
For such cases, the `@Cacheable` annotation lets you specify how the key is generated |
|
through its `key` attribute. You can use <<core.adoc#expressions, SpEL>> to pick the |
|
arguments of interest (or their nested properties), perform operations, or even |
|
invoke arbitrary methods without having to write any code or implement any interface. |
|
This is the recommended approach over the |
|
<<cache-annotations-cacheable-default-key, default generator>>, since methods tend to be |
|
quite different in signatures as the code base grows. While the default strategy might |
|
work for some methods, it rarely works for all methods. |
|
|
|
The following examples use various SpEL declarations (if you are not familiar with SpEL, |
|
do yourself a favor and read <<core.adoc#expressions, Spring Expression Language>>): |
|
|
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
@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) |
|
---- |
|
|
|
The preceding snippets show how easy it is to select a certain argument, one of its |
|
properties, or even an arbitrary (static) method. |
|
|
|
If the algorithm responsible for generating the key is too specific or if it needs |
|
to be shared, you can define a custom `keyGenerator` on the operation. To do so, |
|
specify the name of the `KeyGenerator` bean implementation to use, as the following |
|
example shows: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
@Cacheable(cacheNames="books", keyGenerator="myKeyGenerator") |
|
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) |
|
---- |
|
|
|
NOTE: The `key` and `keyGenerator` parameters are mutually exclusive and an operation |
|
that specifies both results in an exception. |
|
|
|
[[cache-annotations-cacheable-default-cache-resolver]] |
|
==== Default Cache Resolution |
|
|
|
The caching abstraction uses a simple `CacheResolver` that |
|
retrieves the caches defined at the operation level by using the configured |
|
`CacheManager`. |
|
|
|
To provide a different default cache resolver, you need to implement the |
|
`org.springframework.cache.interceptor.CacheResolver` interface. |
|
|
|
[[cache-annotations-cacheable-cache-resolver]] |
|
==== Custom Cache Resolution |
|
|
|
The default cache resolution fits well for applications that work with a |
|
single `CacheManager` and have no complex cache resolution requirements. |
|
|
|
For applications that work with several cache managers, you can set the |
|
`cacheManager` to use for each operation, as the following example shows: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
@Cacheable(cacheNames="books", cacheManager="anotherCacheManager") <1> |
|
public Book findBook(ISBN isbn) {...} |
|
---- |
|
<1> Specifying `anotherCacheManager`. |
|
|
|
|
|
You can also replace the `CacheResolver` entirely in a fashion similar to that of |
|
replacing <<cache-annotations-cacheable-key, key generation>>. The resolution is |
|
requested for every cache operation, letting the implementation actually resolve |
|
the caches to use based on runtime arguments. The following example shows how to |
|
specify a `CacheResolver`: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
@Cacheable(cacheResolver="runtimeCacheResolver") <1> |
|
public Book findBook(ISBN isbn) {...} |
|
---- |
|
<1> Specifying the `CacheResolver`. |
|
|
|
|
|
[NOTE] |
|
==== |
|
Since Spring 4.1, the `value` attribute of the cache annotations are no longer |
|
mandatory, since this particular information can be provided by the `CacheResolver` |
|
regardless of the content of the annotation. |
|
|
|
Similarly to `key` and `keyGenerator`, the `cacheManager` and `cacheResolver` |
|
parameters are mutually exclusive, and an operation specifying both |
|
results in an exception, as a custom `CacheManager` is ignored by the |
|
`CacheResolver` implementation. This is probably not what you expect. |
|
==== |
|
|
|
[[cache-annotations-cacheable-synchronized]] |
|
==== Synchronized Caching |
|
|
|
In a multi-threaded environment, certain operations might be concurrently invoked for |
|
the same argument (typically on startup). By default, the cache abstraction does not |
|
lock anything, and the same value may be computed several times, defeating the purpose |
|
of caching. |
|
|
|
For those particular cases, you can use the `sync` attribute to instruct the underlying |
|
cache provider to lock the cache entry while the value is being computed. As a result, |
|
only one thread is busy computing the value, while the others are blocked until the entry |
|
is updated in the cache. The following example shows how to use the `sync` attribute: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
@Cacheable(cacheNames="foos", sync=true) <1> |
|
public Foo executeExpensiveOperation(String id) {...} |
|
---- |
|
<1> Using the `sync` attribute. |
|
|
|
NOTE: This is an optional feature, and your favorite cache library may not support it. |
|
All `CacheManager` implementations provided by the core framework support it. See the |
|
documentation of your cache provider for more details. |
|
|
|
[[cache-annotations-cacheable-condition]] |
|
==== Conditional Caching |
|
|
|
Sometimes, a method might not be suitable for caching all the time (for example, it might |
|
depend on the given arguments). The cache annotations support such use cases through the |
|
`condition` parameter, which takes a `SpEL` expression that is evaluated to either `true` |
|
or `false`. If `true`, the method is cached. If not, it behaves as if the method is not |
|
cached (that is, the method is invoked every time no matter what values are in the cache |
|
or what arguments are used). For example, the following method is cached only if the |
|
argument `name` has a length shorter than 32: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
@Cacheable(cacheNames="book", condition="#name.length() < 32") <1> |
|
public Book findBook(String name) |
|
---- |
|
<1> Setting a condition on `@Cacheable`. |
|
|
|
|
|
In addition to the `condition` parameter, you can use the `unless` parameter to veto the |
|
adding of a value to the cache. Unlike `condition`, `unless` expressions are evaluated |
|
after the method has been invoked. To expand on the previous example, perhaps we only |
|
want to cache paperback books, as the following example does: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback") <1> |
|
public Book findBook(String name) |
|
---- |
|
<1> Using the `unless` attribute to block hardbacks. |
|
|
|
|
|
The cache abstraction supports `java.util.Optional` return types. If an `Optional` value |
|
is _present_, it will be stored in the associated cache. If an `Optional` value is not |
|
present, `null` will be stored in the associated cache. `#result` always refers to the |
|
business entity and never a supported wrapper, so the previous example can be rewritten |
|
as follows: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback") |
|
public Optional<Book> findBook(String name) |
|
---- |
|
|
|
Note that `#result` still refers to `Book` and not `Optional<Book>`. Since it might be |
|
`null`, we use SpEL's <<core.adoc#expressions-operator-safe-navigation, safe navigation operator>>. |
|
|
|
[[cache-spel-context]] |
|
==== Available Caching SpEL Evaluation Context |
|
|
|
Each `SpEL` expression evaluates against a dedicated <<core.adoc#expressions-language-ref, `context`>>. |
|
In addition to the built-in parameters, the framework provides dedicated caching-related |
|
metadata, such as the argument names. The following table describes the items made |
|
available to the context so that you can use them for key and conditional computations: |
|
|
|
[[cache-spel-context-tbl]] |
|
.Cache SpEL available metadata |
|
|=== |
|
| Name| Location| Description| Example |
|
|
|
| `methodName` |
|
| Root object |
|
| The name of the method being invoked |
|
| `#root.methodName` |
|
|
|
| `method` |
|
| Root object |
|
| The method being invoked |
|
| `#root.method.name` |
|
|
|
| `target` |
|
| Root object |
|
| The target object being invoked |
|
| `#root.target` |
|
|
|
| `targetClass` |
|
| Root object |
|
| The class of the target being invoked |
|
| `#root.targetClass` |
|
|
|
| `args` |
|
| Root object |
|
| The arguments (as array) used for invoking the target |
|
| `#root.args[0]` |
|
|
|
| `caches` |
|
| Root object |
|
| Collection of caches against which the current method is run |
|
| `#root.caches[0].name` |
|
|
|
| Argument name |
|
| Evaluation context |
|
| Name of any of the method arguments. If the names are not available |
|
(perhaps due to having no debug information), the argument names are also available under the `#a<#arg>` |
|
where `#arg` stands for the argument index (starting from `0`). |
|
| `#iban` or `#a0` (you can also use `#p0` or `#p<#arg>` notation as an alias). |
|
|
|
| `result` |
|
| Evaluation context |
|
| The result of the method call (the value to be cached). Only available in `unless` |
|
expressions, `cache put` expressions (to compute the `key`), or `cache evict` |
|
expressions (when `beforeInvocation` is `false`). For supported wrappers (such as |
|
`Optional`), `#result` refers to the actual object, not the wrapper. |
|
| `#result` |
|
|=== |
|
|
|
|
|
[[cache-annotations-put]] |
|
=== The `@CachePut` Annotation |
|
|
|
When the cache needs to be updated without interfering with the method execution, |
|
you can use the `@CachePut` annotation. That is, the method is always invoked and its |
|
result is placed into the cache (according to the `@CachePut` options). It supports |
|
the same options as `@Cacheable` and should be used for cache population rather than |
|
method flow optimization. The following example uses the `@CachePut` annotation: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
@CachePut(cacheNames="book", key="#isbn") |
|
public Book updateBook(ISBN isbn, BookDescriptor descriptor) |
|
---- |
|
|
|
IMPORTANT: Using `@CachePut` and `@Cacheable` annotations on the same method is generally |
|
strongly discouraged because they have different behaviors. While the latter causes the |
|
method invocation to be skipped by using the cache, the former forces the invocation in |
|
order to run a cache update. This leads to unexpected behavior and, with the exception |
|
of specific corner-cases (such as annotations having conditions that exclude them from each |
|
other), such declarations should be avoided. Note also that such conditions should not rely |
|
on the result object (that is, the `#result` variable), as these are validated up-front to |
|
confirm the exclusion. |
|
|
|
|
|
[[cache-annotations-evict]] |
|
=== The `@CacheEvict` annotation |
|
|
|
The cache abstraction allows not just population of a cache store but also eviction. |
|
This process is useful for removing stale or unused data from the cache. As opposed to |
|
`@Cacheable`, `@CacheEvict` demarcates methods that perform cache |
|
eviction (that is, methods that act as triggers for removing data from the cache). |
|
Similarly to its sibling, `@CacheEvict` requires specifying one or more caches |
|
that are affected by the action, allows a custom cache and key resolution or a |
|
condition to be specified, and features an extra parameter |
|
(`allEntries`) that indicates whether a cache-wide eviction needs to be performed |
|
rather than just an entry eviction (based on the key). The following example evicts |
|
all entries from the `books` cache: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
@CacheEvict(cacheNames="books", allEntries=true) <1> |
|
public void loadBooks(InputStream batch) |
|
---- |
|
<1> Using the `allEntries` attribute to evict all entries from the cache. |
|
|
|
This option comes in handy when an entire cache region needs to be cleared out. |
|
Rather than evicting each entry (which would take a long time, since it is inefficient), |
|
all the entries are removed in one operation, as the preceding example shows. |
|
Note that the framework ignores any key specified in this scenario as it does not apply |
|
(the entire cache is evicted, not only one entry). |
|
|
|
You can also indicate whether the eviction should occur after (the default) or before |
|
the method is invoked by using the `beforeInvocation` attribute. The former provides the |
|
same semantics as the rest of the annotations: Once the method completes successfully, |
|
an action (in this case, eviction) on the cache is run. If the method does not |
|
run (as it might be cached) or an exception is thrown, the eviction does not occur. |
|
The latter (`beforeInvocation=true`) causes the eviction to always occur before the |
|
method is invoked. This is useful in cases where the eviction does not need to be tied |
|
to the method outcome. |
|
|
|
Note that `void` methods can be used with `@CacheEvict` - as the methods act as a |
|
trigger, the return values are ignored (as they do not interact with the cache). This is |
|
not the case with `@Cacheable` which adds data to the cache or updates data in the cache |
|
and, thus, requires a result. |
|
|
|
|
|
[[cache-annotations-caching]] |
|
=== The `@Caching` Annotation |
|
|
|
Sometimes, multiple annotations of the same type (such as `@CacheEvict` or |
|
`@CachePut`) need to be specified -- for example, because the condition or the key |
|
expression is different between different caches. `@Caching` lets multiple nested |
|
`@Cacheable`, `@CachePut`, and `@CacheEvict` annotations be used on the same method. |
|
The following example uses two `@CacheEvict` annotations: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") }) |
|
public Book importBooks(String deposit, Date date) |
|
---- |
|
|
|
|
|
[[cache-annotations-config]] |
|
=== The `@CacheConfig` annotation |
|
|
|
So far, we have seen that caching operations offer many customization options and that |
|
you can set these options for each operation. However, some of the customization options |
|
can be tedious to configure if they apply to all operations of the class. For |
|
instance, specifying the name of the cache to use for every cache operation of the |
|
class can be replaced by a single class-level definition. This is where `@CacheConfig` |
|
comes into play. The following examples uses `@CacheConfig` to set the name of the cache: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
@CacheConfig("books") <1> |
|
public class BookRepositoryImpl implements BookRepository { |
|
|
|
@Cacheable |
|
public Book findBook(ISBN isbn) {...} |
|
} |
|
---- |
|
<1> Using `@CacheConfig` to set the name of the cache. |
|
|
|
`@CacheConfig` is a class-level annotation that allows sharing the cache names, |
|
the custom `KeyGenerator`, the custom `CacheManager`, and the custom `CacheResolver`. |
|
Placing this annotation on the class does not turn on any caching operation. |
|
|
|
An operation-level customization always overrides a customization set on `@CacheConfig`. |
|
Therefore, this gives three levels of customizations for each cache operation: |
|
|
|
* Globally configured, available for `CacheManager`, `KeyGenerator`. |
|
* At the class level, using `@CacheConfig`. |
|
* At the operation level. |
|
|
|
|
|
[[cache-annotation-enable]] |
|
=== Enabling Caching Annotations |
|
|
|
It is important to note that even though declaring the cache annotations does not |
|
automatically trigger their actions - like many things in Spring, the feature has to be |
|
declaratively enabled (which means if you ever suspect caching is to blame, you can |
|
disable it by removing only one configuration line rather than all the annotations in |
|
your code). |
|
|
|
To enable caching annotations add the annotation `@EnableCaching` to one of your |
|
`@Configuration` classes: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
@Configuration |
|
@EnableCaching |
|
public class AppConfig { |
|
} |
|
---- |
|
|
|
Alternatively, for XML configuration you can use the `cache:annotation-driven` element: |
|
|
|
[source,xml,indent=0,subs="verbatim,quotes"] |
|
---- |
|
<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> |
|
---- |
|
|
|
Both the `cache:annotation-driven` element and the `@EnableCaching` annotation let you |
|
specify various options that influence the way the caching behavior is added to the |
|
application through AOP. The configuration is intentionally similar with that of |
|
<<data-access.adoc#tx-annotation-driven-settings, `@Transactional`>>. |
|
|
|
NOTE: The default advice mode for processing caching annotations is `proxy`, which allows |
|
for interception of calls through the proxy only. Local calls within the same class |
|
cannot get intercepted that way. For a more advanced mode of interception, consider |
|
switching to `aspectj` mode in combination with compile-time or load-time weaving. |
|
|
|
NOTE: For more detail about advanced customizations (using Java configuration) that are |
|
required to implement `CachingConfigurer`, see the |
|
{api-spring-framework}/cache/annotation/CachingConfigurer.html[javadoc]. |
|
|
|
[[cache-annotation-driven-settings]] |
|
.Cache annotation settings |
|
[cols="1,1,1,3"] |
|
|=== |
|
| XML Attribute | Annotation Attribute | Default | Description |
|
|
|
| `cache-manager` |
|
| N/A (see the {api-spring-framework}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`] javadoc) |
|
| `cacheManager` |
|
| The name of the cache manager to use. A default `CacheResolver` is initialized behind |
|
the scenes with this cache manager (or `cacheManager` if not set). For more |
|
fine-grained management of the cache resolution, consider setting the 'cache-resolver' |
|
attribute. |
|
|
|
| `cache-resolver` |
|
| N/A (see the {api-spring-framework}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`] javadoc) |
|
| A `SimpleCacheResolver` using the configured `cacheManager`. |
|
| The bean name of the CacheResolver that is to be used to resolve the backing caches. |
|
This attribute is not required and needs to be specified only as an alternative to |
|
the 'cache-manager' attribute. |
|
|
|
| `key-generator` |
|
| N/A (see the {api-spring-framework}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`] javadoc) |
|
| `SimpleKeyGenerator` |
|
| Name of the custom key generator to use. |
|
|
|
| `error-handler` |
|
| N/A (see the {api-spring-framework}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`] javadoc) |
|
| `SimpleCacheErrorHandler` |
|
| The name of the custom cache error handler to use. By default, any exception thrown during |
|
a cache related operation is thrown back at the client. |
|
|
|
| `mode` |
|
| `mode` |
|
| `proxy` |
|
| The default mode (`proxy`) processes annotated beans to be proxied by using Spring's AOP |
|
framework (following proxy semantics, as discussed earlier, applying to method calls |
|
coming in through the proxy only). The alternative mode (`aspectj`) instead weaves the |
|
affected classes with Spring's AspectJ caching aspect, modifying the target class byte |
|
code to apply to any kind of method call. AspectJ weaving requires `spring-aspects.jar` |
|
in the classpath as well as load-time weaving (or compile-time weaving) enabled. (See |
|
<<core.adoc#aop-aj-ltw-spring, Spring configuration>> for details on how to set up |
|
load-time weaving.) |
|
|
|
| `proxy-target-class` |
|
| `proxyTargetClass` |
|
| `false` |
|
| Applies to proxy mode only. Controls what type of caching proxies are created for |
|
classes annotated with the `@Cacheable` or `@CacheEvict` annotations. If the |
|
`proxy-target-class` attribute is set to `true`, class-based proxies are created. |
|
If `proxy-target-class` is `false` or if the attribute is omitted, standard JDK |
|
interface-based proxies are created. (See <<core.adoc#aop-proxying, Proxying Mechanisms>> |
|
for a detailed examination of the different proxy types.) |
|
|
|
| `order` |
|
| `order` |
|
| Ordered.LOWEST_PRECEDENCE |
|
| Defines the order of the cache advice that is applied to beans annotated with |
|
`@Cacheable` or `@CacheEvict`. (For more information about the rules related to |
|
ordering AOP advice, see <<core.adoc#aop-ataspectj-advice-ordering, Advice Ordering>>.) |
|
No specified ordering means that the AOP subsystem determines the order of the advice. |
|
|=== |
|
|
|
NOTE: `<cache:annotation-driven/>` looks for `@Cacheable/@CachePut/@CacheEvict/@Caching` |
|
only on beans in the same application context in which it is defined. This means that, |
|
if you put `<cache:annotation-driven/>` in a `WebApplicationContext` for a |
|
`DispatcherServlet`, it checks for beans only in your controllers, not your services. |
|
See <<web.adoc#mvc-servlet, the MVC section>> for more information. |
|
|
|
.Method visibility and cache annotations |
|
**** |
|
When you use proxies, you should apply the cache annotations only to methods with |
|
public visibility. If you do annotate protected, private, or package-visible methods |
|
with these annotations, no error is raised, but the annotated method does not exhibit |
|
the configured caching settings. Consider using AspectJ (see the rest of this section) |
|
if you need to annotate non-public methods, as it changes the bytecode itself. |
|
**** |
|
|
|
TIP: Spring recommends that you only annotate concrete classes (and methods of concrete |
|
classes) with the `@Cache{asterisk}` annotations, as opposed to annotating interfaces. |
|
You certainly can place an `@Cache{asterisk}` annotation on an interface (or an interface |
|
method), but this works only if you use the proxy mode (`mode="proxy"`). If you use the |
|
weaving-based aspect (`mode="aspectj"`), the caching settings are not recognized on |
|
interface-level declarations by the weaving infrastructure. |
|
|
|
NOTE: In proxy mode (the default), only external method calls coming in through the |
|
proxy are intercepted. This means that self-invocation (in effect, a method within the |
|
target object that calls another method of the target object) does not lead to actual |
|
caching at runtime even if the invoked method is marked with `@Cacheable`. Consider |
|
using the `aspectj` mode in this case. Also, the proxy must be fully initialized to |
|
provide the expected behavior, so you should not rely on this feature in your |
|
initialization code (that is, `@PostConstruct`). |
|
|
|
|
|
[[cache-annotation-stereotype]] |
|
=== Using Custom Annotations |
|
|
|
.Custom annotation and AspectJ |
|
**** |
|
This feature works only with the proxy-based approach but can be enabled |
|
with a bit of extra effort by using AspectJ. |
|
|
|
The `spring-aspects` module defines an aspect for the standard annotations only. |
|
If you have defined your own annotations, you also need to define an aspect for |
|
those. Check `AnnotationCacheAspect` for an example. |
|
**** |
|
|
|
The caching abstraction lets you use your own annotations to identify what method |
|
triggers cache population or eviction. This is quite handy as a template mechanism, |
|
as it eliminates the need to duplicate cache annotation declarations, which is |
|
especially useful if the key or condition are specified or if the foreign imports |
|
(`org.springframework`) are not allowed in your code base. Similarly to the rest |
|
of the <<core.adoc#beans-stereotype-annotations, stereotype>> annotations, you can |
|
use `@Cacheable`, `@CachePut`, `@CacheEvict`, and `@CacheConfig` as |
|
<<core.adoc#beans-meta-annotations, meta-annotations>> (that is, annotations that |
|
can annotate other annotations). In the following example, we replace a common |
|
`@Cacheable` declaration with our own custom annotation: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
@Retention(RetentionPolicy.RUNTIME) |
|
@Target({ElementType.METHOD}) |
|
@Cacheable(cacheNames="books", key="#isbn") |
|
public @interface SlowService { |
|
} |
|
---- |
|
|
|
In the preceding example, we have defined our own `SlowService` annotation, |
|
which itself is annotated with `@Cacheable`. Now we can replace the following code: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
@Cacheable(cacheNames="books", key="#isbn") |
|
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) |
|
---- |
|
|
|
The following example shows the custom annotation with which we can replace the |
|
preceding code: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
@SlowService |
|
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) |
|
---- |
|
|
|
Even though `@SlowService` is not a Spring annotation, the container automatically picks |
|
up its declaration at runtime and understands its meaning. Note that, as mentioned |
|
<<cache-annotation-enable, earlier>>, annotation-driven behavior needs to be enabled. |
|
|
|
|
|
|
|
[[cache-jsr-107]] |
|
== JCache (JSR-107) Annotations |
|
|
|
Since version 4.1, Spring's caching abstraction fully supports the JCache standard |
|
(JSR-107) annotations: `@CacheResult`, `@CachePut`, `@CacheRemove`, and `@CacheRemoveAll` |
|
as well as the `@CacheDefaults`, `@CacheKey`, and `@CacheValue` companions. |
|
You can use these annotations even without migrating your cache store to JSR-107. |
|
The internal implementation uses Spring's caching abstraction and provides default |
|
`CacheResolver` and `KeyGenerator` implementations that are compliant with the |
|
specification. In other words, if you are already using Spring's caching abstraction, |
|
you can switch to these standard annotations without changing your cache storage |
|
(or configuration, for that matter). |
|
|
|
|
|
[[cache-jsr-107-summary]] |
|
=== Feature Summary |
|
|
|
For those who are familiar with Spring's caching annotations, the following table |
|
describes the main differences between the Spring annotations and their JSR-107 |
|
counterparts: |
|
|
|
.Spring vs. JSR-107 caching annotations |
|
[cols="1,1,3"] |
|
|=== |
|
| Spring | JSR-107 | Remark |
|
|
|
| `@Cacheable` |
|
| `@CacheResult` |
|
| Fairly similar. `@CacheResult` can cache specific exceptions and force the |
|
execution of the method regardless of the content of the cache. |
|
|
|
| `@CachePut` |
|
| `@CachePut` |
|
| While Spring updates the cache with the result of the method invocation, JCache |
|
requires that it be passed it as an argument that is annotated with `@CacheValue`. |
|
Due to this difference, JCache allows updating the cache before or after the |
|
actual method invocation. |
|
|
|
| `@CacheEvict` |
|
| `@CacheRemove` |
|
| Fairly similar. `@CacheRemove` supports conditional eviction when the |
|
method invocation results in an exception. |
|
|
|
| `@CacheEvict(allEntries=true)` |
|
| `@CacheRemoveAll` |
|
| See `@CacheRemove`. |
|
|
|
| `@CacheConfig` |
|
| `@CacheDefaults` |
|
| Lets you configure the same concepts, in a similar fashion. |
|
|=== |
|
|
|
JCache has the notion of `javax.cache.annotation.CacheResolver`, which is identical |
|
to the Spring's `CacheResolver` interface, except that JCache supports only a single |
|
cache. By default, a simple implementation retrieves the cache to use based on the |
|
name declared on the annotation. It should be noted that, if no cache name is |
|
specified on the annotation, a default is automatically generated. See the javadoc |
|
of `@CacheResult#cacheName()` for more information. |
|
|
|
`CacheResolver` instances are retrieved by a `CacheResolverFactory`. It is possible |
|
to customize the factory for each cache operation, as the following example shows: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
@CacheResult(cacheNames="books", cacheResolverFactory=MyCacheResolverFactory.class) <1> |
|
public Book findBook(ISBN isbn) |
|
---- |
|
<1> Customizing the factory for this operation. |
|
|
|
NOTE: For all referenced classes, Spring tries to locate a bean with the given type. |
|
If more than one match exists, a new instance is created and can use the regular |
|
bean lifecycle callbacks, such as dependency injection. |
|
|
|
Keys are generated by a `javax.cache.annotation.CacheKeyGenerator` that serves the |
|
same purpose as Spring's `KeyGenerator`. By default, all method arguments are taken |
|
into account, unless at least one parameter is annotated with `@CacheKey`. This is |
|
similar to Spring's <<cache-annotations-cacheable-key, custom key generation |
|
declaration>>. For instance, the following are identical operations, one using |
|
Spring's abstraction and the other using JCache: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
@Cacheable(cacheNames="books", key="#isbn") |
|
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) |
|
|
|
@CacheResult(cacheName="books") |
|
public Book findBook(@CacheKey ISBN isbn, boolean checkWarehouse, boolean includeUsed) |
|
---- |
|
|
|
You can also specify the `CacheKeyResolver` on the operation, similar to how you can |
|
specify the `CacheResolverFactory`. |
|
|
|
JCache can manage exceptions thrown by annotated methods. This can prevent an update of |
|
the cache, but it can also cache the exception as an indicator of the failure instead of |
|
calling the method again. Assume that `InvalidIsbnNotFoundException` is thrown if the |
|
structure of the ISBN is invalid. This is a permanent failure (no book could ever be |
|
retrieved with such a parameter). The following caches the exception so that further |
|
calls with the same, invalid, ISBN throw the cached exception directly instead of |
|
invoking the method again: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
@CacheResult(cacheName="books", exceptionCacheName="failures" |
|
cachedExceptions = InvalidIsbnNotFoundException.class) |
|
public Book findBook(ISBN isbn) |
|
---- |
|
|
|
|
|
=== Enabling JSR-107 Support |
|
|
|
You do not need to do anything specific to enable the JSR-107 support alongside Spring's |
|
declarative annotation support. Both `@EnableCaching` and the `cache:annotation-driven` |
|
XML element automatically enable the JCache support if both the JSR-107 API and the |
|
`spring-context-support` module are present in the classpath. |
|
|
|
NOTE: Depending on your use case, the choice is basically yours. You can even mix and |
|
match services by using the JSR-107 API on some and using Spring's own annotations on |
|
others. However, if these services impact the same caches, you should use a consistent |
|
and identical key generation implementation. |
|
|
|
|
|
|
|
[[cache-declarative-xml]] |
|
== Declarative XML-based Caching |
|
|
|
If annotations are not an option (perhaps due to having no access to the sources |
|
or no external code), you can use XML for declarative caching. So, instead of |
|
annotating the methods for caching, you can specify the target method and the |
|
caching directives externally (similar to the declarative transaction management |
|
<<data-access.adoc#transaction-declarative-first-example, advice>>). The example |
|
from the previous section can be translated into the following example: |
|
|
|
[source,xml,indent=0] |
|
[subs="verbatim"] |
|
---- |
|
<!-- the service we want to make cacheable --> |
|
<bean id="bookService" class="x.y.service.DefaultBookService"/> |
|
|
|
<!-- cache definitions --> |
|
<cache:advice id="cacheAdvice" cache-manager="cacheManager"> |
|
<cache:caching cache="books"> |
|
<cache:cacheable method="findBook" key="#isbn"/> |
|
<cache:cache-evict method="loadBooks" all-entries="true"/> |
|
</cache:caching> |
|
</cache:advice> |
|
|
|
<!-- apply the cacheable behavior to all BookService interfaces --> |
|
<aop:config> |
|
<aop:advisor advice-ref="cacheAdvice" pointcut="execution(* x.y.BookService.*(..))"/> |
|
</aop:config> |
|
|
|
<!-- cache manager definition omitted --> |
|
---- |
|
|
|
In the preceding configuration, the `bookService` is made cacheable. The caching semantics |
|
to apply are encapsulated in the `cache:advice` definition, which causes the `findBooks` |
|
method to be used for putting data into the cache and the `loadBooks` method for evicting |
|
data. Both definitions work against the `books` cache. |
|
|
|
The `aop:config` definition applies the cache advice to the appropriate points in the |
|
program by using the AspectJ pointcut expression (more information is available in |
|
<<core.adoc#aop, Aspect Oriented Programming with Spring>>). In the preceding example, |
|
all methods from the `BookService` are considered and the cache advice is applied to them. |
|
|
|
The declarative XML caching supports all of the annotation-based model, so moving between |
|
the two should be fairly easy. Furthermore, both can be used inside the same application. |
|
The XML-based approach does not touch the target code. However, it is inherently more |
|
verbose. When dealing with classes that have overloaded methods that are targeted for |
|
caching, identifying the proper methods does take an extra effort, since the `method` |
|
argument is not a good discriminator. In these cases, you can use the AspectJ pointcut |
|
to cherry pick the target methods and apply the appropriate caching functionality. |
|
However, through XML, it is easier to apply package or group or interface-wide caching |
|
(again, due to the AspectJ pointcut) and to create template-like definitions (as we did |
|
in the preceding example by defining the target cache through the `cache:definitions` |
|
`cache` attribute). |
|
|
|
|
|
|
|
[[cache-store-configuration]] |
|
== Configuring the Cache Storage |
|
|
|
The cache abstraction provides several storage integration options. To use them, you need |
|
to declare an appropriate `CacheManager` (an entity that controls and manages `Cache` |
|
instances and that can be used to retrieve these for storage). |
|
|
|
|
|
[[cache-store-configuration-jdk]] |
|
=== JDK `ConcurrentMap`-based Cache |
|
|
|
The JDK-based `Cache` implementation resides under |
|
`org.springframework.cache.concurrent` package. It lets you use `ConcurrentHashMap` |
|
as a backing `Cache` store. The following example shows how to configure two caches: |
|
|
|
[source,xml,indent=0,subs="verbatim,quotes"] |
|
---- |
|
<!-- simple cache manager --> |
|
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager"> |
|
<property name="caches"> |
|
<set> |
|
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default"/> |
|
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="books"/> |
|
</set> |
|
</property> |
|
</bean> |
|
---- |
|
|
|
The preceding snippet uses the `SimpleCacheManager` to create a `CacheManager` for the |
|
two nested `ConcurrentMapCache` instances named `default` and `books`. Note that the |
|
names are configured directly for each cache. |
|
|
|
As the cache is created by the application, it is bound to its lifecycle, making it |
|
suitable for basic use cases, tests, or simple applications. The cache scales well |
|
and is very fast, but it does not provide any management, persistence capabilities, |
|
or eviction contracts. |
|
|
|
|
|
[[cache-store-configuration-eviction]] |
|
=== Ehcache-based Cache |
|
|
|
Ehcache 3.x is fully JSR-107 compliant and no dedicated support is required for it. See |
|
<<cache-store-configuration-jsr107>> for details. |
|
|
|
|
|
[[cache-store-configuration-caffeine]] |
|
=== Caffeine Cache |
|
|
|
Caffeine is a Java 8 rewrite of Guava's cache, and its implementation is located in the |
|
`org.springframework.cache.caffeine` package and provides access to several features |
|
of Caffeine. |
|
|
|
The following example configures a `CacheManager` that creates the cache on demand: |
|
|
|
[source,xml,indent=0,subs="verbatim,quotes"] |
|
---- |
|
<bean id="cacheManager" |
|
class="org.springframework.cache.caffeine.CaffeineCacheManager"/> |
|
---- |
|
|
|
You can also provide the caches to use explicitly. In that case, only those |
|
are made available by the manager. The following example shows how to do so: |
|
|
|
[source,xml,indent=0,subs="verbatim,quotes"] |
|
---- |
|
<bean id="cacheManager" class="org.springframework.cache.caffeine.CaffeineCacheManager"> |
|
<property name="cacheNames"> |
|
<set> |
|
<value>default</value> |
|
<value>books</value> |
|
</set> |
|
</property> |
|
</bean> |
|
---- |
|
|
|
The Caffeine `CacheManager` also supports custom `Caffeine` and `CacheLoader`. |
|
See the https://github.com/ben-manes/caffeine/wiki[Caffeine documentation] |
|
for more information about those. |
|
|
|
|
|
[[cache-store-configuration-gemfire]] |
|
=== GemFire-based Cache |
|
|
|
GemFire is a memory-oriented, disk-backed, elastically scalable, continuously available, |
|
active (with built-in pattern-based subscription notifications), globally replicated |
|
database and provides fully-featured edge caching. For further information on how to |
|
use GemFire as a `CacheManager` (and more), see the |
|
{docs-spring-gemfire}/html/[Spring Data GemFire reference documentation]. |
|
|
|
|
|
[[cache-store-configuration-jsr107]] |
|
=== JSR-107 Cache |
|
|
|
Spring's caching abstraction can also use JSR-107-compliant caches. The JCache |
|
implementation is located in the `org.springframework.cache.jcache` package. |
|
|
|
Again, to use it, you need to declare the appropriate `CacheManager`. |
|
The following example shows how to do so: |
|
|
|
[source,xml,indent=0,subs="verbatim,quotes"] |
|
---- |
|
<bean id="cacheManager" |
|
class="org.springframework.cache.jcache.JCacheCacheManager" |
|
p:cache-manager-ref="jCacheManager"/> |
|
|
|
<!-- JSR-107 cache manager setup --> |
|
<bean id="jCacheManager" .../> |
|
---- |
|
|
|
|
|
[[cache-store-configuration-noop]] |
|
=== Dealing with Caches without a Backing Store |
|
|
|
Sometimes, when switching environments or doing testing, you might have cache |
|
declarations without having an actual backing cache configured. As this is an invalid |
|
configuration, an exception is thrown at runtime, since the caching infrastructure |
|
is unable to find a suitable store. In situations like this, rather than removing the |
|
cache declarations (which can prove tedious), you can wire in a simple dummy cache that |
|
performs no caching -- that is, it forces the cached methods to be invoked every time. |
|
The following example shows how to do so: |
|
|
|
[source,xml,indent=0,subs="verbatim,quotes"] |
|
---- |
|
<bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager"> |
|
<property name="cacheManagers"> |
|
<list> |
|
<ref bean="jdkCache"/> |
|
<ref bean="gemfireCache"/> |
|
</list> |
|
</property> |
|
<property name="fallbackToNoOpCache" value="true"/> |
|
</bean> |
|
---- |
|
|
|
The `CompositeCacheManager` in the preceding chains multiple `CacheManager` instances and, |
|
through the `fallbackToNoOpCache` flag, adds a no-op cache for all the definitions not |
|
handled by the configured cache managers. That is, every cache definition not found in |
|
either `jdkCache` or `gemfireCache` (configured earlier in the example) is handled by |
|
the no-op cache, which does not store any information, causing the target method to be |
|
invoked every time. |
|
|
|
|
|
|
|
[[cache-plug]] |
|
== Plugging-in Different Back-end Caches |
|
|
|
Clearly, there are plenty of caching products out there that you can use as a backing |
|
store. For those that do not support JSR-107 you need to provide a `CacheManager` and a |
|
`Cache` implementation. This may sound harder than it is, since, in practice, the classes |
|
tend to be simple https://en.wikipedia.org/wiki/Adapter_pattern[adapters] that map the |
|
caching abstraction framework on top of the storage API, as the _Caffeine_ classes do. |
|
Most `CacheManager` classes can use the classes in the |
|
`org.springframework.cache.support` package (such as `AbstractCacheManager` which takes |
|
care of the boiler-plate code, leaving only the actual mapping to be completed). |
|
|
|
|
|
|
|
[[cache-specific-config]] |
|
== How can I Set the TTL/TTI/Eviction policy/XXX feature? |
|
|
|
Directly through your cache provider. The cache abstraction is an abstraction, |
|
not a cache implementation. The solution you use might support various data |
|
policies and different topologies that other solutions do not support (for example, |
|
the JDK `ConcurrentHashMap` -- exposing that in the cache abstraction would be useless |
|
because there would no backing support). Such functionality should be controlled |
|
directly through the backing cache (when configuring it) or through its native API. |
|
|
|
|