Spring Cache深度解析

tech2023-02-12  93

【Spring源码】Spring Cache深度解析

SpringCache 缓存缓存抽象 JSR107:JCacheSpring缓存抽象深入了解 annotationconfiginterceptorsupportconcurrent总结

SpringCache

缓存

  “缓存”是我们日常开发中非常重要的一个环节,是提高产品性能、保证程序稳定最简单粗暴的方法之一。

  缓存(Cache)应用在很多地方,比如:操作系统磁盘缓存、Web服务器缓存、客户端浏览器缓存、应用程序缓存、数据库缓存等等。我们此次进行深入了解的,就是应用在Java后端程序缓存的SpringCache。

缓存抽象

缓存抽象概念参考

JSR107:JCache

  说起JSR107或者说是JCache,估计大多数小伙伴都会觉得非常的陌生,没用过且还没听过。JSR107的草案提得其实是非常的早的,但是第一个Final Release版本却一直难产到了2014年,如图(本文截自JSR官网)

  虽然最终它还是被作为JSR规范提出了,但那时已经4102年了,黄瓜菜早就凉凉~

  在还没有缓存规范出来之前,作为Java市场标准制定的强有力竞争者:Spring框架动作频频,早在2011年就提供了它自己的缓存抽象(Spring3.1)。这一切依托于Spring的良好生态下,各大缓存厂商纷纷提供了实现产品。

  因此目前而言,关于缓存这块业界有个通识:

Spring Cache缓存抽象已经成了业界实际的标准(几乎所有产品都支持)

JSR107仅仅只是官方的标准而已(支持的产品并不多)

因为JSR107使用得极少,因此此处对它只做比较简单的一个概念介绍即可。

Spring缓存抽象

  上面说了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。

Cache

  该接口定义了通用缓存操作的接口,也就是所有缓存实现(RedisCache、EhCache等)都需要实现该接口的这些方法。这个接口定义很好理解,就是操作缓存的基础单位,可以是本地的(本地Map等),也可以是远程的(Redis等)。

CacheManager

  这个接口就更好理解了——缓存管理器,实现该接口的类负责对其下的缓存进行管理。该接口只定义了最最简单但必须的方法,获取缓存和获取缓存名称。

annotation

annotation包中是一些注解相关的配置类或注解类。 

  从下图中,我们能看到几个非常熟悉的注解,特别是缓存三大将:@Cacheable、@CachePut和@CacheEvict。

  这三大注解的功能想必也不用多说,@EnableCaching自然就是开启注解驱动的缓存功能。@Caching用于在一个类或一个方法上,配置多个基础缓存注解——也就是三大注解。而@CacheConfig可能相对用的比较少,该注解提供了一种在类级别共享常见的缓存相关设置的机制,当此注解出现在给定的类上时,它将为该类中定义的任何缓存操作提供一组默认设置,其中只有四个属性,这四个属性作为类中方法上三大注解缺少某个属性时作为缺省值使用。

接下来看看其中的两个接口和一个抽象类。 首先是CachingConfigurer接口:

  该类是配置类实现接口,该接口明确地指定在注解驱动的缓存管理中,如何处理缓存和key的生成方法。子类CachingConfigurerSupport实现了该接口的所有基础实现。

  通常情况下,不会直接实现该接口,推荐通过继承其子类CachingConfigureSupport来定义配置,此时可以比较方便地仅实现自己所需的配置,而无需实现接口定义的所有方法。具体配置方式可以参考源码中测试类中给出的demo。

接着是CacheAnnotationParser接口:

  该接口定义了三个方法。   首先是isCandidateClass方法,顾名思义是判断给定类是否满足规定注解格式的缓存操作类。   然后是两个类似的方法,分别是从类或方法上,解析缓存定义,也就是从类或方法上,找到上述四大注解定义。   其在SpringCache中的默认实现为SpringCacheAnnotationParser:

  在该实现类中,上面提到的规定的注解格式,只有以下四种。

  接口中定义的两个解析缓存定义的方法,最终都走到了parseCacheAnnotations方法上,入参cachingConfig为类或方法对应的默认配置(注解中未配置某一属性时,采用的缺省配置),入参ae即为对应的类或方法,入参localOnly的作用是在上一级调用过程中,确保实现类上的注解优先于接口上的注解。

  该方法主要做的就是解析在类或方法上解析这四种注解,具体的解析通过对应的CacheOperation.Builder来完成。

最后是AbstractCachingConfiguration抽象类:

  该类为配置的抽象实现类,配置支持Spring注解驱动的缓存管理的通用结构。其中enableCaching顾名思义是指定是否支持缓存的参数,后四个成员变量和上面说的CachingConfigurer中的方法一一对应。

  该类会自动装载用户通过实现CachingConfig接口或继承CachingConfigSupport定义的配置,并且使用这些配置。当然这是个抽象类,该类被相应拦截方式的配置类继承。例如基于代理proxy-based和基于AspectJ的配置类都继承了该类。

最后annotation包中还有三个比较重要的类。   AnnotationCacheOperationSource是以注解格式缓存元数据的CacheOperationSource接口实现类。该类读取Spring的Cacheable,CachePut和CacheEvict注解,并暴露Spring缓存基础下的相应缓存操作定义。

  该类中主要的方法就是这两个findCacheOpeartions(),分别从类和方法上获取到缓存操作集合。它们都是通过下面这个determineCacheOperations()方法解析。

  写法是函数式编程,即通过CacheAnnotationParser接口定义的两个方法来获取缓存操作集合,而该类默认定义的CacheAnnotationParser就是SpringCacheAnnotationParser,该类上面已经有介绍。

 

接下来是CachingConfigurationSelector类:

  该类是@EnableCaching中mode参数指定的拦截方式策略选择器。其通过静态代码块初始化时通过类加载获取是加载jsr107和jcache的相关类,并根据结果初始化两个布尔变量,这两个布尔变量用于导入时额外导入相关类。

  该选择器逻辑很简单,就是通过mode参数来判断加载哪些配置。

  我们主要来看看默认情况下,即mode=proxy时,必须导入的类是AutoProxyRegistrar和ProxyCachingConfiguration。ProxyCachingConfiguration我们放后面讲,先来简单讲一下AutoProxyRegistrar。该类根据当前BeanDefinitionRegistry来注册一个自动代理创建器,其基于一个@Enable*注解,该注解需要包含mode和proxyTargetClass参数,并正确设值。也就是说在SpringCache中配置的一些增强Advisor,将会通过该自动代理创建器自动生成。

接着往下看最后一个ProxyCachingConfiguration

  该类继承AbstractCachingConfiguration,其中定义了三个基础bean:

  按顺序看首先是CacheOperationSource,返回一个注解格式下的缓存操作源,该类上面有介绍;接着是CacheInterceptor,具体将在interceptor包中介绍,主要做的就是方法拦截缓存调用的过程了;最后才是BeanFactoryCacheOperationSourceAdvisor,我们可以看到,在声明bean时进行了setCacheOperationSource操作,该操作其实就是设置了切入点,在AnnotationCacheOperationSource中的缓存操作切入点,并且将增强器设置为该类中的cacheInterceptor,做的事情也就很明显了,即在这些缓存操作的切入点,进行SpringCache缓存增强。

config

  看到这里,应该能发现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

  接下来轮到interceptor包了,之前annotation和config中主要是一些配置操作,接下来我们看看SpringCache是如何在方法上做缓存的。

  老规矩,我们还是从接口开始看起。interceptor包中有7个接口,分别是BasicOperation、CacheErrorHandler、CacheOperationInvocationContext、CacheOperationInvoker、CacheOperationSource、CacheResolver和KeyGenerator。该包中的其他类,基本上也都是这几个接口的实现类。

首先是BasicOperation

  这个接口很简单,是所有缓存操作的顶层接口,其中只定义了一个getCacheNames()方法,来获取操作的缓存名称集合。我们来看他的实现类,先直接放出一个类继承关系图。

  我们可以看到,三大缓存操作都是继承了CacheOperation抽象类,实现了BasicOperation。

  该抽象类定义了一些通用的属性,并提供一个抽象的Builder建造器。我们往下看CacheableOperation:

  子类只是定义了其额外的一些属性,用于指定一个和condition相反功能的unless和用于缓存同步的sync。显然,其他两个子类也是一样,CacheEvictOperation额外定义了cacheWide和beforeInvocation,分别用于指定缓存清除范围和清除操作执行时机;CachePutOperation则额外定义了一个unless操作。

  现在应该能发现,前面说过的CacheAnnotationParser从类或方法上解析出来的就是CacheOperation的集合,也就是CacheableOperation、CachePutOperation和CacheEvictOperation三种缓存操作。

CacheErrorHandler

  该接口是处理缓存相关错误的策略接口。在大多数情况下,缓存实现抛出的所有异常都应该被直接抛出,但是在一些情况下,需要用特殊的方式去处理缓存实现抛出的异常。

  该接口定义了处理获取、设置、移除、清除缓存四种操作异常时的处理方法。也就是CachingConfigurer中的ErrorHandler,可以通过继承CachingConfigurerSupport并重写errorHandler()方法自定义。默认的实现类SimpleCacheErrorHandler不作额外处理,直接抛出异常。

CacheOperationInvocationContext

  这个接口比较重要,是缓存操作的调用上下文,其定义了获取缓存操作、目标对象、方法和参数的接口,也就是说在SpringCache的整个过程中,都是通过该上下文来获取这些相关信息。其在CacheInteceptor的抽象父类CacheAspectSupport中有实现CacheOperationContext:

  其通过构造方法CacheOperationContext(CacheOperationMetatdata metadata, Object[] args, Object target)传入CacheOperationMetatdata实例对象进行创建。而CacheOperationMetatdata是CacheAspectSupport中的内部静态类,其最终是通过CacheOperationMetadata(CacheOperation operation, Method method, Class<?> targetClass, KeyGenerator keyGenerator, CacheResolver cacheResolver)构造方法进行创建,也就是说其是缓存操作的第一层封装。

  那为什么不直接把CacheOperation封装到CacheOperationContext上下文里呢?其实CacheAspectSupport通过Map<CacheOperationCacheKey, CacheOperationMetadata> metadataCache变量将缓存操作元数据进行了缓存,避免每次调用缓存操作时,都需要进行生成key和获取CacheResolver/CacheManager的操作。

CacheOperationInvoker

  其中ThrowableWrapper是其自定义的一个异常包装类。

  该接口被注解为@FunctionalInterface,也就是函数式编程接口。其目前只作为MethodInvocation的一层抽象,也就是缓存注解的底层方法调用,主要功能还是对异常做了一个自定义操作。

CacheOperationSource

  该接口是之前annotation包中有提到的AnnotationCacheOperationSource的顶层接口,定义了一个获取缓存操作集合的方法。接下来我们回到AnnotationCacheOperationSource这个类,其实这个类并没有直接实现CacheOperationSource接口,而是继承了抽象类AbstractFallbackCacheOperationSource。

这里就需要重点讲一下AbstractFallbackCacheOperationSource了,因为接口定义的方式其实是在这个抽象类中实现的,AnnotationCacheOperationSource实现的两个方法(从类和方法中获取缓存操作)也是在该类中定义的抽象方法,委托给子类(AnnotationCacheOperationSource)实现。

  我们来看看这个接口定义方法的实现,该方法在获取缓存操作时,做了一步缓存,避免进行频繁且重复的操作,其key是通过method和targetClass获得的MethodClassKey。

  我们重点来看看红框中的这一步,类名中带有fallback字样,说明该类实现中提供了fallback策略,其实就是在这一步完成的,其顺序为1.specific target method;2. target class; 3. declaring method; 4. declaring class/interface.,保证尽可能获取到缓存操作。其中的findCacheOperations(xxx)方法即为委托给子类AnnotationCacheOperationSource实现的方法,这个在上文中有介绍了。

接下来是两个比较基础的接口CacheResovler

  该接口定义了从缓存操作调用上下文CacheOperationInvocationContext中真正解析出所操作的Cache的方法。

  我们来看一下该接口的实现类,首先也是一个抽象类AbstractCacheResolver,该类持有一个CacheManager成员,其通过CacheManager来进行缓存的管理。

  其主要的方法的实现如下,其中getCacheNames()方法委托给子类实现:

  在SpringCache中,给定默认的SimpleCacheResolver,其实现的getCacheNames()直接从给定的上下文中获取缓存操作中的缓存名称(可以自定义CacheResolver实现骚操作,例如使用参数做缓存名称等)。此外还提供了一个给定缓存名称的NamedCacheResolver实现,有兴趣可以自行了解。

KeyGenerator

  该接口为缓存key生成器接口,基于给定方法(缓存上下文中)和参数生成key。其也有默认实现SimpleKeyGenerator,通过参数生成简单的SimpleKey类作为key。可以实现该接口自定义key的生成策略,这个就比较常见了。

AbstractCacheInvoker

  接口介绍完了,顺便介绍一下抽象类。该包中总共有三个抽象类,上面介绍了其中之二——AbstractCacheResolver和AbstractFallbackCacheOperationSource,分别是抽象缓存处理器和抽象缓存操作源。接下来介绍一下AbstractCacheInvoker,该类是调用缓存操作的基础组件:

  该抽象类实现了四个最重要的缓存操作方法,并且没有定义抽象方法,因此是类似工具类的使用,只需要继承该抽象类,就可以对相关缓存进行操作。

  我们可以看到,该类持有一个CacheErrorHandler的,也就是说,在操作缓存时发生的异常都是在这一个部分进行处理的。

接下来是最最重要的,就是上面过程中多少有提一嘴的CacheAspectSupport和CacheInterceptor。 CacheAspectSupport

 

  该类中最重要的方法就是它的execute()方法,我们接下来重点看一看:

  这一步主要做的事尝试进行缓存操作,否则直接执行底层方法,那么接着看看红框中实际调用缓存操作的方法。方法太长,就不整个截图了,分部分截图:

  首先进入方法,先判断是否需要同步缓存操作,也就是@Cacheable的sync参数。那可能你会好奇,这部分也没有看到做同步操作呀?其实,同步操作在get(Object key, Callable valueLoader)中:

  没错,同步的操作是交给具体的缓存实现去做的,感兴趣的同学可以去看看RedisCache、EhCache等缓存是如何实现的。

  接着是无需同步的操作:

  那么这就是缓存操作的实际处理逻辑,其中的一些具体方法大家可以自行在源码中点进去看看逻辑,大概的逻辑通过方法名也能知道。

CacheInterceptor   这个类非常简单,是CacheAspectSupport的子类,对方法执行做了一层拦截,使其进入缓存操作逻辑。

剩下还有几个类,将分功能介绍。 首先是用于SpEL——Spring Expression Language表达式计算的CacheEvaluationContext、CacheExpressionRootObject、CacheOperationExpressionEvaluator和VariableNotAvailableException。

  我们先来看看这个上下文对象CacheEvaluationContext:

  该类实现的是SpEL表达式解析EvaluationContext接口。

  其中定义了以上这些方法,除此之外CacheEvaluationContext还定义了一个不可操作的缓存变量集合Set unavailableVariables,并且通过父类MethodBasedEvaluationContext实现了懒汉模式加载变量,避免了参数发现时不必要的类字节码的解析。

  CacheExpressionRootObject作为表达式解析过程中的根对象:

  接下来是核心类CacheOperationExpressionEvaluator:

  该类是解析SpEL表达式的工具类,作为可重用,线程安全的组件使用。其中最重要的方法就是红框中的这三个方法了,分别是计算表达式给定的缓存key、缓存条件和非缓存条件。其是通过SpEL计算的,本文不做额外的详细介绍。

  值得关注的是这三个方法调用的getExpression()方法,在这步中做了一个缓存操作,避免重复对不变的SpEL表达式进行计算求值。

  对表达式的求值过程,自然也是封装在CacheAspectSupport里的CacheOperationContext中。

然后是代理工厂bean,用于简化声明式缓存处理。这是使用标准AOP ProxyFactoryBean的一个便捷替代方式,并且包含一个CacheInterceptor定义。

  该类持有一个CacheInterceptor对象,并且默认匹配所有方法。其创建了一个主要拦截器对方法进行缓存拦截。

最后是CompositeCacheOperationSource,该类是支持复数缓存操作源的CacheOperationSource实现类,其持有一个CacheOperationSource数组对象,能够迭代缓存操作源数组获取缓存操作。还有一个缓存操作源切面CacheOperationSourcePointcut,用于BeanFactoryCacheOperationSourceAdvisor,就不多做介绍了。

support

  接下来是support包,顾名思义该包中的内容是对SpringCache这套逻辑起到一些辅助支持功能的类。

同样,我们先来看看它的抽象类AbstractCacheManager:

  该类实现了常见CacheManager方法。一些缓存管理器并不是直接实现CacheManager接口,而是通过继承该抽象类来完成,例如RedisCacheManager和EhCacheManager等。

AbstractValueAdaptingCache

  相信你看到这个类名也有一些想法了。没错,这个类也实现了Cache接口,其他多数缓存实现都是通过继承该抽线类完成,主要功能是在存储到底层存储之前兼容调整null值(和其他潜在的特殊值)。

接下来是一些SpringCache给出的CacheManager。

  分别是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接口的实现类。

concurrent

  最后这个包中的内容,其实是一个基于ConcurrentMap的本地缓存实现,可以在测试或简单的场景下使用。感兴趣的同学可以自行了解~

总结

  本文主要对spring-context包中缓存的部分源码进行了解读。具体的推荐大家看看源码,跟着作者的思路往下看看,会对平时使用的SpringCache有一个更深入的理解。

最新回复(0)