专栏首页BAT的乌托邦玩转Spring Cache --- @Cacheable/@CachePut/@CacheEvict注解的原理深度剖析和使用【享学Spring】

玩转Spring Cache --- @Cacheable/@CachePut/@CacheEvict注解的原理深度剖析和使用【享学Spring】

前言

上篇文章介绍了@EnableCaching,用它来开启Spring对缓存注解的支持。本篇文章将继续分析Spring Cache,并且讲解的是我们最为关心的:缓存注解实操方面的原理支持和使用。

开发过程中因注解的优雅、使用简单使得这种方式广泛被大家所接受和使用,本文将按照先原理,再实操的步骤,一步步解惑Spring缓存注解的原理

缓存注解

关于Spring的缓存注解,一共有如下5个:

  1. @Cacheable:缓存
// @since 3.1  可以标注在方法上、类上  下同
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
	// 缓存名称  可以写多个~
	@AliasFor("cacheNames")
	String[] value() default {};
	@AliasFor("value")
	String[] cacheNames() default {};

	// 支持写SpEL,切可以使用#root
	String key() default "";
	// Mutually exclusive:它和key属性互相排斥。请只使用一个
	String keyGenerator() default "";

	String cacheManager() default "";
	String cacheResolver() default "";

	// SpEL,可以使用#root。  只有true时,才会作用在这个方法上
	String condition() default "";
	// 可以写SpEL #root,并且可以使用#result拿到方法返回值~~~
	String unless() default "";
	
	// true:表示强制同步执行。(若多个线程试图为**同一个键**加载值,以同步的方式来进行目标方法的调用)
	// 同步的好处是:后一个线程会读取到前一个缓存的缓存数据,不用再查库了~~~ 
	// 默认是false,不开启同步one by one的
	// @since 4.3  注意是sync而不是Async
	// 它的解析依赖于Spring4.3提供的Cache.get(Object key, Callable<T> valueLoader);方法
	boolean sync() default false;
}
  1. @CachePut:缓存更新
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {

	@AliasFor("cacheNames")
	String[] value() default {};
	@AliasFor("value")
	String[] cacheNames() default {};

	// 注意:它和上面区别是。此处key它还能使用#result
	String key() default "";
	String keyGenerator() default "";

	String cacheManager() default "";
	String cacheResolver() default "";

	String condition() default "";
	String unless() default "";
}
  1. @CacheEvict:缓存删除
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {

	@AliasFor("cacheNames")
	String[] value() default {};
	@AliasFor("value")
	String[] cacheNames() default {};

	// 它也能使用#result
	String key() default "";
	String keyGenerator() default "";

	String cacheManager() default "";
	String cacheResolver() default "";
	String condition() default "";

	// 是否把上面cacheNames指定的所有的缓存都清除掉,默认false
	boolean allEntries() default false;
	// 是否让清理缓存动作在目标方法之前执行,默认是false(在目标方法之后执行)
	// 注意:若在之后执行的话,目标方法一旦抛出异常了,那缓存就清理不掉了~~~~
	boolean beforeInvocation() default false;

}
  1. @Caching:用于处理复杂的缓存情况。比如用户既要根据id缓存一份,也要根据电话缓存一份,还要根据电子邮箱缓存一份,就可以使用它
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Caching {
	Cacheable[] cacheable() default {};
	CachePut[] put() default {};
	CacheEvict[] evict() default {};
}
  1. @CacheConfig:可以在类级别上标注一些共用的缓存属性。(所有方法共享,@since 4.1)
// @since 4.1 出现得还是比较晚的
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheConfig {
	String[] cacheNames() default {};
	String keyGenerator() default "";
	String cacheManager() default "";
	String cacheResolver() default "";
}

属性说明表格:

属性名

解释

value

缓存的名称。可定义多个(至少需要定义一个)

cacheNames

同value属性

keyGenerator

key生成器。字符串为:beanName

key

缓存的 key。可使用SpEL。优先级大于keyGenerator

cacheManager

缓存管理器。填写beanName

cacheResolver

缓存处理器。填写beanName

condition

缓存条件。若填写了,返回true才会执行此缓存。可使用SpEL

unless

否定缓存。false就生效。可以写SpEL

sync

true:所有相同key的同线程顺序执行。默认值是false

allEntries

是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存

beforeInvocation

是否在方法执行前就清空,缺省为 false,如果指定为 true

原理分析

先阅读:【小家Spring】玩转Spring Cache — @Cacheable/@CachePut/@CacheEvict缓存注解相关基础类打点 再读本文,效果会像德芙一般丝滑~

从上篇文章中已经知道了@EnableCaching主要向容器注入了三个Bean:CacheOperationSourceBeanFactoryCacheOperationSourceAdvisorCacheInterceptor。他们是让注解生效的核心类。

CacheOperationSource

它代表缓存操作源,已经分析过。

BeanFactoryCacheOperationSourceAdvisor

从名字就能看出它是一个增强器Advisor,并且还和BeanFactory有关。

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

从上配置知道,这个增强器的切面Advice是CacheInterceptor,并且持有CacheOperationSource的引用。

public class BeanFactoryCacheOperationSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor {
	@Nullable
	private CacheOperationSource cacheOperationSource;

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

	public void setCacheOperationSource(CacheOperationSource cacheOperationSource) {
		this.cacheOperationSource = cacheOperationSource;
	}

	// 注意:此处你可以自定义一个ClassFilter,过滤掉你想忽略的类
	public void setClassFilter(ClassFilter classFilter) {
		this.pointcut.setClassFilter(classFilter);
	}

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

Advisor的实现非常的简单,切点是CacheOperationSourcePointcut,核心逻辑都依托于缓存属性源。所以还没有看这块的,此处再一次推荐:【小家Spring】玩转Spring Cache — @Cacheable/@CachePut/@CacheEvict缓存注解相关基础类打点

CacheInterceptor

缓存拦截器。先说明一点,它的实现模式几乎和TransactionInterceptor一毛一样。所以我又想建议一句了,有空先看看它吧:【小家Spring】源码分析Spring的事务拦截器:TransactionInterceptor和事务管理器:PlatformTransactionManager

同样,CacheInterceptor是缓存真正执行的核心,处理逻辑还是稍显复杂的。

// @since 3.1  它是个MethodInterceptor环绕增强器~~~
public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor, Serializable {

	@Override
	@Nullable
	public Object invoke(final MethodInvocation invocation) throws Throwable {
		Method method = invocation.getMethod();

		// 采用函数的形式,最终把此函数传交给父类的execute()去执行
		// 但是很显然,最终**执行目标方法**的是invocation.proceed();它
		
		//这里就是对执行方法调用的一次封装,主要是为了处理对异常的包装。
		CacheOperationInvoker aopAllianceInvoker = () -> {
			try {
				return invocation.proceed();
			}
			catch (Throwable ex) {
				throw new CacheOperationInvoker.ThrowableWrapper(ex);
			}
		};

		try {
			// //真正地去处理缓存操作的执行,很显然这是父类的方法,所以我们要到父类CacheAspectSupport中去看看。
			return execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments());
		} catch (CacheOperationInvoker.ThrowableWrapper th) {
			throw th.getOriginal();
		}
	}

}

这个类本身的实现很少,主要逻辑都在他的抽象父类:CacheAspectSupport

CacheAspectSupport

它类似于TransactionAspectSupport,父类实现了所有的核心逻辑

// @since 3.1  它相较于TransactionAspectSupport额外实现了SmartInitializingSingleton接口
// SmartInitializingSingleton应该也不会陌生。它在初始化完所有的单例Bean后会执行这个接口的`afterSingletonsInstantiated()`方法
// 比如我们熟悉的ScheduledAnnotationBeanPostProcessor、EventListenerMethodProcessor都是这么来处理的

// 另外还需要注意,它还继承自AbstractCacheInvoker:主要对异常情况用CacheErrorHandler处理
public abstract class CacheAspectSupport extends AbstractCacheInvoker implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton {

	// CacheOperationCacheKey:缓存的key  CacheOperationMetadata就是持有一些基础属性的性息
	// 这个缓存挺大,相当于每一个类、方法都有气对应的**缓存属性元数据**
	
	private final Map<CacheOperationCacheKey, CacheOperationMetadata> metadataCache = new ConcurrentHashMap<>(1024);
	// 解析一些condition、key、unless等可以写el表达式的处理器~~~
	// 之前讲过的熟悉的有:EventExpressionEvaluator
	private final CacheOperationExpressionEvaluator evaluator = new CacheOperationExpressionEvaluator();
	// 属性源,默认情况下是基于注解的`AnnotationCacheOperationSource`
	@Nullable
	private CacheOperationSource cacheOperationSource;
	// 看到了吧  key生成器默认使用的SimpleKeyGenerator
	// 注意SingletonSupplier是Spring5.1的新类,实现了接口java.util.function.Supplier  主要是对null值进行了容错
	private SingletonSupplier<KeyGenerator> keyGenerator = SingletonSupplier.of(SimpleKeyGenerator::new);

	@Nullable
	private SingletonSupplier<CacheResolver> cacheResolver;
	@Nullable
	private BeanFactory beanFactory;
	private boolean initialized = false;

	// @since 5.1
	public void configure(@Nullable Supplier<CacheErrorHandler> errorHandler, @Nullable Supplier<KeyGenerator> keyGenerator,@Nullable Supplier<CacheResolver> cacheResolver, @Nullable Supplier<CacheManager> cacheManager) {
		// 第二个参数都是默认值,若调用者没传的话
		this.errorHandler = new SingletonSupplier<>(errorHandler, SimpleCacheErrorHandler::new);
		this.keyGenerator = new SingletonSupplier<>(keyGenerator, SimpleKeyGenerator::new);
		this.cacheResolver = new SingletonSupplier<>(cacheResolver, () -> SimpleCacheResolver.of(SupplierUtils.resolve(cacheManager)));
	}

	// 此处:若传入了多个cacheOperationSources,那最终使用的就是CompositeCacheOperationSource包装起来
	// 所以发现,Spring是支持我们多种 缓存属性源的
	public void setCacheOperationSources(CacheOperationSource... cacheOperationSources) {
		Assert.notEmpty(cacheOperationSources, "At least 1 CacheOperationSource needs to be specified");
		this.cacheOperationSource = (cacheOperationSources.length > 1 ? new CompositeCacheOperationSource(cacheOperationSources) : cacheOperationSources[0]);
	}
	// @since 5.1 单数形式的设置
	public void setCacheOperationSource(@Nullable CacheOperationSource cacheOperationSource) {
		this.cacheOperationSource = cacheOperationSource;
	}
	
	... // 省略各种get/set方法~~~

	// CacheOperationSource必须不为null,因为一切依托于它
	@Override
	public void afterPropertiesSet() {
		Assert.state(getCacheOperationSource() != null, "The 'cacheOperationSources' property is required: " + "If there are no cacheable methods, then don't use a cache aspect.");
	}

	// 这个来自于接口:SmartInitializingSingleton  在实例化完所有单例Bean后调用
	@Override
	public void afterSingletonsInstantiated() {
		// 若没有给这个切面手动设置cacheResolver  那就去拿CacheManager吧
		// 这就是为何我们只需要把CacheManager配进容器里即可  就自动会设置在切面里了
		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 {
				// 请注意:这个方法实际上是把CacheManager包装成了一个SimpleCacheResolver
				// 所以最终还是给SimpleCacheResolver赋值
				setCacheManager(this.beanFactory.getBean(CacheManager.class));
			} ...
		}
		this.initialized = true;
	}

	// 主要为了输出日志,子类可复写
	protected String methodIdentification(Method method, Class<?> targetClass) {
		Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
		return ClassUtils.getQualifiedMethodName(specificMethod);
	}

	// 从这里也能看出,至少要指定一个Cache才行(也就是cacheNames)
	protected Collection<? extends Cache> getCaches(CacheOperationInvocationContext<CacheOperation> context, CacheResolver cacheResolver) {

		Collection<? extends Cache> caches = cacheResolver.resolveCaches(context);
		if (caches.isEmpty()) {
			throw new IllegalStateException("No cache could be resolved for '" +
					context.getOperation() + "' using resolver '" + cacheResolver +
					"'. At least one cache should be provided per cache operation.");
		}
		return caches;
	}


	// 这个根据CacheOperation 这部分还是比较重要的
	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) {
			// 1、指定了KeyGenerator就去拿这个Bean(没有就报错,所以key不要写错了)
			// 没有指定就用默认的
			KeyGenerator operationKeyGenerator;
			if (StringUtils.hasText(operation.getKeyGenerator())) {
				operationKeyGenerator = getBean(operation.getKeyGenerator(), KeyGenerator.class);
			} else {
				operationKeyGenerator = getKeyGenerator();
			}

			// 1、自己指定的CacheResolver
			// 2、再看指定的的CacheManager,包装成一个SimpleCacheResolver
			// 3、
			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
			metadata = new CacheOperationMetadata(operation, method, targetClass, operationKeyGenerator, operationCacheResolver);
			this.metadataCache.put(cacheKey, metadata);
		}
		return metadata;
	}

	// qualifiedBeanOfType的意思是,@Bean类上面标注@Qualifier注解也生效
	protected <T> T getBean(String beanName, Class<T> expectedType) {
		if (this.beanFactory == null) {
			throw new IllegalStateException(
					"BeanFactory must be set on cache aspect for " + expectedType.getSimpleName() + " retrieval");
		}
		return BeanFactoryAnnotationUtils.qualifiedBeanOfType(this.beanFactory, expectedType, beanName);
	}

	// 请Meta数据的缓存
	protected void clearMetadataCache() {
		this.metadataCache.clear();
		this.evaluator.clear();
	}


	// 父类最为核心的方法,真正执行目标方法 + 缓存操作
	@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  解剖代理(N层都能解开)
			Class<?> targetClass = getTargetClass(target);
			CacheOperationSource cacheOperationSource = getCacheOperationSource();
			
			if (cacheOperationSource != null) {
				// 简单的说就是拿到该方法上所有的CacheOperation缓存操作,最终一个一个的执行~~~~
				Collection<CacheOperation> operations = cacheOperationSource.getCacheOperations(method, targetClass);
				if (!CollectionUtils.isEmpty(operations)) {

					// CacheOperationContexts是非常重要的一个私有内部类
					// 注意它是复数哦~不是CacheOperationContext单数  所以它就像持有多个注解上下文一样  一个个执行吧
					// 所以我建议先看看此类的描述,再继续往下看~~~
					return execute(invoker, method, new CacheOperationContexts(operations, method, args, target, targetClass));
				}
			}
		}

		// 若还没初始化  直接执行目标方法即可
		return invoker.invoke();
	}



	@Nullable
	private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
		// Special handling of synchronized invocation
		// 如果是需要同步执行的话,这块还是
		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, cache.get(key, () -> unwrapReturnValue(invokeOperation(invoker))));
				}
				catch (Cache.ValueRetrievalException ex) {
					// The invoker wraps any Throwable in a ThrowableWrapper instance so we
					// can just make sure that one bubbles up the stack.
					throw (CacheOperationInvoker.ThrowableWrapper) 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));

		// Collect puts from any @Cacheable miss, if no cached item is found
		List<CachePutRequest> cachePutRequests = new LinkedList<>();
		// 如果缓存没有命中,那就准备一个cachePutRequest
		// 因为@Cacheable首次进来肯定命中不了,最终肯定是需要执行一次put操作的~~~这样下次进来就能命中了呀
		if (cacheHit == null) {
			collectPutRequests(contexts.get(CacheableOperation.class), CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
		}

		Object cacheValue;
		Object returnValue;

		// 如果缓存命中了,并且并且没有@CachePut的话,也就直接返回了~~
		if (cacheHit != null && !hasCachePut(contexts)) {
			// If there are no put requests, just use the cache hit
			cacheValue = cacheHit.get();
			// wrapCacheValue主要是支持到了Optional
			returnValue = wrapCacheValue(method, cacheValue);
		} else { //到此处,目标方法就肯定是需要执行了的~~~~~
			// Invoke the method if we don't have a cache hit
			// 啥都不说,先invokeOperation执行目标方法,拿到方法的的返回值  后续在处理put啥的
			returnValue = invokeOperation(invoker);
			cacheValue = unwrapReturnValue(returnValue);
		}

		// Collect any explicit @CachePuts   explicit:明确的
		collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);

		// Process any collected put requests, either from @CachePut or a @Cacheable miss
		for (CachePutRequest cachePutRequest : cachePutRequests) {
			// 注意:此处unless啥的生效~~~~
			// 最终执行cache.put(key, result);方法
			cachePutRequest.apply(cacheValue);
		}

		// Process any late evictions beforeInvocation=true的会在此处最先执行~~~  beforeInvocation=false的会在此处最后执行~~~
		// 所以中途若抛出异常,此部分就不会执行了~~~~
		processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
		return returnValue;
	}


	// 缓存属性的上下文们。每个方法可以对应多个上下文~~~
	private class CacheOperationContexts {

		// 因为方法上可以标注多个注解  
		// 需要注意的是它的key是Class,而CacheOperation的子类也就那三个哥们而已~
		private final MultiValueMap<Class<? extends CacheOperation>, CacheOperationContext> contexts;
		// 是否要求同步执行,默认值是false
		private final boolean sync;

		public CacheOperationContexts(Collection<? extends CacheOperation> operations, Method method, Object[] args, Object target, Class<?> targetClass) {

			this.contexts = new LinkedMultiValueMap<>(operations.size());
			for (CacheOperation op : operations) {
				this.contexts.add(op.getClass(), getOperationContext(op, method, args, target, targetClass));
			}

			// sync这个属性虽然不怎么使用,但determineSyncFlag这个方法可以看一下
			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; // 只有校验都通过后,才返回true
			}
			return false;
		}
	}
	...
}

以上,拦截器实现了Spring Cache处理注解缓存的执行的核心步骤,个人建议上述代码可多读几遍,其义自见。

处理缓存注解的步骤总结

Spring Cache是Spring框架的核心模块之一,不可谓不重要。用了好几篇文章专门来讲解使用、分析原理。下面按照正常的思路,我把Spring处理的步骤总结如下:

  1. CacheOperation封装了@CachePut@Cacheable@CacheEvict(下称三大缓存注解)的属性信息,以便于拦截的时候能直接操作此对象来执行逻辑。 1. 解析三大注解到CacheOperation的过程是由CacheAnnotationParser完成的
  2. CacheAnnotationSource代表缓存属性源,非常非常重要的一个概念。它提供接口方法来获取目标方法的CacheOperation集合。由上可知,这个具体工作是委托给CacheAnnotationParser去完成的
  3. BeanFactoryCacheOperationSourceAdvisor它代表增强器,至于需要增强哪些类呢???就是看有没有存在CacheOperation属性的方法
  4. CacheInterceptor实现了MethodInterceptor接口,在Spring AOP中实现对执行方法的拦截。在调用invoke执行目标方法前后,通过CacheAnnotationSource获取到方法所有的缓存操作属性,从而一个个的执行
  5. 执行的时候,每一个CacheOperation最后被封装成了CacheOperationContext,而CacheOperationContext最终通过CacheResolver解析出缓存对象Cache(可能是多个)
  6. 最后最后最后,CacheInterceptor调用其父类AbstractCacheInvoker执行对应的doPut / doGet / doEvict / doClear 等等。(可以处理执行异常)

CacheProxyFactoryBean:手动实现Cache功能

其实ProxyFactoryBean的设计模式在Spring AOP中已经非常不陌生了:【小家Spring】面向切面编程Spring AOP创建代理的方式:ProxyFactoryBean、ProxyFactory、AspectJProxyFactory(JDK Proxy和CGLIB)

如下截图,Spring内有非常多的xxxProxyFactoryBean的实现:

如果说把@EnableCaching称为自动模式的话,那使用CacheProxyFactoryBean就完全是手动档。话不多说,此处给个使用Demo就收场了:

@Configuration
public class RootConfig {

    @Bean
    public CacheProxyFactoryBean cacheProxyFactoryBean() {
        CacheProxyFactoryBean proxyFactoryBean = new CacheProxyFactoryBean();
        // 使用AnnotationCacheOperationSource来识别三大注解缓存
        proxyFactoryBean.setCacheOperationSources(new AnnotationCacheOperationSource());

        // 设置需要代理的目标类
        CacheDemoService cacheDemoService = new CacheDemoServiceImpl();
        proxyFactoryBean.setTarget(cacheDemoService);
        //proxyFactoryBean.setProxyInterfaces();

        // 设置个性化的一些东西
        CacheManager cacheManager = new ConcurrentMapCacheManager();
        proxyFactoryBean.setCacheManager(cacheManager);
        //proxyFactoryBean.setKeyGenerator();
        //proxyFactoryBean.setCacheResolver();

        return proxyFactoryBean;
    }

}


//@Service // 因为使用了CacheProxyFactoryBean手动额皮质,此处请不要再被扫描进去,否则容器内就出现两个这样的Bean了
public class CacheDemoServiceImpl implements CacheDemoService {

    @Cacheable(cacheNames = "demoCache", key = "#id")
    @Override
    public Object getFromDB(Integer id) {
        System.out.println("模拟去db查询~~~" + id);
        return "hello cache...";
    }
}

测试:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RootConfig.class, CacheConfig.class})
public class TestSpringBean {

    @Autowired
    private CacheDemoService cacheDemoService;

    @Test
    public void test1() {
        cacheDemoService.getFromDB(1);
        cacheDemoService.getFromDB(1);
    }

}

打印结果:

模拟去db查询~~~1

只输出一套日志:缓存生效

此示例中@EnableCaching可不是打开状态哦,但我们依然能够使用手动档让缓存生效。 使用手动档,我们可以很方便的使用NameMatchCacheOperationSource来根据方法名匹配~~~


缓存注解使用案例

关于缓存注解的常规使用案例,我觉得本文没有必要介绍。 接下来主要讲解一些特殊的使用:

若方法返回值为null,还会缓存吗?

比如上例返回值改为null:

@Service
public class CacheDemoServiceImpl implements CacheDemoService {

    @Cacheable(cacheNames = "demoCache", key = "#id")
    @Override
    public Object getFromDB(Integer id) {
        System.out.println("模拟去db查询~~~" + id);
        //return "hello cache...";
        return null;
    }
}

执行单测:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RootConfig.class, CacheConfig.class})
public class TestSpringBean {
    @Autowired
    private CacheDemoService cacheDemoService;
    @Autowired
    private CacheManager cacheManager;

    @Test
    public void test1() {
        cacheDemoService.getFromDB(1);
        cacheDemoService.getFromDB(1);

        System.out.println("----------验证缓存是否生效----------");
        Cache cache = cacheManager.getCache("demoCache");
        System.out.println(cache);
        System.out.println(cache.get(1, String.class));
    }
}

结果打印:

模拟去db查询~~~1
----------验证缓存是否生效----------
org.springframework.cache.concurrent.ConcurrentMapCache@15f2eda3
null

结论是:默认情况下,返回值是null也是会缓存的(第二次过来就不会再查询了)。通过一个断点会更清晰:

但假如修改缓存注解如下:

// 注意:unless 是非的意思
@Cacheable(cacheNames = "demoCache", key = "#id",unless = "#result == null")

运行打印如下:

模拟去db查询~~~1
模拟去db查询~~~1
----------验证缓存是否生效----------
org.springframework.cache.concurrent.ConcurrentMapCache@6a282fdd
null

查了两次DB,证明此时返回null就不会再缓存了,unless生效。

倘若修改配置如下:

    @Bean
    public CacheManager cacheManager() {
        ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
        cacheManager.setAllowNullValues(false);
        return cacheManager;
    }

运行则报错:

java.lang.IllegalArgumentException: Cache 'demoCache' is configured to not allow null values but null was provided

一般情况下,不建议这么设置,因为一般都是允许缓存null值的。

@Cacheable注解sync=true的效果

在多线程环境下,某些操作可能使用相同参数同步调用(相同的key)。默认情况下,缓存不锁定任何资源,可能导致多次计算,而违反了缓存的目的。对于这些特定的情况,属性 sync 可以指示底层将缓存锁住,使只有一个线程可以进入计算,而其他线程堵塞,直到返回结果更新到缓存中(Spring4.3提供的)。

下面给个例子直接看看效果就成:

@Cacheable(cacheNames = "demoCache", key = "#id")

测试Demo(多线程):

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RootConfig.class, CacheConfig.class})
public class TestSpringBean {

    @Autowired
    private CacheDemoService cacheDemoService;

    @Test
    public void test1() throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                cacheDemoService.getFromDB(1);
            }).start();
        }

        // 保证main线程不要这么快结束(否则没有日志结果的~~~)
        TimeUnit.SECONDS.sleep(10);

    }

}

打印结果:

模拟去db查询~~~1
模拟去db查询~~~1
模拟去db查询~~~1
模拟去db查询~~~1
模拟去db查询~~~1

打印可能5次、可能6次不确定。但是若我们sync=true了呢?

@Cacheable(cacheNames = "demoCache", key = "#id", sync = true)

再次运行测试打印结果如下:

模拟去db查询~~~1

永远只会打印一次。 所以在高并发环境下,我个人是十分建议开启此同步开关的,至于非高并发,无所谓啦~

注解可重复标注吗?

因为源码都分析了,所以此处看结论即可。

    @CachePut(cacheNames = "demoCache", key = "#id") // 不同的注解可以标注多个
    //@Cacheable(cacheNames = "demoCache", key = "#id") // 相同注解标注两个是不行的 因为它并不是@Repeatable的
    @Cacheable(cacheNames = "demoCache", key = "#id")
    @Override
    public Object getFromDB(Integer id) {
        System.out.println("模拟去db查询~~~" + id);
        return "hello cache...";
    }

不同的注解可以标注多个,且都能生效。若需要相同注解标注多个等更复杂的场景,推荐使用@Caching注解

如何给缓存注解设置专用的key生成器、缓存管理器等等

标准写法是这样的:

@EnableCaching
@Configuration
public class CacheConfig implements CachingConfigurer {
    @Override
    public CacheResolver cacheResolver() {
        return null;
    }
    @Override
    public KeyGenerator keyGenerator() {
        return null;
    }
    @Override
    public CacheErrorHandler errorHandler() {
        return null;
    }
}

实现对应的方法,可以new一个对象返回,也可以用容器内的。

大多数情况下我们都不需要特别的指定缓存注解使用的管理器,因为它自己会去容器里找。 但是,但是,但是当你使用了多套缓存时,我还是建议显示的指定的。

总结

本篇文章相对较长,因为从原理处深度分析了Spring Cache的执行过程,希望能帮助到大家做到心里有数,从而更加健康的书写代码和扩展功能

关于缓存注解这块,我相信很多人都有一个痛点:注解并不支持设置过期时间。其实我想说,如果你看了上篇文章就知道,这个Spring它做不了,因为它并没有对Expire进行抽象。 但是Spring做不了不代表我们自己做不了,因此有兴趣的同学可以在此基础上,扩展出可以自定义超时时间的能力~~~~

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 聊聊Spring Cache的缓存抽象与JSR107缓存抽象JCache,并使用API方式使用Spring Cache【享学Spring】

    缓存(Cache)是计算机领域一个极其重要的概念,它是提高硬件(比如CPU、显卡)、软件运行效率非常重要且有效的一个手段,它的最大特点就一个字:速度非常快。

    YourBatman
  • 玩转Spring Cache --- @Cacheable/@CachePut/@CacheEvict缓存注解相关基础类打点【享学Spring】

    本文算是了解缓存注解原理的先行文章,因为它抽象出来的模块类比较多,所以做这篇文章进行关键类的打点。 若我们需要扩展缓存注解的能力,对这些抽象是非常有必要深入了...

    YourBatman
  • 【小家Spring】Spring Boot中使用RedisTemplate优雅的操作Redis,并且解决RedisTemplate泛型注入失败的问题

    Spring为了简化我们对持久化层的操作,针对各种持久化方案提供了统一的Template进行操作。比如我们的熟悉的JdbcTemplate就是让我们方便的操作的...

    YourBatman
  • Android 内存缓存:手把手教你学会LrhCache算法

    当 accessOrder 参数设置为true时,存储顺序(遍历顺序) = 外部访问顺序

    Carson.Ho
  • Windows Server AppFabric Caching

    这套 AppFabric Caching 比我用过的 memcached 复杂多了,MSDN有一篇文章进行介绍Introduction to Caching w...

    张善友
  • 细说shiro之七:缓存

    Shiro作为一个开源的权限框架,其组件化的设计思想使得开发者可以根据具体业务场景灵活地实现权限管理方案,权限粒度的控制非常方便。 首先,我们来看看Shiro...

    2Simple
  • 微服务化后缓存怎么做

    最近接手的代码中遇到几个缓存的问题,存在一些设计原则的问题,这里总结一下,希望可以对你有帮助

    方丈的寺院
  • 线程安全的iOS通用缓存库-SwiftlyCache(推广)

    iOS开发中或多或少都会使用到Cache来减少网络请求,在网络上也有很多使用Objective-c开发的Cache框架,而Swift开发的Cache框架相对来说...

    大话swift
  • 阿里老司机教你微服务化后缓存怎么做

    最近接手的代码中遇到几个缓存的问题,存在一些设计原则的问题,这里总结一下,希望可以对你有帮助

    本人秃顶程序员
  • redis缓存

    缓存穿透,是指查询一个数据库一定不存在的数据。正常的使用缓存流程大致是,数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询...

    挑战者

扫码关注云+社区

领取腾讯云代金券