“缓存”是我们日常开发中非常重要的一个环节,是提高产品性能、保证程序稳定最简单粗暴的方法之一。
缓存(Cache)应用在很多地方,比如:操作系统磁盘缓存、Web服务器缓存、客户端浏览器缓存、应用程序缓存、数据库缓存等等。我们此次进行深入了解的,就是应用在Java后端程序缓存的SpringCache。
缓存抽象概念参考
说起JSR107或者说是JCache,估计大多数小伙伴都会觉得非常的陌生,没用过且还没听过。JSR107的草案提得其实是非常的早的,但是第一个Final Release版本却一直难产到了2014年,如图(本文截自JSR官网)
虽然最终它还是被作为JSR规范提出了,但那时已经4102年了,黄瓜菜早就凉凉~
在还没有缓存规范出来之前,作为Java市场标准制定的强有力竞争者:Spring框架动作频频,早在2011年就提供了它自己的缓存抽象(Spring3.1)。这一切依托于Spring的良好生态下,各大缓存厂商纷纷提供了实现产品。
因此目前而言,关于缓存这块业界有个通识:
Spring Cache缓存抽象已经成了业界实际的标准(几乎所有产品都支持)
JSR107仅仅只是官方的标准而已(支持的产品并不多)
因为JSR107使用得极少,因此此处对它只做比较简单的一个概念介绍即可。
上面说了JCache真正发布都到2014年了,而早在2011年Spring3.1版本就定义了它自己的缓存抽象,旨在帮助开发者简化缓存的开发,并且最终流行开来。
这就是我们目前使用的SpringCache。进入源码查看,SpringCache的大部分内容,都在spring-context包内。
还有一部分具体缓存的实现,则在spring-context-support包内,该包内是一些缓存方案的具体实现。当然,还有一部分内容在org.springframework.cache.aspectj包中,该部分内容主要是springcache通过AspectJ方式拦截方法时,一些必要的类。
首先,此次源码阅读基于spring-framework 5.1.x版本。在源码阅读的过程中,因为我水平有限,可能有一些表述不太合适,但应该不影响后面的阅读体验,请见谅。
推荐大家阅读时在github上folk一份spring-framework的源码跟着一起看。当然,可以通过folk或clone我的仓库来看https://github.com/Kwin1113/spring-framework/tree/read,在read分支中,对spring-context包内cache部分做了作者注释的翻译,方便大家阅读。
接下来,我们深入看一看spring-context这一部分。我们从包结构来看,org.springframework.cache下几个主要的包为annotation、config、interceptor。在了解这些包内容之前,我们先来关注一下包外的这两个接口,Cache和CacheManager。
这三大注解的功能想必也不用多说,@EnableCaching自然就是开启注解驱动的缓存功能。@Caching用于在一个类或一个方法上,配置多个基础缓存注解——也就是三大注解。而@CacheConfig可能相对用的比较少,该注解提供了一种在类级别共享常见的缓存相关设置的机制,当此注解出现在给定的类上时,它将为该类中定义的任何缓存操作提供一组默认设置,其中只有四个属性,这四个属性作为类中方法上三大注解缺少某个属性时作为缺省值使用。
2. 接下来看看其中的两个接口和一个抽象类。
通常情况下,不会直接实现该接口,推荐通过继承其子类CachingConfigureSupport来定义配置,此时可以比较方便地仅实现自己所需的配置,而无需实现接口定义的所有方法。具体配置方式可以参考源码中测试类中给出的demo。
该选择器逻辑很简单,就是通过mode参数来判断加载哪些配置。
我们主要来看看默认情况下,即mode=proxy时,必须导入的类是AutoProxyRegistrar和ProxyCachingConfiguration。ProxyCachingConfiguration我们放后面讲,先来简单讲一下AutoProxyRegistrar。该类根据当前BeanDefinitionRegistry来注册一个自动代理创建器,其基于一个@Enable*注解,该注解需要包含mode和proxyTargetClass参数,并正确设值。也就是说在SpringCache中配置的一些增强Advisor,将会通过该自动代理创建器自动生成。
看到这里,应该能发现annotation包中基本上是通过注解驱动的SpringCache。那么我们接下来看一下config包中的内容。
首先,我们从这个CacheNamespaceHandler开始。
可以看到,CacheNamespaceHandler实现了NamespaceHandler接口,那么Spring会默认调用该类的init()方法。
该方法注册了两个bean定义解析器,我们继续来看AnnotationDrivenCacheBeanDefinitionParser,该类是BeanDefinitionParser的实现类,允许用户轻松配置所有需要注解缓存区分的基础bean。
其在加载时,通过类加载器判断是否加载了jsr107和jcache的类,并初始化两个布尔值变量,通过这两个变量来进行相关的额外bean的注册。看到这里,你一定发现了,这个类和annotation包中的CachingConfigurationSelector是一样的逻辑,我们进行简单的分析就行。
这边入口方法parse()会通过xml中cache:annotation-driven标签中的mode属性来进行判断,通过什么方式来进行方法拦截。默认情况下,mode参数为proxy,也就是通过Spring AOP代理拦截。那么通过Spring AOP代理拦截的这个registerCacheAdvisor()方法做了些什么呢?我们顺着代码进入第一行:
所以说,这一步主要是将InfrastructureAdvisorAutoProxyCreate注入到容器中。该类主要做的是自动创建代理,而从这个名字上也能看出来,该类只为Role为INFRASTRUCTURE的类生成代理。
在往下看这一行代码:
巧了,这一步做的就是定义上一步做的Role为INFRASTRUCTURE的bean定义,我们接着看看他定义了哪些缓存增强。
这一步定义了三个BeanDefinition.ROLE_INFRASTRUCTURE的bean定义,分别是AnnotationCacheOperationSource、CacheInterceptor和BeanFactoryCacheOperationSourceAdvisor。
并且仔细看,能看到CacheInterceptor和BeanFactoryOperationSourceAdvisor,都是将第一步的缓存操作资源(cacheOperationSources)通过运行时引用作为该bean的属性值。
看到这里,其实config包中,做的事是和annotation包中那几个类完全相同,只是解析方式和注入方式有些微的差别。
接下来轮到interceptor包了,之前annotation和config中主要是一些配置操作,接下来我们看看SpringCache是如何在方法上做缓存的。
老规矩,我们还是从接口开始看起。interceptor包中有7个接口,分别是BasicOperation、CacheErrorHandler、CacheOperationInvocationContext、CacheOperationInvoker、CacheOperationSource、CacheResolver和KeyGenerator。该包中的其他类,基本上也都是这几个接口的实现类。
现在应该能发现,前面说过的CacheAnnotationParser从类或方法上解析出来的就是CacheOperation的集合,也就是CacheableOperation、CachePutOperation和CacheEvictOperation三种缓存操作。
该接口是处理缓存相关错误的策略接口。在大多数情况下,缓存实现抛出的所有异常都应该被直接抛出,但是在一些情况下,需要用特殊的方式去处理缓存实现抛出的异常。
该接口定义了处理获取、设置、移除、清除缓存四种操作异常时的处理方法。也就是CachingConfigurer中的ErrorHandler,可以通过继承CachingConfigurerSupport并重写errorHandler()方法自定义。默认的实现类SimpleCacheErrorHandler不作额外处理,直接抛出异常。
那为什么不直接把CacheOperation封装到CacheOperationContext上下文里呢?其实CacheAspectSupport通过Map
该接口被注解为@FunctionalInterface,也就是函数式编程接口。其目前只作为MethodInvocation的一层抽象,也就是缓存注解的底层方法调用,主要功能还是对异常做了一个自定义操作。
我们重点来看看红框中的这一步,类名中带有fallback字样,说明该类实现中提供了fallback策略,其实就是在这一步完成的,其顺序为1.specific target method;2. target class; 3. declaring method; 4. declaring class/interface.,保证尽可能获取到缓存操作。其中的findCacheOperations(xxx)方法即为委托给子类AnnotationCacheOperationSource实现的方法,这个在上文中有介绍了。
6. 接下来是两个比较基础的接口CacheResovler
该接口定义了从缓存操作调用上下文CacheOperationInvocationContext中真正解析出所操作的Cache的方法。
我们来看一下该接口的实现类,首先也是一个抽象类AbstractCacheResolver,该类持有一个CacheManager成员,其通过CacheManager来进行缓存的管理。
其主要的方法的实现如下,其中getCacheNames()方法委托给子类实现:
在SpringCache中,给定默认的SimpleCacheResolver,其实现的getCacheNames()直接从给定的上下文中获取缓存操作中的缓存名称(可以自定义CacheResolver实现骚操作,例如使用参数做缓存名称等)。此外还提供了一个给定缓存名称的NamedCacheResolver实现,有兴趣可以自行了解。
KeyGenerator
该接口为缓存key生成器接口,基于给定方法(缓存上下文中)和参数生成key。其也有默认实现SimpleKeyGenerator,通过参数生成简单的SimpleKey类作为key。可以实现该接口自定义key的生成策略,这个就比较常见了。
AbstractCacheInvoker
接口介绍完了,顺便介绍一下抽象类。该包中总共有三个抽象类,上面介绍了其中之二——AbstractCacheResolver和AbstractFallbackCacheOperationSource,分别是抽象缓存处理器和抽象缓存操作源。接下来介绍一下AbstractCacheInvoker,该类是调用缓存操作的基础组件:
该抽象类实现了四个最重要的缓存操作方法,并且没有定义抽象方法,因此是类似工具类的使用,只需要继承该抽象类,就可以对相关缓存进行操作。
我们可以看到,该类持有一个CacheErrorHandler的,也就是说,在操作缓存时发生的异常都是在这一个部分进行处理的。
接着是无需同步的操作:
那么这就是缓存操作的实际处理逻辑,其中的一些具体方法大家可以自行在源码中点进去看看逻辑,大概的逻辑通过方法名也能知道。
我们先来看看这个上下文对象CacheEvaluationContext:
该类实现的是SpEL表达式解析EvaluationContext接口。
其中定义了以上这些方法,除此之外CacheEvaluationContext还定义了一个不可操作的缓存变量集合Set unavailableVariables,并且通过父类MethodBasedEvaluationContext实现了懒汉模式加载变量,避免了参数发现时不必要的类字节码的解析。
CacheExpressionRootObject作为表达式解析过程中的根对象:
接下来是核心类CacheOperationExpressionEvaluator:
该类是解析SpEL表达式的工具类,作为可重用,线程安全的组件使用。其中最重要的方法就是红框中的这三个方法了,分别是计算表达式给定的缓存key、缓存条件和非缓存条件。其是通过SpEL计算的,本文不做额外的详细介绍。
值得关注的是这三个方法调用的getExpression()方法,在这步中做了一个缓存操作,避免重复对不变的SpEL表达式进行计算求值。
对表达式的求值过程,自然也是封装在CacheAspectSupport里的CacheOperationContext中。
接下来是support包,顾名思义该包中的内容是对SpringCache这套逻辑起到一些辅助支持功能的类。
同样,我们先来看看它的抽象类AbstractCacheManager:
该类实现了常见CacheManager方法。一些缓存管理器并不是直接实现CacheManager接口,而是通过继承该抽象类来完成,例如RedisCacheManager和EhCacheManager等。
相信你看到这个类名也有一些想法了。没错,这个类也实现了Cache接口,其他多数缓存实现都是通过继承该抽线类完成,主要功能是在存储到底层存储之前兼容调整null值(和其他潜在的特殊值)。
分别是CompositeCacheManager,组合CacheManager实现,可对委托{@link CacheManager}实例的给定集合进行迭代。
NoOpCacheManager,基础的无操作CacheManager实现,适用于禁用缓存,通常用于在没有实际存储的情况下支持缓存声明。其对应的缓存为NoOpCache,接受缓存但实际上不作存储。
SimpleCacheManager,通过给定的缓存集合来工作的简单缓存管理器,对于测试或简单的缓存声明很有用。
剩下还有两个类。
NullValue,简单的可序列化类,用于在不支持缓存null的存储时替换null。也就是AbstractValueAdaptingCache中对null值的兼容对象。
SimpleValueWrapper,org.springframework.cache.Cache.ValueWrapper的简单实现。在Cache实际操作中,并不是直接返回缓存的对象,而是返回包装类ValueWrapper。在获取缓存操作时,返回缓存的值本身可能为null,将通过ValueWrapper中返回,直接返回null表示缓存中没有该key的映射。该类就是ValueWrapper接口的实现类。
本文主要对spring-context包中缓存的部分源码进行了解读。具体的推荐大家看看源码,跟着作者的思路往下看看,会对平时使用的SpringCache有一个更深入的理解。
欢迎大家在评论区提出意见想法~