The introduction
This article will only discuss how spring-cache is implemented in SpringBoot. There are many ways to use it on the web, but I will not describe it in detail here. These classes need to be covered in this article, so list them first.
- @enablecaching: indicates the cache entry. It is used to enable spring-cache
- What kind of AbstractCachingConfiguration CachingConfigurationSelector choose to use
- ProxyCachingConfiguration annotation-based cache configuration
- CachingConfigurerSupport: Cache-configurerSupport classes that require project inheritance using Spring Cache and can only have one associated bean per project.
- AbstractCachingConfiguration: abstract cache configuration, used to initialize the configuration. Load cacheManager, cacheResolver, and keyGenerator
- AbstractCachingConfiguration ProxyCachingConfiguration: inheritance, the default caching proxy configuration, used to configure the interceptor.
- CacheInterceptor A caching aspect interceptor
- CacheAspectSupport Is the base class for caching aspects, such as CacheInterceptor or AspectJ aspects. This makes the underlying Spring caching infrastructure easy to use to implement aspects for any aspect system
- The Cache abstraction defines the operation behavior of the Cache
- CacheManager CacheManager: obtains the cache interface
1: What did the boot do
- Add @enablecaching. This annotation will @Import one
CachingConfigurationSelector
Class.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(CachingConfigurationSelector.class)
public @interface EnableCaching {
}
Copy the code
- while
CachingConfigurationSelector
theselectImports
The AdviceMode attribute in the annotation determines whether to go PROXY or ASPECTJ
@Override
public String[] selectImports(AdviceMode adviceMode) {
switch (adviceMode) {
// PROXY is used by default
case PROXY:
return getProxyImports();
case ASPECTJ:
return getAspectJImports();
default:
return null; }}Copy the code
selectImports
Method returns the names of two more classes, which will be loaded here
private String[] getProxyImports() {
List<String> result = new ArrayList<>(3);
result.add(AutoProxyRegistrar.class.getName());
result.add(ProxyCachingConfiguration.class.getName());
if (jsr107Present && jcacheImplPresent) {
result.add(PROXY_JCACHE_CONFIGURATION_CLASS);
}
return StringUtils.toStringArray(result);
}
Copy the code
- In the first
AutoProxyRegistrar
theregisterBeanDefinitions
What does the method do
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
boolean candidateFound = false;
Set<String> annTypes = importingClassMetadata.getAnnotationTypes();
for (String annType : annTypes) {
AnnotationAttributes candidate = AnnotationConfigUtils.attributesFor(importingClassMetadata, annType);
if (candidate == null) {
continue;
}
Object mode = candidate.get("mode");
Object proxyTargetClass = candidate.get("proxyTargetClass");
if(mode ! =null&& proxyTargetClass ! =null && AdviceMode.class == mode.getClass() &&
Boolean.class == proxyTargetClass.getClass()) {
candidateFound = true;
if (mode == AdviceMode.PROXY) {
/ / if the mode of mode is the PROXY to sign up for a container automatic PROXY creator, here is InfrastructureAdvisorAutoProxyCreator implementation
AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
if ((Boolean) proxyTargetClass) {
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
return; }}}}if(! candidateFound && logger.isInfoEnabled()) { String name = getClass().getSimpleName(); logger.info(String.format("%s was imported but no annotations were found " +
"having both 'mode' and 'proxyTargetClass' attributes of type " +
"AdviceMode and boolean respectively. This means that auto proxy " +
"creator registration and configuration may not have occurred as " +
"intended, and components may not be proxied as expected. Check to " +
"ensure that %s has been @Import'ed on the same class where these " +
"annotations are declared; otherwise remove the import of %s " +
"altogether.", name, name, name)); }}Copy the code
@Nullable
public static BeanDefinition registerAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry) {
return registerAutoProxyCreatorIfNecessary(registry, null);
}
@Nullable
public static BeanDefinition registerAutoProxyCreatorIfNecessary(
BeanDefinitionRegistry registry, @Nullable Object source) {
return registerOrEscalateApcAsRequired(InfrastructureAdvisorAutoProxyCreator.class, registry, source);
}
Copy the code
Can be seen that creates a BeanDefinition InfrastructureAdvisorAutoProxyCreator type It is a AbstractAdvisorAutoProxyCreator, scan all default Advisor (section) of the class, can determine whether the agent, the proxy class generation agent.
- Look at the
ProxyCachingConfiguration
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyCachingConfiguration extends AbstractCachingConfiguration {
@Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor( CacheOperationSource cacheOperationSource, CacheInterceptor cacheInterceptor) {
BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();
advisor.setCacheOperationSource(cacheOperationSource);
advisor.setAdvice(cacheInterceptor);
if (this.enableCaching ! =null) {
advisor.setOrder(this.enableCaching.<Integer>getNumber("order"));
}
return advisor;
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public CacheOperationSource cacheOperationSource(a) {
return new AnnotationCacheOperationSource();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public CacheInterceptor cacheInterceptor(CacheOperationSource cacheOperationSource) {
CacheInterceptor interceptor = new CacheInterceptor();
interceptor.configure(this.errorHandler, this.keyGenerator, this.cacheResolver, this.cacheManager);
interceptor.setCacheOperationSource(cacheOperationSource);
returninterceptor; }}Copy the code
As you can see here to instantiate the three beans were BeanFactoryCacheOperationSourceAdvisor, CacheOperationSource, CacheInterceptor.
Say BeanFactoryCacheOperationSourceAdvisor first, It is inherited from AbstractBeanFactoryPointcutAdvisor, is used in each bean initialization time (each bean will be loaded into advised object – > targetSource and Advisor [] Array), each bean is called when the method is first traversed advisor method, and then call the native bean(targetSource) method, aop effect is realized;
Bean when loading BeanFactoryCacheOperationSourceAdvisor getPointcut () will be available, namely CacheOperationSourcePointcut Then call CacheOperationSourcePointcut. Matches () method, used to match the corresponding bean, Assuming that bean in BeanFactoryCacheOperationSourceAdvisor scanning matchs () method returns true, The result is that the invoke() method in the CacheInterceptor is called when each bean’s method is called
CacheOperationSource again is an interface, the implementation class is AnnotationCacheOperationSource, parent class is AbstractFallbackCacheOperationSource
The parent class AbstractFallbackCacheOperationSource getCacheOperations method
@Override
@Nullable
public Collection<CacheOperation> getCacheOperations(Method method, @NullableClass<? > targetClass) {
if (method.getDeclaringClass() == Object.class) {
return null;
}
Object cacheKey = getCacheKey(method, targetClass);
Collection<CacheOperation> cached = this.attributeCache.get(cacheKey);
if(cached ! =null) {
return(cached ! = NULL_CACHING_ATTRIBUTE ? cached :null);
}
else {
// Look for annotations on target methods, such as @cachable @cacheput
Collection<CacheOperation> cacheOps = computeCacheOperations(method, targetClass);
if(cacheOps ! =null) {
if (logger.isTraceEnabled()) {
logger.trace("Adding cacheable method '" + method.getName() + "' with attribute: " + cacheOps);
}
this.attributeCache.put(cacheKey, cacheOps);
}
else {
this.attributeCache.put(cacheKey, NULL_CACHING_ATTRIBUTE);
}
returncacheOps; }}Copy the code
This is essentially an encapsulation of attributeCache, Invocation method-class by invocation method-CacheOperation and then invocation method-class from cacheInterceptor.invoke () CacheOperationSource. GetCacheOperations () get to CacheOperation, CacheOperation is actually trigger corresponding spring – cache annotation operation – for the realization of the cache
Finally AnnotationCacheOperationSource, its findCacheOperations () method, which is used to resolve whether to intercept the method (such as @ Cachable @ CachePut) annotations mark method
Start the parsing section and that’s it. Below is the process executed in a method annotated like @cachable
2: Execute the process
- Methods annotated as @cachable come in
CacheInterceptor.invoke()
methods
public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor.Serializable {
@Override
@Nullable
public Object invoke(final MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
CacheOperationInvoker aopAllianceInvoker = () -> {
try {
return invocation.proceed();
}
catch (Throwable ex) {
throw new CacheOperationInvoker.ThrowableWrapper(ex);
}
};
Object target = invocation.getThis();
Assert.state(target != null."Target must not be null");
try {
return execute(aopAllianceInvoker, target, method, invocation.getArguments());
}
catch (CacheOperationInvoker.ThrowableWrapper th) {
throwth.getOriginal(); }}}Copy the code
- The parent class is then called
CacheAspectSupport.execute()
methods
@Nullable
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); CacheOperationSource cacheOperationSource = getCacheOperationSource();if(cacheOperationSource ! =null) {
Class+Method
Collection<CacheOperation> operations = cacheOperationSource.getCacheOperations(method, targetClass);
if(! CollectionUtils.isEmpty(operations)) {return execute(invoker, method,
// Note that this object is created with the Cache information already identified.
newCacheOperationContexts(operations, method, args, target, targetClass)); }}}return invoker.invoke();
}
Copy the code
New CacheOperationContexts(Operations, Method, args, Target, targetClass) The resulting object will find the cache corresponding to the cacheName from the CacheManager#getCache method.
public CacheOperationContext(CacheOperationMetadata metadata, Object[] args, Object target) {
this.metadata = metadata;
this.args = extractArgs(metadata.method, args);
this.target = target;
// Go to the Cache manager to find the corresponding Cache
this.caches = CacheAspectSupport.this.getCaches(this, metadata.cacheResolver);
this.cacheNames = createCacheNames(this.caches); }...protected Collection<? extends Cache> getCaches(
CacheOperationInvocationContext<CacheOperation> context, CacheResolver cacheResolver) {
// Use CacheResolver to find the corresponding Caches
Collection<? extends Cache> caches = cacheResolver.resolveCaches(context);
if (caches.isEmpty()) {
throw new IllegalStateException("No cache could be resolved for '" +
context.getOperation() + "' using resolver '" + cacheResolver +
"'. At least one cache should be provided per cache operation.");
}
returncaches; }...@Override
publicCollection<? extends Cache> resolveCaches(CacheOperationInvocationContext<? > context) { Collection<String> cacheNames = getCacheNames(context);if (cacheNames == null) {
return Collections.emptyList();
}
Collection<Cache> result = new ArrayList<>(cacheNames.size());
for (String cacheName : cacheNames) {
// Find the Cache based on the cacheName in the CacheManager.
Cache cache = getCacheManager().getCache(cacheName);
if (cache == null) {
throw new IllegalArgumentException("Cannot find cache named '" +
cacheName + "' for " + context.getOperation());
}
result.add(cache);
}
return result;
}
Copy the code
Continue to perform
/* * This method can be thought of as handling @cacheable, @cacheput, @cacheevict @caching annotations */
@Nullable
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, handleSynchronizedGet(invoker, key, cache));
}
catch (Cache.ValueRetrievalException ex) {
// Directly propagate ThrowableWrapper from the invoker,
// or potentially also an IllegalArgumentException etc.ReflectionUtils.rethrowRuntimeException(ex.getCause()); }}else {
// No caching required, only call the underlying method
returninvokeOperation(invoker); }}// Annotated by @cacheevict with property beforeInvocation = true, which means that the Cache needs to be cleared before the invocation, the Cache doGet method or the //doClear method is called
processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
CacheOperationExpressionEvaluator.NO_RESULT);
// Try to fetch data from the Cache by calling the Cache doGet method
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
// If the cache is not hit, build the data wrapper object that needs to be cached, ready to put the data into the cache after the real method is executed
List<CachePutRequest> cachePutRequests = new ArrayList<>();
if (cacheHit == null) {
// If marked by @cacheable, the result will need to be cached and collected
collectPutRequests(contexts.get(CacheableOperation.class),
CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
}
Object cacheValue;
Object returnValue;
// The cache is hit, and no result information needs to be cached
if(cacheHit ! =null && !hasCachePut(contexts)) {
// If there are no put requests, just use the cache hit
cacheValue = cacheHit.get();
returnValue = wrapCacheValue(method, cacheValue);
}
else {
// The target method is executed if the cache is not hit
returnValue = invokeOperation(invoker);
cacheValue = unwrapReturnValue(returnValue);
}
// If marked by @cacheput, it needs to be cached and collected
collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
// If there is data that needs to be cached, it is cached and the doPut method of Cache is called
for (CachePutRequest cachePutRequest : cachePutRequests) {
cachePutRequest.apply(cacheValue);
}
// Annotated by @cacheevict with the property beforeInvocation = false, clear the cache after the method invocation
processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
return returnValue;
}
Copy the code
-
The above doGet, doPut operations such as defined in the org. Springframework. Cache# Cache interface, there are a lot of implementation, such as ConcurrentMapCache EhCacheCache, RedisCache and so on. Each of these caches has its own custom Cache implementation.
-
What is the underlying implementation of annotated caching (@cacheable, @cacheput, @cacheevict)? Take RedisCache for example
public class RedisCache extends AbstractValueAdaptingCache {
/* * The @cacheable annotation executes this method to retrieve data from the cache. */
@Override
@Nullable
public ValueWrapper get(Object key) {
return toValueWrapper(lookup(key));
}
/ * * in the cache, corresponding @ CachePut or @ Cacheable annotation * @ see org. Springframework. Cache. Cache# put (java.lang.object, Java. Lang. Object) * /
@Override
public void put(Object key, @Nullable Object value) {
Object cacheValue = preProcessCacheValue(value);
if(! isAllowNullValues() && cacheValue ==null) {
throw new IllegalArgumentException(String.format(
"Cache '%s' does not allow 'null' values. Avoid storing null via '@Cacheable(unless=\"#result == null\")' or configure RedisCache to allow 'null' via RedisCacheConfiguration.", name)); } cacheWriter.put(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), cacheConfig.getTtl()); }}/ * * clear the cache corresponding @ CacheEvict * @ see org. Springframework. Cache. Cache# evict (java.lang.object) * /
@Override
public void evict(Object key) {
cacheWriter.remove(name, createAndConvertCacheKey(key));
}
Copy the code