Spring Cache 帶你飛(一)

rickiyang發表於2021-11-01

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 存起來。

看到這裡我們開始有點眉目,開始知道去哪裡找攔截了什麼和操作了什麼。接下來繼續對 CacheOperationCacheInterceptor 進行深入。

CacheOperation

CacheOperationSource 介面中只有一個方法:

public interface CacheOperationSource {
    default boolean isCandidateClass(Class<?> targetClass) {
        return true;
    }

    @Nullable
    Collection<CacheOperation> getCacheOperations(Method method, @Nullable Class<?> targetClass);
}

該介面中只有一個方法:通過介面和類名獲得對應的 CacheOperationCacheOperation 是對快取操作的抽象封裝,它的實現類有 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)。

相關文章