spring cache源码解析(四)——从@EnableCaching开始来阅读源码

一、spring cache四个注解

  • @EnableCaching:开启缓存功能;
  • @Cacheable:获取缓存;
  • @CachePut:更新缓存;
  • @CacheEvict:删除缓存;
  • @Caching:组合定义多种缓存功能;
  • @CacheConfig:定义公共设置,位于类之上;

1.1 @Cacheable里的几个参数

cacheManager:

cacheResolver:没太理解它的使用场景;

sync:是否同步。

  • 比如同时有3个请求进来,且三个请求的参数一致:如果sync为false,三个请求同时执行;如果sync为true,只会有一个请求执行方法操作,其他两个等待第一个的缓存结果,后续从缓存拿数据。
  • 官方话术:在多线程环境下,某些操作可能使用相同参数同步调用。默认情况下,缓存不锁定任何资源,可能导致多次计算,而违反了缓存的目的。对于这些特定的情况,属性 sync 可以指示底层将缓存锁住,使只有一个线程可以进入计算,而其他线程堵塞,直到返回结果更新到缓存中。
  • 只对参数一直参数影响,参数不一致不会影响;

拓展:

  • hutool工具类,测试专用:创建线程并发测试

二、spring cache实现原理-前言

没有讲源码之前,我们可以想象是怎么实现的:

  • 通过aop动态代理对指定方法进行增强;

2.1 正常的aop是怎么写的?

  • @Aspect;
  • @Before、@Around等,确定以什么样的方式,对什么样的方法进行拦截,最终实现对方法的增强;

例子就不举了,参考芋道:IdempotentAspect类。

三、@EnableCaching

@EnableCaching注解是缓存的开关,如果要使用缓存功能,就必要打开这个开关,这个注解可以定义在Configuration类或者springboot的启动类上面。

3.1 @Import(CachingConfigurationSelector.class)

3.2 @Import

@Import注解就是给Spring容器中导入一些组件,这里传入了一个组件的选择器:CachingConfigurationSelector.class。

它有一个实现其他类的selectImports方法,这个方法的本质是:将所有需要导入的组件以全类名的方式返回;

(出自SpringBoot自动装配原理与自己写一个starter第2.1.2.2节)

3.3 CachingConfigurationSelector

类机构图:

  • ImportSelector,定义了selectImports方法,返回值是字符串数组,这个数组是全路径类名,最终会spring拿到这些类名,实例化对应的类;

>注意这里,泛型是EnableCaching类。

  • AdviceModeImportSelector#selectImports:

  • CachingConfigurationSelector#selectImports:

  • CachingConfigurationSelector#getProxyImports:

3.4 ProxyCachingConfiguration.class

ProxyCachingConfiguration.class代码截图如下:

注入了三个对象:

  • BeanFactoryCacheOperationSourceAdvisor:作用相当于@Aspect注解的类做类似。注入一个xxxAdvisor对象到spring中,其实就相当于注入了一个@Aspect注解的类;
  • CacheOperationSource;
  • CacheInterceptor;

3.5 BeanFactoryCacheOperationSourceAdvisor:

set了两个bean:

  • CacheOperationSource:是一个解析器,用来拿类或方法上的注解;
  • CacheInterceptor:是一个拦截器,此次的核心代码就在这里;

BeanFactoryCacheOperationSourceAdvisor创建好了之后,相当于aop就创建好了,这个创建方式和我们手动写@Aspect类不太一样,但执行的过程是一样的。

由于CacheInterceptor类内容过多,且比较重要,下面单开一节讲:

四、CacheInterceptor

4.1 CacheInterceptor类关系图:

  • MethodInterceptor:spring aop相关的一个接口,用来拦截方法调用,它只有一个invoke方法;
  • CacheAspectSupport:

这里的MethodInterceptor接口是位于"org.aopalliance.intercept"包,除此之外Cglib工具包中的"org.springframework.cglib.proxy"包也有MethodInterceptor接口,但这个接口不是我们所需要的。

4.2 CacheInterceptor#invoke

此方法是重写的MethodInterceptor#invoke方法。

  1. 用匿名内部类,包装一个函数(此处是指原被注解接口的方法),后面可以调用到这个函数;
  2. 调用父类CacheAspectSupport#execute方法;

    图上的三个问题下面一一解答。

4.1.1 initialized的是如何变为true的:

CacheAspectSupport实现了这两个类:

  • InitializingBean;
  • SmartInitializingSingleton;

InitializingBean:

SmartInitializingSingleton:

可以看到这两个都只有一个方法供实现类实现,看方法名可以看出来,是实例化之后或属性设置了之后分别调用这两个方法的。

看下afterSingletonsInstantiated方法:

4.1.2 Collection包装的是什么?

从上图可以看出,拿到的是:

  • 当前请求的方法信息,以及注解的相关信息(以列表的形式,只一个注解);

接下来追一下这段上面这段代码:

java 复制代码
Collection<CacheOperation> operations =
cacheOperationSource.getCacheOperations(method, targetClass);

4.1.3 cacheOperationSource.getCacheOperations(method, targetClass);

追踪到了AbstractFallbackCacheOperationSource类,类结构如下:

java 复制代码
public abstract class AbstractFallbackCacheOperationSource implements
CacheOperationSource

可以看到,他是一个抽象类,实现了CacheOperationSource接口。

继续往下:

无非就是三个步骤:

  1. 从attributeCache的Map中拿该方法对应的spring cache注解;
  2. 如果拿到了(空List也算拿到了),直接返回;如果拿的是空,证明没拿过,解析该方法,拿该方法上的spring cache注解;
  3. 把拿到的spring cache注解放到attributeCache的Map中;
  4. 解析到是空,放空List;

接下来看AbstractFallbackCacheOperationSource#computeCacheOperations源码:

上面用到了AopUtils#getMostSpecificMethod,它的源码我们也来看一下;

java 复制代码
/**获取最特殊的方法*/
public static Method getMostSpecificMethod(Method method, @Nullable Class<?> targetClass) {  

    Class<?> specificTargetClass = targetClass != null ? ClassUtils.getUserClass(targetClass) : null;   
    // 获取最为准确的方法,即如果传入的method只是一个接口方法,则会去找其实现类的同一方法进行
    Method resolvedMethod = ClassUtils.getMostSpecificMethod(method, specificTargetClass);  
    // 如果当前方法是一个泛型方法,则会找Class文件中实际实现的方法
    return BridgeMethodResolver.findBridgedMethod(resolvedMethod);}

接下来,看一下findCacheOperations(specificMethod),中间的我了跳过了,直接追溯到SpringCacheAnnotationParser#parseCacheAnnotations方法:

parseCacheableAnnotation方法:

parseCacheableAnnotation方法:

这是解析Caching注解的方法,其他的spring cache注解也有对应的解析方法,这里就不往下看了。

到这里cacheOperationSource.getCacheOperations(method, targetClass)方法解读已经结束了,Collection装的是什么对象我们也知道了。

4.1.4 CacheAspectSupport#execute

java 复制代码
private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
......
}

这个方法是核心逻辑了。

4.1.4.1 同步处理

先看上半部分:如果sync为true,会先执行哪些逻辑:

  • 1处我没弄懂,这里的同步体现在哪?------难道是CucurrentHashMap,一定是它!!
  • 4处,就是拿缓存,如果没有,调底层方法,但同样没体现出同步;------同上,CucurrentHashMap,其他的比如redis,线程安全的。

注意:

sync如果是true,一定是@Cacheable注解。其他注解没有sync属性。

从上面可以看出,sync如果是true,其他的注解就不生效了,它直接就返回了。代码就是这样的

所以我们再回过头去看Cacheable中sync属性上方的注释,它写到:使用sync为true,会有这些限制:

  1. 不支持unless,这个从代码可以看到,只支持了condition,没有支持unless;这个我没想清楚为什么。。。但Interceptor代码就是这样写的;
  2. 只能有一个cache,因为代码就写死了一个。我猜这是为了更好地支持同步,它把同步放到了Cache里面去实现;
  3. 不支持其它的Cache操作,代码里面写死了,只支持Cachable,我猜这也是为了支持同步;

拓展阅读:https://blog.csdn.net/yiyihuazi/article/details/109065327

4.1.4.2 后续处理

继续看后续代码:

java 复制代码
         
        //如果是@CacheEvict并配置了beforeInvocation是true,代表在执行实际方法前清除缓存
		processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
				CacheOperationExpressionEvaluator.NO_RESULT);

		// 检查是否有@Cacheable命中缓存
		Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));

		// 如果没有找到缓存项,则把每个没命中的@Cacheable,转成CachePut参数,供后面put缓存用
		List<CachePutRequest> cachePutRequests = new LinkedList<>();
		if (cacheHit == null) {
			collectPutRequests(contexts.get(CacheableOperation.class),
					CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
		}

		Object cacheValue;
		Object returnValue;

		if (cacheHit != null && !hasCachePut(contexts)) {
			// 如果没有@CachePut注解操作,直接获取缓存值
			cacheValue = cacheHit.get();
			returnValue = wrapCacheValue(method, cacheValue);
		}
		else {
			// 执行实际方法,不需要取缓存
			returnValue = invokeOperation(invoker);
			cacheValue = unwrapReturnValue(returnValue);
		}

		// Collect any explicit @CachePuts
        //收集任何显式的@CachePuts
		collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);

		// Process any collected put requests, either from @CachePut or a @Cacheable miss
        //处理任何收集到的放置请求,无论是来自@CachePut还是@Cacheable miss
		for (CachePutRequest cachePutRequest : cachePutRequests) {
			cachePutRequest.apply(cacheValue);
		}

		// Process any late evictions
        //处理任何迟来的驱逐注解;与前面的区别在于beforeInvocation是true还是false。true是方法执行之前删除缓存,false是执行之后
		processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);

		return returnValue;

猜测是@CacheEvict的allEntries设置:

相关推荐
森屿Serien几秒前
Spring Boot常用注解
java·spring boot·后端
苹果醋31 小时前
React源码02 - 基础知识 React API 一览
java·运维·spring boot·mysql·nginx
Hello.Reader1 小时前
深入解析 Apache APISIX
java·apache
菠萝蚊鸭2 小时前
Dhatim FastExcel 读写 Excel 文件
java·excel·fastexcel
旭东怪2 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
007php0072 小时前
Go语言zero项目部署后启动失败问题分析与解决
java·服务器·网络·python·golang·php·ai编程
∝请叫*我简单先生2 小时前
java如何使用poi-tl在word模板里渲染多张图片
java·后端·poi-tl
ssr——ssss2 小时前
SSM-期末项目 - 基于SSM的宠物信息管理系统
java·ssm
一棵星2 小时前
Java模拟Mqtt客户端连接Mqtt Broker
java·开发语言
鲤籽鲲3 小时前
C# Random 随机数 全面解析
android·java·c#