Spring Cache利用AOP通过合理的抽象,实现了基于注解的缓存功能并且提供了一些默认实现。这样业务代码不用关心底层是使用了什么缓存框架,只需要简单地加一个注解,就能实现缓存功能了。优点替换缓存工具很方便,缺点对于缓存抽象缺少复杂的功能,例如缓存设置过期时间等功能没有很好的支持。
本篇文章主要介绍了阅读spring cache源码类的阅读顺序以及介绍使用
1、cache定义
1 | // cache定义 |
2、cache manager定义
1 | // 管理cache的管理器 |
3、cache&manager两者简单实现
3.1 空实现
1 | // 空cache实现 |
3.2 默认实现
1 | // 空值表示 |
4、cache key生成器
1 | // 定义key生成器 |
5、解析Cache对象
CacheResolver在AOP时会需要根据注解,解析出Cache对象.
1 | // Cache对象解析器 |
6、获取CacheOperation对象
判断一个目标类是否是注册过的CacheAnnotationParser 遍历所有的缓存注解解析器,只要有一个支持解析目标类,则返回true
1 | // 解析class or method注解,获取CacheOperation集合 |
7、AOP增强代码定义
调用此实例定义的缓存操作
1 | // 缓存操作的调用,这里主要对异常进行抛出,告诉调用方需要专门处理此问题类型 |
8、注解定义
1 | // Spring在每次执行前都会检查Cache中是否存在相同key的缓存元素,如果存在就不再执行该方法,而是直接从缓存中获取结果进行返回,否则才会执行并将返回结果存入指定的缓存中 |
9、自动加载以及配置项
1 | // spring 会回调selectImports方法注册了ProxyCachingConfiguration |
10、spring cache额外配置(这一部分代码可以略)
10.1 并发包
基于{@code java.util.concurrent}的缓存的实现包。提供{@link org.springframework.cache.CacheManager CacheManager}和{@link org.springframework.cache.cache cache}实现,以便在运行时使用基于JDK的线程池在Spring上下文中使用
1 | // ConcurrentMapCacheManager使用了JDK的ConcurrentMap。 没什么特殊的类似于SimpleCacheManager |
10.2 配置项
声明式缓存配置的支持包,XML模式是主要配置格式。
1 | // {@link org.springframework.beans.factory.xml.BeanDefinitionParser}实现,允许用户轻松配置启用注释驱动的缓存划分所需的所有基础结构bean。 |
11、总结
至此我们已经把org.springframework.cache
下的所有类库功能讲了一边下面我们进行个小结,看看整个流程是怎样的。
这里copy了一张图:来自https://blog.csdn.net/zhouping118/article/details/108440982
11.1 启动流程
先看看几个字段
1 | CacheManager固名思义:就是对Cache进行管理,可以管理多个Cache. |
1 | 1 启用spring chche |
11.2 运行流程
- 使用:在User类上编写方法String getName(),标注注解@Cacheable;
- 运行过程
- 拦截:
- 拦截器拦截标注有@Cacheable的getName方法,调用父类CacheAspectSupport的execute方法执行;
- 解析缓存注解
- 使用AnnotationCacheOperationSource,AnnotationCacheOperationSource委托SpringCacheAnnotationParser把@Cacheable解析为CacheableOperation,返回CacheOperation集合(一个方法上可以有多个缓存注解,所以返回集合);
- 使用DefaultCacheConfig读取类上的CacheConfig参数;
- 拼装配置
- 把getName上的所有缓存注解封装为CacheOperationContexts。里面包含多个CacheOperationContext,CacheableOperation封装为其中的一个CacheOperationContext(CacheOperationContext包含CacheOperationMetadata);
- 如果配置了sync=true(只有解@Cacheable可配置sync)
- 获取缓存的key值
- 如果@Cacheable没有配置key,则使用SimpleKeyGenerator生成key;
- 如果配置了key,使用CacheOperationExpressionEvaluator创建EvaluationContext,然后计算key中的SpEL表达式的值,做为key返回;
- 获取Cache
- 使用SimpleCacheResolver,SimpleCacheResolver根据@Cacheable配置的cacheNames,调用CacheManager获取Cache;
- 然后调用Cache获取缓存,如果没有缓存,则调用目标方法,并返回目标方法的返回值;
- 获取缓存的key值
- 如果没有配置sync=true
- 执行移除缓存(beforeInvocation=true)
- 如果指定了key,执行上面获取缓存的key的逻辑。移除指定key的缓存/移除所有缓存;
- 执行获取缓存
- 执行上面获取缓存的key的逻辑,然后从配置的所有缓存中获取缓存值;
- 如果缓存没有命中,则执行目标方法,并把返回值保存在缓存中;
- 执行修改缓存
- 执行移除缓存(beforeInvocation=false)
- 执行移除缓存(beforeInvocation=true)
- 拦截:
12、Redis CacheManager实现
先贴一张图
13 spring cache的一些问题
- 默认不支持TTL(Time To Live),也就是缓存过期时间;也不支持TTI(Time To Idle)空闲期,即一个数据多久没被访问将从缓存中移除的时间;还不支持移除策略(Eviction policy),即即如果缓存满了,从缓存中移除数据的策略。
- 诸如上面属性或者其他具体缓存实现的特有属性应该直接通过Cache的具体实现来设置,并且只有Cache实现自己去完成,Spring Cache仅仅是一个抽象,仅提供了比较基本的缓存行为,或许有一些方案可以设置过期时间但是只能设置一个统一的过期时间,而不能精确到每一个key,这明显不够灵活。
- 上面的这个缺点就导致了Spring Cache的实际应用并不是很广泛,就拿Redis来说,或许大部分人都还是在使用的RedisTemplate来操作Reids缓存,因此它可以设置过期时间、以及选择缓存的数据结构、以及获取缓存的方式,这明显比Spring Cache的几个注解要灵活得多!
- 无法根据查询结果中的内容生成缓存key,比如getUser(uid)方法,想通过查询出来的user.email生成缓存key就无法实现了,因为#result不能应用在key上面!
- 由于声明式的Spring Cache是基于Spring AOP实现的,因此默认的AOP实现的缺点在Spring Cache上同样不可避免:
- 默认的AOP的机制导致了声明式缓存只能进行方法级别的缓存管理,也就是说只能在方法执行之前从缓存尝试取数据,方法执行完毕之后才能将结果添加到缓存中!
- AOP的机制导致了同一个AOP类中的事务方法互相调用时,被调用方法的事务配置不会生效,因为Spring AOP的代理机制最终还是通过原始目标对象本身去调用目标方法的,这样被调用的方法就会因为是原始对象调用的而不被拦截,当然也有解决办法,和此前同一个类的AOP方法互相调用的解决办法是一样的,那就是获取代理对象,通过代理对象去调用内层方法!
- 无论是基于JDK动态代理还是CGLIB代理,由于本身的缺陷,它们代理的方法的增强都具有限制。对于JDK的代理,目标类必须实现符合规则的接口(不是说只要是实现了接口就会使用JDK代理,具体规则在AOP源码部分有讲解),并且只能代理实现的接口的方法,而对于CGLIB的代理,目标类不能是final的,并且需要代理的方法也不能是private/final/static的。这些AOP代理的限制也是缓存增强方法的限制。而缓存的代理类型也是通过标签的proxy-target-class属性或者注解的proxyTargetClass属性统一控制的。
总结
- Spring Cache是一个缓存抽象,将大多数缓存的基本功能抽取出来,然后定义一个统一的使用方式(配置和注解),只要是基于Spring的项目,都可以将第三方缓存与Spring Cache快速的结合起来。这样的好处就是我们只需要掌握Spring Cache的使用就能够相当于掌握了大多数第三方缓存的基本操作,降低了学习成本。
- 当前,Spring Cache仅仅抽取了缓存的基本操作行为,对于具体的某些缓存的高级行为,Spring Cache是无能为力的,比如Redis,Spring Cache仅能比较方便的存取普通的key和value,而对于特殊的缓存结构,比如hash、set、zset等,以及它们的特殊操作,比如lpush、rpop等等,此时Spring Cache的能力就显得捉襟见肘了。
- 为此,如果是项目没有使用的特别复杂的缓存操作和缓存结构,那么使用Spring Cache是非常方便(还要考虑过期时间的设置),如果项目的缓存操作比较复杂,那么建议还是建议使用专门的缓存操作API吧,比如RedisTemplate。
14 参考
1 | https://blog.csdn.net/zhouping118/article/details/108440982 |
- 本文作者: 初心
- 本文链接: http://funzzz.fun/2022/03/29/spring-cache源码阅读类顺序/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!