<dependency>
<groupId>io.github.ms100</groupId>
<artifactId>cache-as-multi</artifactId>
<version>1.3.1</version>
</dependency>
If the List returned by a batch method cannot guarantee the same size and order as the "object collection parameter",
you can use @CacheAsMulti.asElementField
to specify the field in the List where the elements of the "object collection
parameter" are located. This will be more suitable for database queries.
class CarService {
@Cacheable(cacheNames = "car")
@CacheResult(cacheName = "car")
public List<CarPO> findCars(@CacheAsMulti(asElementField = "info.id") List<Integer> ids) {
// The size of the returned List does not have to be the same as ids.
}
public static class CarPO {
private CarInfoPO info;
private String name;
}
public static class CarInfoPO {
private Integer id;
}
}
This annotation needs to be used in conjunction with the following two sets of annotations to achieve batch caching operations on the method where the annotated parameter is located.
-
Spring's caching annotations
@Cacheable
,@CachePut
,@CacheEvict
-
JSR-107 annotations
@CacheResult
,@CachePut
,@CacheRemove
,@CacheKey
Only PROXY mode is supported, not ASPECTJ mode.
Suppose there is a method for obtaining a single object, as follows:
class FooService {
public Foo getFoo(Integer fooId) {
//...
}
}
At this point, if you need to get a batch of objects, there are usually two ways to write it:
class FooService {
public Map<Integer, Foo> getMultiFoo(Collection<Integer> fooIds) {
//...
}
public List<Foo> getMultiFoo(List<Integer> fooIds) {
//...
}
}
There are two changes to the method of obtaining batch objects compared to the method of obtaining single objects:
- The input parameter changes from a single object (referred to as an "object parameter" below) to an object
collection (referred to as an "object collection parameter" below), for example,
Integer
changes toCollection<Integer>
orSet<Integer>
orList<Integer>
. - The return value changes from a single object to
Map<K, V>
orList<V>
. For example,Map<Integer, Foo>
orList<Foo>
. If the returned type isList
, it should be the same size as the "object collection parameter" and in the same order(PS: After version v1.3, this limitation no longer exists. Please refer to the update details for more information.).
In the above example, if you need to cache the method of obtaining a single object, you will use the @Cacheable
or @CacheResult
annotation: (PS: Here, @CacheResult
and @Cacheable
are used together as an example, in actual use,
usually only one of them is used)
class FooService {
@Cacheable(cacheNames = "foo")
@CacheResult(cacheName = "foo")
public Foo getFoo(Integer fooId) {
// Use fooId to generate cache key and calculate condition and unless conditions, use Foo as cache value
}
}
If @Cacheable
or @CacheResult
is directly added to the method of getting batch objects, a cache key will be
generated for the entire【collection parameter】 and the returned Map
or List
will be used as a cache value.
However, we usually hope that it can be transformed into multiple fooId => Foo
caches, that is: each【element】in
the【collection parameter】and its corresponding value are cached separately. At this time, just add the @CacheAsMulti
annotation on the【collection parameter】to achieve the caching method we want.
class FooService {
@Cacheable(cacheNames = "foo")
@CacheResult(cacheName = "foo")
public Map<Integer, Foo> getMultiFoo(@CacheAsMulti Collection<Integer> fooIds) {
// Generate a cache key and calculate condition and unless conditions for each element in the fooIds collection,
// use the corresponding value in the Map as the cache value
}
@Cacheable(cacheNames = "foo")
@CacheResult(cacheName = "foo")
public List<Foo> getMultiFoo(@CacheAsMulti List<Integer> fooIds) {
// Generate a cache key and calculate condition and unless conditions for each element in the fooIds collection,
// use the corresponding value in the List as the cache value
// In the following examples, the handling method for returning List and returning Map is the same,
// so they will not be separately demonstrated.
}
}
- When used with Spring's
@CachePut
, it follows the same example as above. - When used with
@CacheEvict
, if the@CacheEvict.key()
parameter in the annotation does not contain#result
, there is no requirement for the return type of the method; if#result
is present in the key, the return type of the method needs to beMap
orList
. - When used with both Spring's
@CachePut
and@CacheEvict
, if the key parameter already contains#result
, there is no need for a reference to the object collection parameter. - When used with
@CacheRemove
, there is no requirement for the return type of the method. - When used with JSR-107's
@CachePut
, there is no requirement for the return type of the method, and the following example can be referred to:
Single parameter as key, without configuring @CacheKey
:
class FooService {
@CachePut(cacheName = "foo")
public void putFoo(Integer fooId, @CacheValue String value) {
// Generate the cache key using the fooId parameter and use value as the cache value
}
@CachePut(cacheName = "foo")
public void putMultiFoo(@CacheAsMulti @CacheValue Map<Integer, String> fooIdValueMap) {
// In this case, the @CacheValue parameter of the method must be of type Map
// Generate a cache key using each Entry key in fooIdValueMap, and use Entry value as the cache value
}
}
@CacheAsMulti
annotation cannot replace thekey
parameter in Spring cache annotations, such as@Cacheable.key()
, nor the@CacheKey
and@CacheValue
annotations.- If a custom
KeyGenerator
is used, anObject[]
will be generated by combining eachelement
of thecollection parameter
and other parameters to calculate the cache key usingKeyGenerator.generate(Object, Method, Object...)
; the same goes for customCacheKeyGenerator
. - When used in conjunction with
@Cacheable
,@CacheResult
and@CachePut
, ifCacheAsMulti.strictNull()
istrue
and the return type of the method isMap
, the value of the correspondingelement
in theMap
isnull
, thennull
will be cached, and if theelement
does not exist in theMap
, it will not be cached. - When used in conjunction with
@CachePut
and@CacheEvict
, if the key parameter of the annotation is configured with#result
, and the return type of the method isMap
,null
will be used as the default value to calculate the cache key and condition and unless conditions for theelement
that does not exist in theMap
. @Cacheable.condition()
,@Cacheable.unless()
and other conditional expressions are calculated using eachelement
of thecollection parameter
, and only exclude theelements
that do not meet the conditions, rather than the entire collection.
The org.springframework.cache.Cache
interface only defines a single cache operation and does not support batch
operations. Therefore, the EnhancedCache
interface is defined to extend three batch operation
methods: multiGet
, multiPut
, and multiEvict
.
When using a certain caching medium, there needs to be a corresponding implementation of the EnhancedCache
interface.
If the medium used does not have a corresponding implementation of EnhancedCache
, the
default EnhancedCacheConversionService.EnhancedCacheAdapter
will be used for adaptation, which implements batch
operations by iterating through single operations, resulting in lower efficiency. At the same time, there will be a
warn-level log when the object is created.
Each caching medium also needs to define a converter to automatically convert Cache
to EnhancedCache
. The interface
implemented by the converter is EnhancedCacheConverter
. The converters registered in the BeanFactory
will
automatically be loaded into the EnhancedCacheConversionService
to convert Spring's original Cache
to EnhancedCache
.
The EnhancedCache
interface and the corresponding converters for RedisCache
, ConcurrentMapCache
, Ehcache
,
and caffeineCache
have been implemented in the package, which can be viewed under cache.convert.converter
.
- After the standard
BeanDefinition
, modify the originalOperationSource
andInterceptor
Bean definitions, and replace them with custom (inherited original) definitions. - After the original
OperationSource
queries and constructs anOperation
, query and construct aMultiOperation
and cache it. - Before the original
Interceptor
executes the interception, check whether the correspondingMultiOperation
is cached. If it is, then intercept and execute.
- Define
EnhancedCache
to extend Spring'sCache
. - Define
EnhancedCacheConverter
to convertCache
toEnhancedCache
. - Implement
EnhancedCache
andEnhancedCacheConverter
in the corresponding implementation class ofCache
. - Define
EnhancedCacheConversionService
to automatically inject allEnhancedCacheConverter
(including those defined by the user). - Define
EnhancedCacheResolver
to wrapCacheResolver
, injectEnhancedCacheConversionService
, and convertCache
toEnhancedCache
when callingresolveCaches
to obtainCache
.
GenericTypeResolver
handles generic classes.ReflectionUtils
is a reflection utility class.ResolvableType
handles various field types, return types, and parameter types.AnnotationUtils
is an annotation utility class, for example, to find the annotation of the parent class.AnnotatedElementUtils
is a utility class for annotated elements, for example, to find merged annotations.MergedAnnotations
is an operation utility for merged annotations.
- The parameter alias of Spring's
@AliasFor
annotation is implemented using the above Spring annotation tools. - Handling of
Aware
requires explicit implementation, such as in the implementation ofBeanPostProcessor
. - If a
Map
has no correspondingSet
implementation, you can useCollections.newSetFromMap(new ConcurrentHashMap<>(16))
. AutowiredAnnotationBeanPostProcessor
handles the@Autowired
annotation.ApplicationContextAwareProcessor
handles the implementation of theApplicationContextAware
interface.- When using reflection in Java to get parameter names, if the names are arg0, arg1, etc., in addition to the solution
found online (using javac -parameters), you can also use Spring's
DefaultParameterNameDiscoverer
.
Cache expiration can be configured separately for different caches and different cache implementations, for example Redis:
spring:
cache:
redis:
time-to-live: PT15M #cache for 15 minutes
Premise: Spring generates a separate Cache object for each CacheName configured in the cache annotation.
Usually, you can achieve this in the following three ways:
- Customize
CacheManager
orCacheResolver
. - Use other caching frameworks (cannot use
@CacheAsMulti
), such as JetCache. - Customize separately for different caching implementation.
Simply implement the RedisCacheManagerBuilderCustomizer
interface to customize configuration before
the RedisCacheManager
is generated.
See
RedisCacheCustomizer
class for details.
After that, just add the following configuration:
spring:
cache:
redis:
time-to-live: PT15M #default cache for 15 minutes
cache-as-multi: #Below are the cache-as-multi configurations
serialize-to-json: true #Use RedisSerializer.json() for serialization
cache-name-time-to-live-map: #Cache time corresponding to cacheName
foo: PT15S #foo cache for 15 seconds
demo: PT5M #demo cache for 5 minutes