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方法。
- 用匿名内部类,包装一个函数(此处是指原被注解接口的方法),后面可以调用到这个函数;
- 调用父类CacheAspectSupport#execute方法;
图上的三个问题下面一一解答。
4.1.1 initialized的是如何变为true的:
CacheAspectSupport实现了这两个类:
- InitializingBean;
- SmartInitializingSingleton;
InitializingBean:
SmartInitializingSingleton:
可以看到这两个都只有一个方法供实现类实现,看方法名可以看出来,是实例化之后或属性设置了之后分别调用这两个方法的。
看下afterSingletonsInstantiated方法:
4.1.2 Collection包装的是什么?
从上图可以看出,拿到的是:
- 当前请求的方法信息,以及注解的相关信息(以列表的形式,只一个注解);
接下来追一下这段上面这段代码:
Collection<CacheOperation> operations =
cacheOperationSource.getCacheOperations(method, targetClass);
4.1.3 cacheOperationSource.getCacheOperations(method, targetClass);
追踪到了AbstractFallbackCacheOperationSource类,类结构如下:
public abstract class AbstractFallbackCacheOperationSource implements
CacheOperationSource
可以看到,他是一个抽象类,实现了CacheOperationSource接口。
继续往下:
无非就是三个步骤:
- 从attributeCache的Map中拿该方法对应的spring cache注解;
- 如果拿到了(空List也算拿到了),直接返回;如果拿的是空,证明没拿过,解析该方法,拿该方法上的spring cache注解;
- 把拿到的spring cache注解放到attributeCache的Map中;
- 解析到是空,放空List;
接下来看AbstractFallbackCacheOperationSource#computeCacheOperations源码:
上面用到了AopUtils#getMostSpecificMethod,它的源码我们也来看一下;
/**获取最特殊的方法*/
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
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,会有这些限制:
- 不支持unless,这个从代码可以看到,只支持了condition,没有支持unless;这个我没想清楚为什么。。。但Interceptor代码就是这样写的;
- 只能有一个cache,因为代码就写死了一个。我猜这是为了更好地支持同步,它把同步放到了Cache里面去实现;
- 不支持其它的Cache操作,代码里面写死了,只支持Cachable,我猜这也是为了支持同步;
拓展阅读:https://blog.csdn.net/yiyihuazi/article/details/109065327
4.1.4.2 后续处理
继续看后续代码:
//如果是@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//收集任何显式的@CachePutscollectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);// Process any collected put requests, either from @CachePut or a @Cacheable miss//处理任何收集到的放置请求,无论是来自@CachePut还是@Cacheable missfor (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设置: