Spring之Cache——Spring缓存架构解读

Spring之Cache——Spring缓存架构解读

  • Spring之Cache架构
    • 缓存架构的概述
    • 缓存架构的实现
    • 缓存架构的操作原理
      Spring之Cache——Spring缓存架构解读

Spring Cache

概述
接下来将对Spring的缓存模块进行解读,主要内容分为:缓存的介绍、缓存的实现、两个部分。由于应用部分在网上的教程比较多,例如采用redis等,这里就不再重复了,只讲解Cache架构。

一.缓存架构的概述

  • 作用与功能
  • 拓展性

1.作用与功能

Spring中有一个高速缓存架构,名为Cache,顾名思义,即为一些数据查询等提供高速的数据缓存,从而提高响应速度。

2.拓展性

Cache架构,并没有指定具体的缓存中间件,因此,有着极佳的拓展性。我们可以根据实际业务选择Redis、Memcached等内存数据库充当缓存中间件。

二.缓存架构的实现

  • 缓存架构的位置
  • 架构源码的解读

1.缓存架构的位置

如果您读过Spring的源码,应该知道Spring的架构拓展主要位于spring - context项目。无一例外,缓存的架构拓展就是处于Spring-context项目。
以下是缓存架构的包结构图:
Spring之Cache——Spring缓存架构解读
可以看到,该缓存架构分为annotationconcurrentconfiginterceptorsupport五大包。

2.架构源码的解读

  • 架构模块介绍
    • annotation注解包
    • concurrent并发包
    • config配置包
    • intercetor拦截器包
    • support包

1.annotation

该包的功能为提供使用缓存的注解、以及包含对注解的解析。有如下(以**解的具体应用会在第三部分讲解):

@Cacheable
@CacheEvict
@CachePut
@Caching
@EnableCaching
@CacheConfig

这里重点讲解注解的解析工作,由缓存注解解析器开始:

CacheAnnotationParser.class

翻看源码,发现该类是一个定义了两个方法的接口。

public interface CacheAnnotationParser {
/**
	 * 解析给定类的缓存定义,
	 * 基于此解析器理解的注释类型。
	 * <p>这实质上将已知的缓存注释解析为Spring的元数据
	 * 属性类。如果类不可缓存,则返回{@code null}。
	 *
	 * @param键入带注释的类
	 * @return配置的缓存操作,如果没有找到,则返回{@code null}
	 * @see AnnotationCacheOperationSource#findCacheOperations(Class)
	 */
	@Nullable
	Collection<CacheOperation> parseCacheAnnotations(Class<?> type);

	/**
	 * 解析给定方法的缓存定义,
	 * 基于此解析器理解的注释类型。
	 * <p>这实质上将已知的缓存注释解析为Spring的元数据
	 * 属性类。如果方法不可缓存,则返回{@code null}。
	 *
	 * @param方法带注释的方法
	 * @return配置的缓存操作,如果没有找到,则返回{@code null}
	 * @see AnnotationCacheOperationSource#findCacheOperations(Method)
	 */
	@Nullable
	Collection<CacheOperation> parseCacheAnnotations(Method method);

通过以上的源码,我们了解到的只有两点:

  • 定义了用于解析在类上的注解和解析在方法上注解的两个方法。
  • doc文档简要说明了两个方法各自的用处。

该接口有一个实现类:SpringCacheAnnotationParser 该类的主要功能如下:

  • 解析put、caching、evict、cacheable四大注解的策略实现(caching为其它三大注解的组合注解)
  • 提供默认的缓存配置类、验证缓存操作

接下来,围绕缓存注解的解析过程,解读整个缓存架构的实现。

	@Override
	@Nullable
	public Collection<CacheOperation> parseCacheAnnotations(Class<?> type) {
		DefaultCacheConfig defaultConfig = new DefaultCacheConfig(type);
		return parseCacheAnnotations(defaultConfig, type);
	}
	//为上述方法的实现。
	@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;
	}
	//解析CacheableAnnotation
	private CacheableOperation parseCacheableAnnotation(
			AnnotatedElement ae, DefaultCacheConfig defaultConfig, Cacheable cacheable) {
		CacheableOperation.Builder builder = new CacheableOperation.Builder();
		builder.setName(ae.toString());
		builder.setCacheNames(cacheable.cacheNames());
		builder.setCondition(cacheable.condition());
		builder.setUnless(cacheable.unless());
		builder.setKey(cacheable.key());
		builder.setKeyGenerator(cacheable.keyGenerator());
		builder.setCacheManager(cacheable.cacheManager());
		builder.setCacheResolver(cacheable.cacheResolver());
		builder.setSync(cacheable.sync());
		defaultConfig.applyDefault(builder);
		CacheableOperation op = builder.build();
		validateCacheOperation(ae, op);
		return op;
	}

通过源码,可能这里的内容看起来有点多,但是仔细阅读,会发现,可以分为:

  • 定义了解析注解的方法,并且为私有实现。
  • 通过工具类,获取所有缓存注解。并且对这些注解进行逐个解析(解析名称、缓存仓库、主键、键构造器、缓存管理等参数),基于这些参数,构造缓存基础操作,并进行验证,验证通过则返回缓存操作数的集合(包含以上那些参数信息)

我们可以猜测,该缓存操作集合就是用于保存应用缓存的具体类与方法以及每个操作对应的参数信息。
在该包中,还有一个类:AnnotationCacheOperationSource该类中有一个重要参数:boolean publicMethodsOnly,如果你用过Spring的cache架构,那么你应该知道,缓存注解应该加到public域的方法上,如果加在私有方法上,会出错。这个就是该参数的作用,该参数还用于保证加了缓存注解的方法能够被找到。
好了,annotation包的解读到此为止。

2.interceptor

上一步,在Annotation包中,尚且完成了对缓存注解的解析以及操作数的保存工作。众所周知,高速缓存层是存在数据持久化层与Web层之间的中间层,要使用缓存,就必须对访问数据持久化层的方法进行拦截,Cache架构已经完成了操作数的保存工作,接下来要做的就是对方法的调用进行拦截并调用对应的方法
本来,按照我们的猜想,在spring-context项目中的interceptor应该具有对缓存注解的拦截,但是,很奇怪,翻了半天,在该包中并没发现对注解的拦截,只是定义了各个注解对应的操作数类如:CacheEvictOperation等。于是,想起Cache架构在spring-context-support项目中还有另外的支持。
终于,在spring-context-support项目中发现jcache这个包,该包就是对缓存架构的另外一个拓展。并且发现了如下的结构:Spring之Cache——Spring缓存架构解读
终于找到注解拦截器了,以下列出一个拦截器:

class CachePutInterceptor extends AbstractKeyCacheInterceptor<CachePutOperation, CachePut> {

	public CachePutInterceptor(CacheErrorHandler errorHandler) {
		super(errorHandler);
	}

	@Override
	protected Object invoke(
			CacheOperationInvocationContext<CachePutOperation> context, CacheOperationInvoker invoker) {
		CachePutOperation operation = context.getOperation();
		CacheKeyInvocationContext<CachePut> invocationContext = createCacheKeyInvocationContext(context);
		boolean earlyPut = operation.isEarlyPut();
		Object value = invocationContext.getValueParameter().getValue();
		if (earlyPut) {
			cacheValue(context, value);
		}
		try {
			Object result = invoker.invoke();
			if (!earlyPut) {
				cacheValue(context, value);
			}
			return result;
		} catch (CacheOperationInvoker.ThrowableWrapper ex) {
			Throwable original = ex.getOriginal();
			if (!earlyPut && operation.getExceptionTypeFilter().match(original.getClass())) {
				cacheValue(context, value);
			}
			throw ex;
		}
	}
	protected void cacheValue(CacheOperationInvocationContext<CachePutOperation> context, Object value) {
		Object key = generateKey(context);
		Cache cache = resolveCache(context);
		doPut(cache, key, value);
	}
}

该拦截器拦截的是@CachePut注解,并且执行对应的doPut方法,跳进doPut的实现:

protected void doPut(Cache cache, Object key, @Nullable Object result) {
		try {
			cache.put(key, result);
		} catch (RuntimeException ex) {
			getErrorHandler().handleCachePutError(ex, cache, key, result);
		}
	}
//再跳进cache.put(key, result);很意外!!!发现并没有实现,只是Cache接口下的一个抽象方法。
void put(Object key, @Nullable Object value);

恍然大悟!Cache架构本身不就是没有具体实现,且具有极佳拓展性的架构么!原来,Cache架构中定义了Cache接口,留给如采用Redis充当缓存的组件进行实现拓展,从而实行缓存的功能。

三.缓存架构的操作原理

  • spring能通过注解操作缓存:

    • 是因为它采用天生就具有的强大的AOP机制,拦截业务方法。总体流程是:当一个方法上配置了Cacheable之类的注解后,这个方法被调用时,就会被一个叫CacheInterceptor的拦截器拦截,进入该类的invoke()方法中,如果当前context已经初始化完成,该方法紧接着会调用execute()。execute()方法中会读取原来被调用业务方法上的注解信息,通过这些信息进行相应的缓存操作,再跟据操作的结果决定是否调用原方法中的业务逻辑。这就是spring通过注解操作缓存的总体流程。
  • cacheInterceptor是在srping上下文初始化的时候,通过配置文件中的<cache:annotation-driven />标签注册到context中的。此标签的cache名称空间对应的处理类的是org.springframework.cache.config. CacheNamespaceHandler,其中对于cache:annotation-driven标签的解析类org.springframework.cache.config. AnnotationDrivenCacheBeanDefinitionParser(在CacheNamespaceHandler类的代码中可以找到),在这个类的parse()解析方法中跟据annotation-driven标签中的model属性决定是通过代理的方式实现 aop还是通过aspectj的方式进行切面拦截(默认采用proxy代理方式)。对于代理方式,会调用registerCacheAdvisor()注册缓存Advisor。在这个方法中会注入一个CacheInterceptor类型的拦截器。这就实现了对业务方法的切面拦截。

  • 在registerCacheAdvisor()方法中,还会调用一个叫parseCacheResolution()方法来注入一个缓存解析器CacheResolver,如果<cache:annotation-driven />标签中有cache-resolver的配置,就跟据配置注入一个CacheResolver,否则,就默认注入一个SimpleCacheResolver类型的实例。每个CacheResolver中包装了一个CacheManager,这个CacheManager可通过<cache:annotation-driven />标签中cacheManager之类的配置进行指定,如果没有指定,会自动注入一个叫cacheManager的bean。

  • CacheInterceptor在执行execute()的过程中会调用事先注入的CacheResolver实例的resolveCaches()方法解析业务方法中需要操作的缓存Cache列表(resolveCaches()方法内部实现是通过调用此CacheResolver实例中的cacheManager属性的getCache()方法获取Cache)。获取到需要操作的Cache列表后,遍历这个列表,然后都过调用doGet(Cache cache)或doPut(Cache cache)方法进行缓存操作。doGet()或doPut()方法在CacheInterceptor类的父类AbstractCacheInvoker中定义(注意这里的Cache列表,是spring包装了特定厂商缓存后的Cache对像,是org.springframework.cache.Cache类型的实例)。

  • 总体上说,CacheInterceptor的execute()中对缓存的操作就是通过事先注一个CacheResolver和CacheManager实例,然后通过调用这个CacheResolver实例的resolveCaches()获得需要操作的Cache列表,再遍历列表,将每个Cache实例作为参数传入doGet()或doPut()来实现缓存读取。当然,还需要一些Key之类的参数,这个是由keyGenerator自动生成的。对于keyGenerator,这里不再介绍。看看源码就很容易理清思路。

以上就是对Cache架构的解读[email protected] -林旭