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

spring cache class

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.

Source code analysis (with comments explained)

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.

  1. through the CacheOperationSource, get all the CacheOperation list.
  2. if there is @CacheEvict annotation, and marked as executed before the call, then do the delete/empty cache operation.
  3. if @Cacheable annotation is present, query the cache.
  4. 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.
  5. 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.
  6. if there is @CachePut annotation, then add to cachePutRequests.
  7. 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.
  8. 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
		}
	}
}