SpringCache框架載入/攔截原理

莊傑森發表於2019-04-27

SpringCache框架載入/攔截原理

我github部落格地址

官網文件

docs.spring.io/spring/docs…

背景

專案A中需要多資料來源的實現,比如UserDao.getAllUserList() 需要從readonly庫中讀取,但是UserDao.insert() 需要插入主(寫)庫 
就需要在dao層的方法呼叫上面新增註解!

複製程式碼

瞭解後知道-介面通過jdk代理(mybatis的mapper介面就是通過jdk代理動態生成的-> MapperFactoryBean.class )的,沒辦法被aop的攔截(註解配置的攔截)

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

複製程式碼

然後碰巧接觸了專案B,使用了SpringCache模組,但是Spring的Cache模組居然能夠攔截(spring-cache也是通過註解攔截!!!)

引起了我的興趣,就把原始碼翻了一遍

SpringCache的用途

與 mybatis 對比
	1.  spring-cache 是基於spring的方法級別的,也就是說你方法做了啥不關心,它只負責快取方法結果
		mybatis 的快取(CachingExecutor / BaseExecutor) 是基於資料庫查詢結果的快取
	2.  spring-cache 可以配置各種型別的快取介質(redis , ehcache , hashmap, 甚至db等等) -> 它僅僅是提供介面和預設實現,可以自己擴充
		mybatis 的快取是hashmap,單一!!lowb

複製程式碼

SpringCache 的配置

1.註解(spring-boot) 2.xml配置

這裡只講註解,但是初始化的類都是一樣的!!!

定義 CacheConfigure.java 就能直接使用

@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();
    }

}


複製程式碼

通過 @EnableCaching 註解可以找到 Spring-Cache 初始化的核心類

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

}

複製程式碼

通過註解,把3個類的bean 例項化: BeanFactoryCacheOperationSourceAdvisor 、CacheOperationSource 、 CacheInterceptor

說一下這3個類的作用

BeanFactoryCacheOperationSourceAdvisor.java

/*
  BeanFactoryCacheOperationSourceAdvisor 繼承了 	AbstractBeanFactoryPointcutAdvisor
  在spring 中的效果就是,在每個bean的初始化時 (每個bean都會被載入成 advised 物件 -> 有 targetSource 和 Advisor[] 陣列)
  每個bean被呼叫方法的時候都是先遍歷advisor的方法,然後在呼叫原生bean(也就是targetSource)的方法,實現了aop的效果

  bean 載入的時候 BeanFactoryCacheOperationSourceAdvisor 的 getPointcut()-> 也就是 CacheOperationSourcePointcut 就會被獲取,然後呼叫 
  CacheOperationSourcePointcut.matches()方法, 用來匹配對應的bean 
  假設bean 在 BeanFactoryCacheOperationSourceAdvisor 的掃描中 matchs() 方法返回了true
  結果就是 
  	在每個bean的方法被呼叫的時候 CacheInterceptor 中的 invoke() 方法就會被呼叫 

  總結:
  	spring-cache 也完成了aop一樣的實現(spring-aop也是這樣做的)

  重點就是在 CacheOperationSourcePointcut.matchs() 方法中,怎麼匹配介面的了 這裡先不說後面具體介紹!!!!

*/
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 是個介面

實現類是 -> AnnotationCacheOperationSource.java 重點是父類 -> AbstractFallbackCacheOperationSource.java

講解一下:

程式碼量很少,主要是 attributeCache 的封裝使用,通過把 method - CacheOperation 
然後在 CacheInterceptor.invoke() 的時候通過invocation 獲取到 method-class 然後呼叫 CacheOperationSource.getCacheOperations() 獲取到 CacheOperation
CacheOperation 其實就是觸發對應spring-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;
	}

}



複製程式碼

!!!!  CacheOperationSourcePointcut.java 的 matchs() 方法

用來判斷類是不是符合spring-cache 攔截條件 也就是 @Cachable @CachePut 等等的註解怎麼識別的地方

經過跟蹤程式碼發現是 AnnotationCacheOperationSource.findCacheOperations() 呼叫的

省略部分程式碼....


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


}



複製程式碼

然後就是註解的解析方法 SpringCacheAnnotationParser.java

程式碼很簡單-就不多說了



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


複製程式碼

總結

1.spring-cache 實現了 AbstractBeanFactoryPointcutAdvisor 提供 CacheOperationSourcePointcut (PointCut) 作切點判斷,提供 CacheInterceptor (MethodInterceptor) 作方法攔截
2.spring-cache 提供 CacheOperationSource 作為 method 對應 CacheOperation(快取操作) 的查詢和載入
3.spring-cache 通過 SpringCacheAnnotationParser 來解析自己定義的 @Cacheable @CacheEvict @Caching 等註解類
所以 spring-cache 不使用 aspectj 的方式,通過 CacheOperationSource.getCacheOperations() 方式可以使jdk代理的類也能匹配到
複製程式碼

jdk代理的類的匹配

程式碼類在 CacheOperationSource.getCacheOperations()

重點在於 targetClass 和 method ,如果是對應的 dao.xxx() 就能matchs() 並且攔截

CacheInterceptor -> CacheAspectSupport.execute() 方法

// 程式碼自己看吧。也很簡單 -> 結果就是spring-cache 也可以攔截到mybatis的dao層介面,進行快取

	@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();
	}



複製程式碼

相關文章