Spring cache is a caching API layer that encapsulates common operations for a variety of caches and can easily add caching functionality to your application with the help of annotations.
The common annotations are @Cacheable
, @CachePut
, @CacheEvict
, ever wondered what is the principle behind them? With questions, after reading the source code of Spring cache, make a brief summary.
First the conclusion, the core logic in the CacheAspectSupport class , which encapsulates the main logic of all cache operations, is described in detail below.
Off-topic: How to read open source code?
There are 2 methods, which can be used in combination.
- static code reading : find the key classes, methods where the usages, skilled use of find usages function, find all relevant classes, methods, static analysis of the core logic of the implementation process, step by step, until the establishment of the full picture.
- Run-time debug: add breakpoints on key methods and write a unit test to call the class library/framework, skillfully use step into/step over/resume to dynamically analyze the code execution process.
Core Class Diagram
As shown in the figure, it can be divided into the following classes.
Cache
, CacheManager
: Cache abstracts common operations of cache, such as get, put, and CacheManager is a collection of Cache. The reason why multiple Cache objects are needed is because of the need for multiple cache expiration times, cache entry limits, etc.
CacheInterceptor
, CacheAspectSupport
, AbstractCacheInvoker
: CacheInterceptor
is an AOP method interceptor that does additional logic before and after the method, i.e. querying the cache, writing to the cache, etc. It inherits from .CacheAspectSupport
(the main logic of cache operations), AbstractCacheInvoker
(encapsulates reading and writing to the Cache)
CacheOperation
, AnnotationCacheOperationSource
, SpringCacheAnnotationParser
: CacheOperation
defines the cache name, cache key, cache condition condition, cache operation CacheManager
, etc. AnnotationCacheOperationSource
is a class that gets the cache annotation corresponding to the CacheOperation, while SpringCacheAnnotationParser
is the class that actually parses the annotation, and after parsing it is encapsulated into the CacheOperation collection for AnnotationCacheOperationSource
to find.
The following is an analysis of Spring cache source code with annotated explanations and only excerpts of core code snippets.
1. Parsing annotations
First look at how annotations are parsed. Annotation is just a marker, to make it really work, you need to do parsing operations on annotations and also have the corresponding actual logic.
SpringCacheAnnotationParser: responsible for parsing annotations, return CacheOperation collection
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
public class SpringCacheAnnotationParser implements CacheAnnotationParser, Serializable {
// Parsing class-level cache annotations
@Override
public Collection<CacheOperation> parseCacheAnnotations(Class<?> type) {
DefaultCacheConfig defaultConfig = getDefaultCacheConfig(type);
return parseCacheAnnotations(defaultConfig, type);
}
// 解析方法级别的缓存注解
@Override
public Collection<CacheOperation> parseCacheAnnotations(Method method) {
DefaultCacheConfig defaultConfig = getDefaultCacheConfig(method.getDeclaringClass());
return parseCacheAnnotations(defaultConfig, method);
}
// Parsing cache annotations
private Collection<CacheOperation> parseCacheAnnotations(DefaultCacheConfig cachingConfig, AnnotatedElement ae) {
Collection<CacheOperation> ops = null;
// Parsing the @Cacheable annotation
Collection<Cacheable> cacheables = AnnotatedElementUtils.getAllMergedAnnotations(ae, Cacheable.class);
if (!cacheables.isEmpty()) {
ops = lazyInit(ops);
for (Cacheable cacheable : cacheables) {
ops.add(parseCacheableAnnotation(ae, cachingConfig, cacheable));
}
}
// Parsing the @CacheEvict annotation
Collection<CacheEvict> evicts = AnnotatedElementUtils.getAllMergedAnnotations(ae, CacheEvict.class);
if (!evicts.isEmpty()) {
ops = lazyInit(ops);
for (CacheEvict evict : evicts) {
ops.add(parseEvictAnnotation(ae, cachingConfig, evict));
}
}
// Parsing the @CachePut annotation
Collection<CachePut> puts = AnnotatedElementUtils.getAllMergedAnnotations(ae, CachePut.class);
if (!puts.isEmpty()) {
ops = lazyInit(ops);
for (CachePut put : puts) {
ops.add(parsePutAnnotation(ae, cachingConfig, put));
}
}
// Parsing the @Caching annotation
Collection<Caching> cachings = AnnotatedElementUtils.getAllMergedAnnotations(ae, Caching.class);
if (!cachings.isEmpty()) {
ops = lazyInit(ops);
for (Caching caching : cachings) {
Collection<CacheOperation> cachingOps = parseCachingAnnotation(ae, cachingConfig, caching);
if (cachingOps != null) {
ops.addAll(cachingOps);
}
}
}
return ops;
}
|
AnnotationCacheOperationSource
call SpringCacheAnnotationParser to get the annotation corresponding to the CacheOperation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperationSource implements Serializable {
// Find a list of CacheOperation at the class level
@Override
protected Collection<CacheOperation> findCacheOperations(final Class<?> clazz) {
return determineCacheOperations(new CacheOperationProvider() {
@Override
public Collection<CacheOperation> getCacheOperations(CacheAnnotationParser parser) {
return parser.parseCacheAnnotations(clazz);
}
});
}
// Find the list of CacheOperation at the method level
@Override
protected Collection<CacheOperation> findCacheOperations(final Method method) {
return determineCacheOperations(new CacheOperationProvider() {
@Override
public Collection<CacheOperation> getCacheOperations(CacheAnnotationParser parser) {
return parser.parseCacheAnnotations(method);
}
});
}
}
|
AbstractFallbackCacheOperationSource
The parent class of AnnotationCacheOperationSource, which implements the common logic of getting CacheOperation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
|
public abstract class AbstractFallbackCacheOperationSource implements CacheOperationSource {
/**
* Cache of CacheOperations, keyed by method on a specific target class.
* <p>As this base class is not marked Serializable, the cache will be recreated
* after serialization - provided that the concrete subclass is Serializable.
*/
private final Map<Object, Collection<CacheOperation>> attributeCache =
new ConcurrentHashMap<Object, Collection<CacheOperation>>(1024);
// According to Method, Class reflection information, get the corresponding CacheOperation list
@Override
public Collection<CacheOperation> getCacheOperations(Method method, Class<?> targetClass) {
if (method.getDeclaringClass() == Object.class) {
return null;
}
Object cacheKey = getCacheKey(method, targetClass);
Collection<CacheOperation> cached = this.attributeCache.get(cacheKey);
// Because parsing reflection information is time-consuming, so use map cache to avoid double counting
// If already recorded in map, return directly
if (cached != null) {
return (cached != NULL_CACHING_ATTRIBUTE ? cached : null);
}
// Otherwise do a calculation and write to map
else {
Collection<CacheOperation> cacheOps = computeCacheOperations(method, targetClass);
if (cacheOps != null) {
if (logger.isDebugEnabled()) {
logger.debug("Adding cacheable method '" + method.getName() + "' with attribute: " + cacheOps);
}
this.attributeCache.put(cacheKey, cacheOps);
}
else {
this.attributeCache.put(cacheKey, NULL_CACHING_ATTRIBUTE);
}
return cacheOps;
}
}
// Calculate the list of cache operations, giving priority to the annotations on the methods of the target proxy class, followed by the target proxy class if it does not exist, then the methods of the original class, and finally the original class
private Collection<CacheOperation> computeCacheOperations(Method method, Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// The method may be on an interface, but we need attributes from the target class.
// If the target class is null, the method will be unchanged.
Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
// If we are dealing with method with generic parameters, find the original method.
specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
// call findCacheOperations (implemented by the subclass AnnotationCacheOperationSource), which is eventually parsed by the SpringCacheAnnotationParser
// First try is the method in the target class.
Collection<CacheOperation> opDef = findCacheOperations(specificMethod);
if (opDef != null) {
return opDef;
}
// Second try is the caching operation on the target class.
opDef = findCacheOperations(specificMethod.getDeclaringClass());
if (opDef != null && ClassUtils.isUserLevelMethod(method)) {
return opDef;
}
if (specificMethod != method) {
// Fallback is to look at the original method.
opDef = findCacheOperations(method);
if (opDef != null) {
return opDef;
}
// Last fallback is the class of the original method.
opDef = findCacheOperations(method.getDeclaringClass());
if (opDef != null && ClassUtils.isUserLevelMethod(method)) {
return opDef;
}
}
return null;
}
|
2. Logic execution
Take the logic behind @Cacheable
as an example. The expectation is to check the cache first and use the cache value directly if the cache hits, otherwise execute the business logic and write the result to the cache.
ProxyCachingConfiguration
is a configuration class that generates a Spring bean for the CacheInterceptor
class and the CacheOperationSource
class.
CacheInterceptor
is an AOP method interceptor that gets the CacheOperation result of step 1 parsing annotations (e.g. cache name, cache key, condition condition) through CacheOperationSource
, essentially intercepting the execution of the original method and adding logic before and after.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
// Core class, cache interceptor
public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor, Serializable {
// Intercept the execution of the original method and add logic before and after
@Override
public Object invoke(final MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
// Encapsulate the execution of the original method into a callback interface for subsequent calls
CacheOperationInvoker aopAllianceInvoker = new CacheOperationInvoker() {
@Override
public Object invoke() {
try {
// Execution of the original method
return invocation.proceed();
}
catch (Throwable ex) {
throw new ThrowableWrapper(ex);
}
}
};
try {
// Call the methods of the parent class CacheAspectSupport
return execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments());
}
catch (CacheOperationInvoker.ThrowableWrapper th) {
throw th.getOriginal();
}
}
}
|
CacheAspectSupport
CacheAspectSupport class, the parent class of CacheInterceptor, encapsulates the main logic of all cache operations.
The main process is as follows.
- through the
CacheOperationSource
, get all the CacheOperation
list.
- if there is
@CacheEvict
annotation, and marked as executed before the call, then do the delete/empty cache operation.
- if
@Cacheable
annotation is present, query the cache.
- if the cache is not hit (the query result is null), add to cachePutRequests and write to the cache after subsequent execution of the original method.
- If the cache hits, the cache value is used as the result; if the cache does not hit, or if there is a
@CachePut
annotation, the original method needs to be called and the return value of the original method is used as the result.
- if there is
@CachePut
annotation, then add to cachePutRequests.
- if the cache is not hit, the query result value is written to the cache; if there is
@CachePut
annotation, the method execution result is also written to the cache.
- if there is
@CacheEvict
annotation and it is marked as executed after the call, then do the delete/empty cache operation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
|
// The core class, the cache cut support class, encapsulates the main logic of all cache operations
public abstract class CacheAspectSupport extends AbstractCacheInvoker
implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton {
// CacheInterceptor calls this method of the parent class
protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {
// Check whether aspect is enabled (to cope with cases where the AJ is pulled in automatically)
if (this.initialized) {
Class<?> targetClass = getTargetClass(target);
// Get a list of all CacheOperations via CacheOperationSource
Collection<CacheOperation> operations = getCacheOperationSource().getCacheOperations(method, targetClass);
if (!CollectionUtils.isEmpty(operations)) {
// Continue to call a private execute method to execute
return execute(invoker, method, new CacheOperationContexts(operations, method, args, target, targetClass));
}
}
// If the spring bean is not initialized, the original method is called directly. This is equivalent to the original method not having caching capabilities.
return invoker.invoke();
}
private的execute方法
private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
// Special handling of synchronized invocation
if (contexts.isSynchronized()) {
CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
Cache cache = context.getCaches().iterator().next();
try {
return wrapCacheValue(method, cache.get(key, new Callable<Object>() {
@Override
public Object call() throws Exception {
return unwrapReturnValue(invokeOperation(invoker));
}
}));
}
catch (Cache.ValueRetrievalException ex) {
// The invoker wraps any Throwable in a ThrowableWrapper instance so we
// can just make sure that one bubbles up the stack.
throw (CacheOperationInvoker.ThrowableWrapper) ex.getCause();
}
}
else {
// No caching required, only call the underlying method
return invokeOperation(invoker);
}
}
// If there is a @CacheEvict annotation and it is marked as executed before the call, then do the delete/empty cache operation
// Process any early evictions
processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
CacheOperationExpressionEvaluator.NO_RESULT);
// If the @Cacheable annotation is present, the query cache
// Check if we have a cached item matching the conditions
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
// If the cache is not hit (the query result is null), it will be added to cachePutRequests, and the subsequent execution of the original method will be written to the cache
// Collect puts from any @Cacheable miss, if no cached item is found
List<CachePutRequest> cachePutRequests = new LinkedList<CachePutRequest>();
if (cacheHit == null) {
collectPutRequests(contexts.get(CacheableOperation.class),
CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
}
Object cacheValue;
Object returnValue;
if (cacheHit != null && cachePutRequests.isEmpty() && !hasCachePut(contexts)) {
// For cache hits, use the cache value as the result
// If there are no put requests, just use the cache hit
cacheValue = cacheHit.get();
returnValue = wrapCacheValue(method, cacheValue);
}
else {
// If the cache is not hit, or if there is a @CachePut annotation, the original method needs to be called
// Invoke the method if we don't have a cache hit
// Call the original method and get the result value
returnValue = invokeOperation(invoker);
cacheValue = unwrapReturnValue(returnValue);
}
// If there is a @CachePut annotation, then add to cachePutRequests
// Collect any explicit @CachePuts
collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
// If the cache is not hit, the query result value is written to the cache; if the @CachePut annotation is present, the method execution result is also written to the cache
// Process any collected put requests, either from @CachePut or a @Cacheable miss
for (CachePutRequest cachePutRequest : cachePutRequests) {
cachePutRequest.apply(cacheValue);
}
// If there is a @CacheEvict annotation and it is marked as executed after the call, then do the delete/empty cache operation
// Process any late evictions
processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
return returnValue;
}
private Cache.ValueWrapper findCachedItem(Collection<CacheOperationContext> contexts) {
Object result = CacheOperationExpressionEvaluator.NO_RESULT;
for (CacheOperationContext context : contexts) {
// Query the cache only if the condition is met
if (isConditionPassing(context, result)) {
// Generate the cache key, if the key is specified in the annotation, it is parsed according to the Spring expression, otherwise it is generated using the KeyGenerator class
Object key = generateKey(context, result);
// Query the cache value according to the cache key
Cache.ValueWrapper cached = findInCaches(context, key);
if (cached != null) {
return cached;
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No cache entry for key '" + key + "' in cache(s) " + context.getCacheNames());
}
}
}
}
return null;
}
private Cache.ValueWrapper findInCaches(CacheOperationContext context, Object key) {
for (Cache cache : context.getCaches()) {
// Call the doGet method of the parent class AbstractCacheInvoker to query the cache
Cache.ValueWrapper wrapper = doGet(cache, key);
if (wrapper != null) {
if (logger.isTraceEnabled()) {
logger.trace("Cache entry for key '" + key + "' found in cache '" + cache.getName() + "'");
}
return wrapper;
}
}
return null;
}
|
AbstractCacheInvoker
Parent class of CacheAspectSupport, encapsulating the logic of the final query Cache interface.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public abstract class AbstractCacheInvoker {
// The final query cache method
protected Cache.ValueWrapper doGet(Cache cache, Object key) {
try {
// Calling the query method of the Spring Cache interface
return cache.get(key);
}
catch (RuntimeException ex) {
getErrorHandler().handleCacheGetError(ex, cache, key);
return null; // If the exception is handled, return a cache miss
}
}
}
|