Spring 3.1 版本引入基於 annotation 的 cache 技術,提供了一套抽象的快取實現方案,通過註解方式使用快取,基於配置的方式靈活使用不同快取元件。程式碼具有相當的靈活性和擴充套件性,本文基於 Spring 5.x 原始碼一起分析 Spring Cache 的程式碼藝術。
開啟 Spring Cache
想讓 Spring 提供 Cache 能力很簡單,只需要在啟動類加上 @EnableCaching 註解即可:
@Configuration
@EnableCaching
public class ServerMain {
}
通過在啟動類上新增 EnableCaching 註解將 Cache 相關的元件注入到 Spring 啟動中,通過 Proxy 或者 AspectJ 的方式獲取 Cache 對應的執行資訊。
如果你希望在啟動時修改一些 Cache 底層對應的基礎管理資訊,可以通過在啟動類上覆蓋 CachingConfigurerSupport 提供的相關方法來實現:
@EnableCaching
@SpringBootApplication
public class WebDemoApplication extends CachingConfigurerSupport {
public static void main(String[] args) {
SpringApplication.run(WebDemoApplication.class, args);
}
@Override
public CacheManager cacheManager() {
return super.cacheManager();
}
@Override
public KeyGenerator keyGenerator() {
return super.keyGenerator();
}
}
上面的示例程式碼中重寫了 CacheManager 和 KeyGenerator 兩個類的構建實現,分別實現的功能是 Cache 管理方式和 Cache key 生成方式。
從 Bean 載入機制瞭解 Spring 的 Cache 執行管理
從啟動類入手我們很容易看到整體的 Cache 管理方式:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({CachingConfigurationSelector.class})
public @interface EnableCaching {
boolean proxyTargetClass() default false;
AdviceMode mode() default AdviceMode.PROXY;
int order() default 2147483647;
}
-
proxyTargetClass:false,表示使用 JDK 代理,true 表示使用 cglib 代理。
-
mode:指定 AOP 的模式,當值為 AdviceMode.PROXY 時表示使用 Spring aop,當值為當值為AdviceMode.ASPECTJ 時,表示使用 AspectJ。
EnableCaching 使用時匯入 CachingConfigurationSelector 類,我們看看載入了什麼資訊:
public class CachingConfigurationSelector extends AdviceModeImportSelector<EnableCaching> {
public String[] selectImports(AdviceMode adviceMode) {
switch(adviceMode) {
case PROXY:
return this.getProxyImports();
case ASPECTJ:
return this.getAspectJImports();
default:
return null;
}
}
......
......
......
static {
ClassLoader classLoader = CachingConfigurationSelector.class.getClassLoader();
jsr107Present = ClassUtils.isPresent("javax.cache.Cache", classLoader);
jcacheImplPresent = ClassUtils.isPresent("org.springframework.cache.jcache.config.ProxyJCacheConfiguration", classLoader);
}
}
可以看到主要做了一件事,通過代理方式的不同載入對應的 CacheConfiguration :
- 如果是 JDK Proxy,載入 AutoProxyRegistrar 類和 ProxyCachingConfiguration 類;
- 如果是 AspectJ,則載入 AspectJCachingConfiguration 類。
Spring Boot 預設使用 JDK Proxy,我們就看 JDK Proxy 的使用。
AutoProxyRegistrar 的作用就是建立代理物件,內部通過呼叫 AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry) 方法在 IOC 容器中註冊一個 AutoProxyCreator。最終注入到容器中的 AutoProxyRegistrar 是一個 InfrastructureAdvisorAutoProxyCreator 型別:
@Nullable
public static BeanDefinition registerAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, @Nullable Object source) {
return registerOrEscalateApcAsRequired(InfrastructureAdvisorAutoProxyCreator.class, registry, source);
}
InfrastructureAdvisorAutoProxyCreator 型別只會為基礎設施型別的 Advisor 自動建立代理物件。它只會去找符合條件的 bean建立代理,從原始碼可見只有 role 為 BeanDefinition.ROLE_INFRASTRUCTURE 的滿足條件。
ProxyCachingConfiguration 建立了 3 個 bean,CacheOperationSource 關注如何獲取所有攔截的切面,
CacheInterceptor 解決要對攔截到的切面做什麼。
public class ProxyCachingConfiguration extends AbstractCachingConfiguration {
@Bean(
name = {"org.springframework.cache.config.internalCacheAdvisor"}
)
@Role(2)
public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor(CacheOperationSource cacheOperationSource, CacheInterceptor cacheInterceptor) {
BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();
advisor.setCacheOperationSource(cacheOperationSource);
advisor.setAdvice(cacheInterceptor);
if (this.enableCaching != null) {
advisor.setOrder((Integer)this.enableCaching.getNumber("order"));
}
return advisor;
}
......
}
BeanFactoryCacheOperationSourceAdvisor 是 Spring Cache 自己實現的 Advisor,會對所有能取出 CacheOperation 的方法執行 CacheInterceptor 這個 Advice。
小知識:
Spring AOP 的建立過程本質是實現一個 BeanPostProcessor,在建立 bean 的過程中建立 Proxy,並且為 Proxy 繫結所有適用於該 bean 的 advisor,最終暴露給容器。
Spring 中 AOP 幾個關鍵的概念 advisor, advice, pointcut
advice = 切面攔截中插入的行為
pointcut = 切面的切入點
advisor = advice + pointcut
BeanFactoryCacheOperationSourceAdvisor內部的切入點實現類是 CacheOperationSourcePointcut,切入的邏輯如下:
abstract class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable {
private class CacheOperationSourceClassFilter implements ClassFilter {
private CacheOperationSourceClassFilter() {
}
public boolean matches(Class<?> clazz) {
if (CacheManager.class.isAssignableFrom(clazz)) {
return false;
} else {
CacheOperationSource cas = CacheOperationSourcePointcut.this.getCacheOperationSource();
return cas == null || cas.isCandidateClass(clazz);
}
}
}
}
可以看到切入點主要就是判斷被切入的方法上是否有註解:CacheOperationSourcePointcut.this.getCacheOperationSource()。
ProxyCachingConfiguration 中還有另一個值得關注的類 - AnnotationCacheOperationSource:
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public CacheOperationSource cacheOperationSource() {
return new AnnotationCacheOperationSource();
}
CacheOperationSource 持有一個 CacheAnnotationParser 列表。 CacheAnnotationParser 只有一個實現類:SpringCacheAnnotationParser:
public class SpringCacheAnnotationParser implements CacheAnnotationParser, Serializable {
private static final Set<Class<? extends Annotation>> CACHE_OPERATION_ANNOTATIONS = new LinkedHashSet<>(8);
static {
CACHE_OPERATION_ANNOTATIONS.add(Cacheable.class);
CACHE_OPERATION_ANNOTATIONS.add(CacheEvict.class);
CACHE_OPERATION_ANNOTATIONS.add(CachePut.class);
CACHE_OPERATION_ANNOTATIONS.add(Caching.class);
}
......
@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;
}
......
}
可以看到 Parser 的作用就是將註解對應的方法解析成 CacheOperation 存起來。
看到這裡我們開始有點眉目,開始知道去哪裡找攔截了什麼和操作了什麼。接下來繼續對 CacheOperation 和 CacheInterceptor 進行深入。
CacheOperation
CacheOperationSource 介面中只有一個方法:
public interface CacheOperationSource {
default boolean isCandidateClass(Class<?> targetClass) {
return true;
}
@Nullable
Collection<CacheOperation> getCacheOperations(Method method, @Nullable Class<?> targetClass);
}
該介面中只有一個方法:通過介面和類名獲得對應的 CacheOperation。CacheOperation 是對快取操作的抽象封裝,它的實現類有 3 個:
三個實現類分別對應著 @CacheEvict、@CachePut 和 @Cacheable 註解。
CacheInterceptor
CacheInterceptor 實現的功能就是對 目標方法的實際攔截操作:
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) {
throw th.getOriginal();
}
}
}
CacheInterceptor 程式碼很簡潔,採用函式的形式封裝了真正要執行的函式邏輯,最終把此函式傳交給父類的 execute()去執行。很顯然最終執行目標方法的是 invocation.proceed()。我們直接去父類 CacheAspectSupport 看相關程式碼邏輯:
public abstract class CacheAspectSupport extends AbstractCacheInvoker
implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton {
protected final Log logger = LogFactory.getLog(getClass());
private final Map<CacheOperationCacheKey, CacheOperationMetadata> metadataCache = new ConcurrentHashMap<>(1024);
private final CacheOperationExpressionEvaluator evaluator = new CacheOperationExpressionEvaluator();
@Nullable
private CacheOperationSource cacheOperationSource;
private SingletonSupplier<KeyGenerator> keyGenerator = SingletonSupplier.of(SimpleKeyGenerator::new);
@Nullable
private SingletonSupplier<CacheResolver> cacheResolver;
......
}
上面摘錄出 的一些屬性:
Map<CacheOperationCacheKey, CacheOperationMetadata> metadataCache
這個 Map 快取了所有被註解修飾的類或者方法對應的基本屬性資訊。
CacheOperationExpressionEvaluator evaluator
解析一些 condition、key、unless 等可以寫 el 表示式的處理器。
SingletonSupplier<KeyGenerator> keyGenerator
key 生成器預設使用的 SimpleKeyGenerator,注意 SingletonSupplier 是 Spring5.1 的新類,實現了介面java.util.function.Supplier ,主要是對 null 值進行容錯。
接著看相關的方法:
public abstract class CacheAspectSupport extends AbstractCacheInvoker
implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton {
// 這個介面來自於 SmartInitializingSingleton 在例項化完所有單例Bean後呼叫
//可以看到在這裡例項化 CacheManager, CacheManager 我們後面會說作用
@Override
public void afterSingletonsInstantiated() {
if (getCacheResolver() == null) {
// Lazily initialize cache resolver via default cache manager...
Assert.state(this.beanFactory != null, "CacheResolver or BeanFactory must be set on cache aspect");
try {
setCacheManager(this.beanFactory.getBean(CacheManager.class));
}
catch (NoUniqueBeanDefinitionException ex) {
throw new IllegalStateException("No CacheResolver specified, and no unique bean of type " +
"CacheManager found. Mark one as primary or declare a specific CacheManager to use.", ex);
}
catch (NoSuchBeanDefinitionException ex) {
throw new IllegalStateException("No CacheResolver specified, and no bean of type CacheManager found. " +
"Register a CacheManager bean or remove the @EnableCaching annotation from your configuration.", ex);
}
}
this.initialized = true;
}
//根據CacheOperation 封裝CacheOperationMetadata
protected CacheOperationMetadata getCacheOperationMetadata(
CacheOperation operation, Method method, Class<?> targetClass) {
CacheOperationCacheKey cacheKey = new CacheOperationCacheKey(operation, method, targetClass);
CacheOperationMetadata metadata = this.metadataCache.get(cacheKey);
if (metadata == null) {
KeyGenerator operationKeyGenerator;
if (StringUtils.hasText(operation.getKeyGenerator())) {
operationKeyGenerator = getBean(operation.getKeyGenerator(), KeyGenerator.class);
}
else {
operationKeyGenerator = getKeyGenerator();
}
CacheResolver operationCacheResolver;
if (StringUtils.hasText(operation.getCacheResolver())) {
operationCacheResolver = getBean(operation.getCacheResolver(), CacheResolver.class);
}
else if (StringUtils.hasText(operation.getCacheManager())) {
CacheManager cacheManager = getBean(operation.getCacheManager(), CacheManager.class);
operationCacheResolver = new SimpleCacheResolver(cacheManager);
}
else {
operationCacheResolver = getCacheResolver();
Assert.state(operationCacheResolver != null, "No CacheResolver/CacheManager set");
}
metadata = new CacheOperationMetadata(operation, method, targetClass,
operationKeyGenerator, operationCacheResolver);
this.metadataCache.put(cacheKey, metadata);
}
return metadata;
}
//真正執行目標方法+ 快取 的實現
@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)
// 如果已經初始化過(有CacheManager,CacheResolver),執行這裡
if (this.initialized) {
//getTargetClass拿到原始Class 解剖代理
Class<?> targetClass = getTargetClass(target);
//簡單的說就是拿到該方法上所有的CacheOperation快取操作,最終一個一個的執行
CacheOperationSource cacheOperationSource = getCacheOperationSource();
if (cacheOperationSource != null) {
// CacheOperationContexts是非常重要的一個私有內部類
// 注意它是複數!不是CacheOperationContext單數
// 所以它就像持有多個註解上下文一樣 一個個執行
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();
}
//內部呼叫的 execute 方法
@Nullable
private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
// 判斷是否同步執行
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
return invokeOperation(invoker);
}
}
// sync=false的情況,走這裡
// Process any early evictions beforeInvocation=true的會在此處最先執行~~~
// 最先處理@CacheEvict註解~~~真正執行的方法請參見:performCacheEvict
// context.getCaches()拿出所有的caches,看看是執行cache.evict(key);方法還是cache.clear();
// 需要注意的的是context.isConditionPassing(result); condition條件此處生效,並且可以使用#result
// context.generateKey(result)也能使用#result
// @CacheEvict沒有unless屬性
processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
CacheOperationExpressionEvaluator.NO_RESULT);
// 執行@Cacheable 看看快取是否能夠命中
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
// 如果快取沒有命中,那就準備一個cachePutRequest
// 因為@Cacheable首次進來肯定命中不了,最終肯定是需要執行一次put操作,這樣下次進來就能命中
List<CachePutRequest> cachePutRequests = new ArrayList<>();
if (cacheHit == null) {
collectPutRequests(contexts.get(CacheableOperation.class),
CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
}
Object cacheValue;
Object returnValue;
// 如果快取命中了,並且並且沒有@CachePut的話,也就直接返回
if (cacheHit != null && !hasCachePut(contexts)) {
cacheValue = cacheHit.get();
returnValue = wrapCacheValue(method, cacheValue);
}
else {
// 有 cachePut 的情況,先執行目標方法再 put 快取
returnValue = invokeOperation(invoker);
cacheValue = unwrapReturnValue(returnValue);
}
// 封裝cacheput物件
collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
// 真正統一執行 cacheput 的地方
for (CachePutRequest cachePutRequest : cachePutRequests) {
cachePutRequest.apply(cacheValue);
}
// 最後才執行 cacheEvict
processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
return returnValue;
}
//快取屬性上下文物件
private class CacheOperationContexts {
private final MultiValueMap<Class<? extends CacheOperation>, CacheOperationContext> contexts;
//檢查註解是否配置同步執行的開關
private final boolean sync;
public CacheOperationContexts(Collection<? extends CacheOperation> operations, Method method,
Object[] args, Object target, Class<?> targetClass) {
//將當前 method 上所有快取操作封裝到一個map 物件中
this.contexts = new LinkedMultiValueMap<>(operations.size());
for (CacheOperation op : operations) {
this.contexts.add(op.getClass(), getOperationContext(op, method, args, target, targetClass));
}
//同步執行與否取決於這個函式
this.sync = determineSyncFlag(method);
}
public Collection<CacheOperationContext> get(Class<? extends CacheOperation> operationClass) {
Collection<CacheOperationContext> result = this.contexts.get(operationClass);
return (result != null ? result : Collections.emptyList());
}
public boolean isSynchronized() {
return this.sync;
}
//只有@Cacheable有sync屬性,所以只需要看CacheableOperation即可
private boolean determineSyncFlag(Method method) {
List<CacheOperationContext> cacheOperationContexts = this.contexts.get(CacheableOperation.class);
if (cacheOperationContexts == null) { // no @Cacheable operation at all
return false;
}
boolean syncEnabled = false;
//只要有一個@Cacheable的sync=true了,那就為true 並且下面還有檢查邏輯
for (CacheOperationContext cacheOperationContext : cacheOperationContexts) {
if (((CacheableOperation) cacheOperationContext.getOperation()).isSync()) {
syncEnabled = true;
break;
}
}
// 執行sync=true的檢查邏輯
if (syncEnabled) {
// sync=true時候,不能還有其它的快取操作 也就是說@Cacheable(sync=true)的時候只能單獨使用
if (this.contexts.size() > 1) {
throw new IllegalStateException(
"@Cacheable(sync=true) cannot be combined with other cache operations on '" + method + "'");
}
//@Cacheable(sync=true)時,多個@Cacheable也是不允許的
if (cacheOperationContexts.size() > 1) {
throw new IllegalStateException(
"Only one @Cacheable(sync=true) entry is allowed on '" + method + "'");
}
// 拿到唯一的一個@Cacheable
CacheOperationContext cacheOperationContext = cacheOperationContexts.iterator().next();
CacheableOperation operation = (CacheableOperation) cacheOperationContext.getOperation();
//@Cacheable(sync=true)時,cacheName只能使用一個
if (cacheOperationContext.getCaches().size() > 1) {
throw new IllegalStateException(
"@Cacheable(sync=true) only allows a single cache on '" + operation + "'");
}
//sync=true時,unless屬性是不支援的,並且是不能寫的
if (StringUtils.hasText(operation.getUnless())) {
throw new IllegalStateException(
"@Cacheable(sync=true) does not support unless attribute on '" + operation + "'");
}
return true;
}
return false;
}
}
}
execute 相關的程式碼上面註解寫的很清晰,大家可以跟著多看幾遍。這裡還有一點沒有說,最終的 get Cache 或者 put Cache 的操作在哪裡呢?剛才在 execute 方法中有一句程式碼:
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
這裡我們看到是先查快取中是否有該 Cache,進去看有個 findCaches 方法:
@Nullable
private Cache.ValueWrapper findInCaches(CacheOperationContext context, Object key) {
for (Cache cache : context.getCaches()) {
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;
}
重點關注 doGet 方法,可以看到這個方法是父類 AbstractCacheInvoker 中的,同類裡面還有 doPut,doEvict 和 doClear。
小知識
@Cacheable
註解sync=true
的效果多執行緒環境下存在多個操作使用相同的引數同步呼叫相同的 key,預設情況下快取不鎖定任何資源所以可能導致多次計算。對於這種情況,
sync
屬性可以將底層鎖住,使得只有一個執行緒進行操作,其他執行緒堵塞直到更新完快取返回結果。
小結
至此我們把上面說過的內容總結一下:
BeanFactoryCacheOperationSourceAdvisor
配置 Cache 的切面和攔截實現。攔截的物件即解析出來的 CacheOperation 物件。
每一個 CacheOperation 在執行的時候被封裝為 CacheOperationContext 物件(一個方法可能被多個註解修飾),最終通過 CacheResolver 解析出快取物件 Cache。
CacheOperation
封裝了@CachePut
、@Cacheable
、@CacheEvict
的屬性資訊,以便於攔截的時候能直接操作此物件來執行邏輯。
解析註解對應的程式碼為 CacheOperation 的工作是 CacheAnnotationParser 來完成的。
CacheInterceptor
CacheInterceptor 執行真正的方法執行和 Cache 操作。最終呼叫其父類提供的四個 do 方法處理 Cache。
以上整體過程為 Spring 啟動對相關注解所在類或者方法的攔截和注入,從而實現 Cache 邏輯。限於篇幅本篇暫討論這些內容,下一篇我們來看 Spring 如何實現對多快取底層方案的支援(本地 Cache,Redis,Guava Cache,Caffeine Cache)。