Detailed explanation of SpringCache framework loading and interception principle

  • 2021-07-22 09:53:57
  • OfStack

Official website document

Background

The project A requires an implementation of multiple data sources. For example, UserDao. getAllUserList () needs to be read from the readonly library, but UserDao. insert () needs to be inserted into the main (write) library

You need to add annotations to the method calls in the dao layer!

After understanding, we know-the interface is generated dynamically through jdk proxy (mapper interface of mybatis is generated dynamically through jdk proxy- > MapperFactoryBean. class), cannot be intercepted by aop (intercepted by annotation configuration)


//dao
  @Pointcut("@annotation(com.kaola.cs.data.common.aspect.DataSourceSelect)")
  public void dao() {
  }

Then I happened to come into contact with the project B and used the SpringCache module, but the Cache module of Spring was able to intercept it (spring-cache was also intercepted by annotations! ! ! )

Aroused my interest, I turned over the source code once

Use of SpringCache

Compared with mybatis

1. spring-cache is based on the method level of spring, which means that you don't care what your method does, it is only responsible for caching the method results

The cache of mybatis (CachingExecutor/BaseExecutor) is a cache based on database query results

2. spring-cache can be configured with various types of cache media (redis, ehcache, hashmap, even db, and so on)- > It only provides interface and default implementation, and can be extended by itself

The cache of mybatis is hashmap, single 1! ! lowb

Configuration of SpringCache

1. Notes (spring-boot) 2. xml configuration

Here we only talk about annotations, but the initialized classes are all 1! ! !

Defining CacheConfigure. java can be used directly


@EnableCaching
@Configuration
public class CacheConfigure extends CachingConfigurerSupport {
  @Override
  @Bean
  public CacheManager cacheManager() {
    SimpleCacheManager result = new SimpleCacheManager();
    List<Cache> caches = new ArrayList<>();
    caches.add(new ConcurrentMapCache("testCache"));
    result.setCaches(caches);
    return result;
  }

  @Override
  @Bean
  public CacheErrorHandler errorHandler() {
    return new SimpleCacheErrorHandler();
  }

}

The core classes initialized by Spring-Cache can be found through the @ EnableCaching annotation

ProxyCachingConfiguration.java


@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyCachingConfiguration extends AbstractCachingConfiguration {

 @Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)
 @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
 public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor() {
 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() {
 return new AnnotationCacheOperationSource();
 }

 @Bean
 @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
 public CacheInterceptor cacheInterceptor() {
 CacheInterceptor interceptor = new CacheInterceptor();
 interceptor.configure(this.errorHandler, this.keyGenerator, this.cacheResolver, this.cacheManager);
 interceptor.setCacheOperationSource(cacheOperationSource());
 return interceptor;
 }

}

Instantiate the bean of three classes through annotations: BeanFactoryCacheOperationSourceAdvisor, CacheOperationSource, CacheInterceptor
Say the role of these three classes under 1

BeanFactoryCacheOperationSourceAdvisor.java


/*
 BeanFactoryCacheOperationSourceAdvisor  Inherit  AbstractBeanFactoryPointcutAdvisor
  In spring  The effect in is that in every bean Initialization of  ( Each bean Will be loaded into  advised  Object  ->  Have  targetSource  And  Advisor[]  Array )
  Each bean When the method is called, it is traversed first advisor Method, and then call the native bean( That is targetSource) The method of, implements the aop The effect of 

 bean  When loading  BeanFactoryCacheOperationSourceAdvisor  Adj.  getPointcut()->  That is  CacheOperationSourcePointcut  Will be obtained, and then call the  
 CacheOperationSourcePointcut.matches() Method ,  Used to match the corresponding bean 
  Hypothesis bean  In  BeanFactoryCacheOperationSourceAdvisor  In the scan of  matchs()  Method returns the true
  The result is that  
   In every bean When the method of is called  CacheInterceptor  In  invoke()  Method is called  

  Summary: 
  spring-cache  It's also finished aop1 Sample implementation (spring-aop It's the same thing )

  The point is that  CacheOperationSourcePointcut.matchs()  Method, how to match the interface   Let's not talk about the specific introduction here! ! ! ! 

*/
public class BeanFactoryCacheOperationSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor {

 @Nullable
 private CacheOperationSource cacheOperationSource;

 private final CacheOperationSourcePointcut pointcut = new CacheOperationSourcePointcut() {
 @Override
 @Nullable
 protected CacheOperationSource getCacheOperationSource() {
  return cacheOperationSource;
 }
 };


 /**
 * Set the cache operation attribute source which is used to find cache
 * attributes. This should usually be identical to the source reference
 * set on the cache interceptor itself.
 */
 public void setCacheOperationSource(CacheOperationSource cacheOperationSource) {
 this.cacheOperationSource = cacheOperationSource;
 }

 /**
 * Set the {@link ClassFilter} to use for this pointcut.
 * Default is {@link ClassFilter#TRUE}.
 */
 public void setClassFilter(ClassFilter classFilter) {
 this.pointcut.setClassFilter(classFilter);
 }

 @Override
 public Pointcut getPointcut() {
 return this.pointcut;
 }

}

CacheOperationSource. java is an interface

The implementation class is- > AnnotationCacheOperationSource. java focuses on the parent class-- > AbstractFallbackCacheOperationSource.java

Explain 1:

The amount of code is very small, mainly the encapsulation of attributeCache, which is used by combining method-CacheOperation

Then get method-class through invocation at CacheInterceptor. invoke () and then call CacheOperationSource. getCacheOperations () to get CacheOperation
CacheOperation actually triggers the operation corresponding to spring-cache annotation-the implementation of obtaining cache


public abstract class AbstractFallbackCacheOperationSource implements CacheOperationSource {

 /**
 * Canonical value held in cache to indicate no caching attribute was
 * found for this method and we don't need to look again.
 */
 private static final Collection<CacheOperation> NULL_CACHING_ATTRIBUTE = Collections.emptyList();


 /**
 * Logger available to subclasses.
 * <p>As this base class is not marked Serializable, the logger will be recreated
 * after serialization - provided that the concrete subclass is Serializable.
 */
 protected final Log logger = LogFactory.getLog(getClass());

 /**
 * 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<>(1024);


 /**
 * Determine the caching attribute for this method invocation.
 * <p>Defaults to the class's caching attribute if no method attribute is found.
 * @param method the method for the current invocation (never {@code null})
 * @param targetClass the target class for this invocation (may be {@code null})
 * @return {@link CacheOperation} for this method, or {@code null} if the method
 * is not cacheable
 */
 @Override
 @Nullable
 public Collection<CacheOperation> getCacheOperations(Method method, @Nullable Class<?> 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 {
  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);
  }
  return cacheOps;
 }
 }

 /**
 * Determine a cache key for the given method and target class.
 * <p>Must not produce same key for overloaded methods.
 * Must produce same key for different instances of the same method.
 * @param method the method (never {@code null})
 * @param targetClass the target class (may be {@code null})
 * @return the cache key (never {@code null})
 */
 protected Object getCacheKey(Method method, @Nullable Class<?> targetClass) {
 return new MethodClassKey(method, targetClass);
 }

 @Nullable
 private Collection<CacheOperation> computeCacheOperations(Method method, @Nullable 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 = AopUtils.getMostSpecificMethod(method, targetClass);

 // 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;
 }


 /**
 * Subclasses need to implement this to return the caching attribute for the
 * given class, if any.
 * @param clazz the class to retrieve the attribute for
 * @return all caching attribute associated with this class, or {@code null} if none
 */
 @Nullable
 protected abstract Collection<CacheOperation> findCacheOperations(Class<?> clazz);

 /**
 * Subclasses need to implement this to return the caching attribute for the
 * given method, if any.
 * @param method the method to retrieve the attribute for
 * @return all caching attribute associated with this method, or {@code null} if none
 */
 @Nullable
 protected abstract Collection<CacheOperation> findCacheOperations(Method method);

 /**
 * Should only public methods be allowed to have caching semantics?
 * <p>The default implementation returns {@code false}.
 */
 protected boolean allowPublicMethodsOnly() {
 return false;
 }

}

! ! ! ! matchs () method of CacheOperationSourcePointcut. java

It is used to determine whether the class meets the spring-cache interception conditions, that is, how to identify the annotations of @ Cachable @ CachePut and so on

The tracing code found that it was called by AnnotationCacheOperationSource. findCacheOperations ()

Omit part of the code....


public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperationSource implements Serializable {


 private final Set<CacheAnnotationParser> annotationParsers;


 @Override
 @Nullable
 protected Collection<CacheOperation> findCacheOperations(Class<?> clazz) {
 return determineCacheOperations(parser -> parser.parseCacheAnnotations(clazz));
 }

 @Override
 @Nullable
 protected Collection<CacheOperation> findCacheOperations(Method method) {
 return determineCacheOperations(parser -> parser.parseCacheAnnotations(method));
 }

 /**
 * Determine the cache operation(s) for the given {@link CacheOperationProvider}.
 * <p>This implementation delegates to configured
 * {@link CacheAnnotationParser CacheAnnotationParsers}
 * for parsing known annotations into Spring's metadata attribute class.
 * <p>Can be overridden to support custom annotations that carry caching metadata.
 * @param provider the cache operation provider to use
 * @return the configured caching operations, or {@code null} if none found
 */
 @Nullable
 protected Collection<CacheOperation> determineCacheOperations(CacheOperationProvider provider) {
 Collection<CacheOperation> ops = null;
 for (CacheAnnotationParser annotationParser : this.annotationParsers) {
  Collection<CacheOperation> annOps = provider.getCacheOperations(annotationParser);
  if (annOps != null) {
  if (ops == null) {
   ops = annOps;
  }
  else {
   Collection<CacheOperation> combined = new ArrayList<>(ops.size() + annOps.size());
   combined.addAll(ops);
   combined.addAll(annOps);
   ops = combined;
  }
  }
 }
 return ops;
 }
}

Then there is the annotation parsing method SpringCacheAnnotationParser. java

The code is very simple.-I won't say much


@Nullable
 private Collection<CacheOperation> parseCacheAnnotations(
  DefaultCacheConfig cachingConfig, AnnotatedElement ae, boolean localOnly) {

 Collection<? extends Annotation> anns = (localOnly ?
  AnnotatedElementUtils.getAllMergedAnnotations(ae, CACHE_OPERATION_ANNOTATIONS) :
  AnnotatedElementUtils.findAllMergedAnnotations(ae, CACHE_OPERATION_ANNOTATIONS));
 if (anns.isEmpty()) {
  return null;
 }

 final Collection<CacheOperation> ops = new ArrayList<>(1);
 anns.stream().filter(ann -> ann instanceof Cacheable).forEach(
  ann -> ops.add(parseCacheableAnnotation(ae, cachingConfig, (Cacheable) ann)));
 anns.stream().filter(ann -> ann instanceof CacheEvict).forEach(
  ann -> ops.add(parseEvictAnnotation(ae, cachingConfig, (CacheEvict) ann)));
 anns.stream().filter(ann -> ann instanceof CachePut).forEach(
  ann -> ops.add(parsePutAnnotation(ae, cachingConfig, (CachePut) ann)));
 anns.stream().filter(ann -> ann instanceof Caching).forEach(
  ann -> parseCachingAnnotation(ae, cachingConfig, (Caching) ann, ops));
 return ops;
 }
 

Summarize

1. spring-cache realizes that AbstractBeanFactoryPointcutAdvisor provides CacheOperationSourcePointcut (PointCut) for tangent point judgment and CacheInterceptor (MethodInterceptor) for method interception

2. spring-cache provides CacheOperationSource as the query and load of method corresponding to CacheOperation (cache operation)

3. spring-cache uses SpringCacheAnnotationParser to resolve self-defined annotation classes such as @ Cacheable @ CacheEvict @ Caching
Therefore, spring-cache does not use aspectj, and CacheOperationSource. getCacheOperations () can make the class of jdk proxy match

Matching of classes for jdk proxies

Code classes in CacheOperationSource. getCacheOperations ()

Focus on targetClass and method. If it is the corresponding dao. xxx (), it can matchs () and intercept

CacheInterceptor - > CacheAspectSupport. execute () Method


//  See the code for yourself. It's also very simple  ->  The result is that spring-cache  It can also be intercepted mybatis Adj. dao Layer interface for caching 

 @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) {
  Collection<CacheOperation> operations = cacheOperationSource.getCacheOperations(method, targetClass);
  if (!CollectionUtils.isEmpty(operations)) {
   return execute(invoker, method,
    new CacheOperationContexts(operations, method, args, target, targetClass));
  }
  }
 }

 return invoker.invoke();
 }

Related articles: