在设计自己的缓存框架之前,有必要了解一下
spring
的
cache
模块。在
spring3.1
及以后的版本中,提供了基于注解的缓存支持,但
spring
并没有对缓存进行具体实现(除了提供一个简单的基于
Map
的实现之外)。本框架就是在此基础上进行扩展。
1
、
spring
通过注解操作缓存的使用方法与示例:
因为篇幅有限,网上有很多现成的使用例子,这里不介绍
spring
缓存具体使用方法。
2
、
spring
通过注解操作缓存的原理:
spring
能通过注解操作缓存,是因为它采用天生就具有的强大的
AOP
机制,拦截业务方法。总体流程是:当一个方法上配置了
Cacheable
之类的注解后,这个方法被调用时,就会被一个叫
CacheInterceptor
的拦截器拦截,进入该类的
invoke()
方法中,如果当前
context
已经初始化完成,该方法紧接着会调用
execute()
。
execute()
方法中会读取原来被调用业务方法上的注解信息,通过这些信息进行相应的缓存操作,再跟据操作的结果决定是否调用原方法中的业务逻辑。这就是
spring
通过注解操作缓存的总体流程。
CacheInterceptor
是在
srping
上下文初始化的时候,通过配置文件中的
<cache:annotation-driven/>
标签注册到
context
中的。此标签的
cache
名称空间对应的处理类的是
org.springframework.cache.config. CacheNamespaceHandler
,其中对于
cache:annotation-driven
标签的解析类是
org.springframework.cache.config.AnnotationDrivenCacheBeanDefinitionParser(
在
CacheNamespaceHandler
类的代码中可以找到
)
,在这个类的
parse()
解析方法中跟据
annotation-driven
标签中的
model
属性决定是通过代理的方式实现
aop
还是通过
aspectj
的方式进行切面拦截
(
默认采用
proxy
代理方式
)
。对于代理方式,会调用
registerCacheAdvisor()
注册缓存
Advisor
。在这个方法中会注入一个
CacheInterceptor
类型的拦截器。这就实现了对业务方法的切面拦截。
在
registerCacheAdvisor()
方法中,还会调用一个叫
parseCacheResolution()
方法来注入一个缓存解析器
CacheResolver
,如果
<cache:annotation-driven />
标签中有
cache-resolver
的配置,就跟据配置注入一个
CacheResolver
,否则,就默认注入一个
SimpleCacheResolver
类型的实例。每个
CacheResolver
中包装了一个
CacheManager
,这个
CacheManager
可通过
<cache:annotation-driven/>
标签中
cacheManager
之类的配置进行指定,如果没有指定,会自动注入一个叫
cacheManager
的
bean
。
CacheInterceptor
在执行
execute()
的过程中会调用事先注入的
CacheResolver
实例的
resolveCaches()
方法解析业务方法中需要操作的缓存
Cache
列表(
resolveCaches()
方法内部实现是通过调用此
CacheResolver
实例中的
cacheManager
属性的
getCache()
方法获取
Cache
)。获取到需要操作的
Cache
列表后,遍历这个列表,然后都过调用
doGet(Cache cache)
或
doPut(Cachecache)
方法进行缓存操作。
doGet()
或
doPut()
方法在
CacheInterceptor
类的父类
AbstractCacheInvoker
中定义
(
注意这里的
Cache
列表,是
spring
包装了特定厂商缓存后的
Cache
对像,是
org.springframework.cache.Cache
类型的实例
)
。
总体上说,
CacheInterceptor
的
execute()
中对缓存的操作就是通过事先注一个
CacheResolver
和
CacheManager
实例,然后通过调用这个
CacheResolver
实例的
resolveCaches()
获得需要操作的
Cache
列表,再遍历列表,将每个
Cache
实例作为参数传入
doGet()
或
doPut()
来实现缓存读取。当然,还需要一些
Key
之类的参数,这个是由
keyGenerator
自动生成的。对于
keyGenerator
,这里不再介绍。看看源码就很容易理清思路。
3
、
spring
缓存模块的类简介:
spring
缓存模块对可用的
Cache
采用适配器模式进行了统一的封装。具体代码在
spring-context-xxx.jar
包中的
org.springframework.cache.Cache
接口,此接口声明一些储如
get(Object key)
,
put(Objectkey,Object value)
等缓存操作的统一
api
。在这个接口中,还声明了一个叫
getNativeCache()
的方法,返回它适配的具体的缓存实现
(
比如在集成
ehcache
时,这个接口实现类的实例调用
getNativeCache()
时会返回
net.sf.ehcache.Cache
类型的实例
)
。每一个
Cache
实例都通过名称加以区分,所以在
Cache
接口中,还声明了一个
getName()
返回此实例的名称。
spring
提供一个叫
ConcurrentMapCache
的基于
Map
的
Cache
实现类,作为它内置的本地缓存实现方案。
Cache
相关的类图如下:
图
:springcache
类图
所有被包装的
Cache
,都由
CacheManager
实例进行统一管理
(
在上文的原理分析中可以看到,在
<cache:annotation-driven />
标签的解析过程中会自动注入一个
CacheManager
实例
)
,他提供一个叫
getCache(String name)
的方法,跟据名称获得一个被包装的
Cache
。
在
Srping
的缓存模块中,
spring-context-xxx.jar
包中自带一个叫
ConcurrentMapCacheManagr
的简单实现类,它可以管理上文的提到的
ConcurrentMapCache
类。开发人员如果配置了此管理器,也就拥有了本地缓存的能力。另外,为了让应用支持同时存在多个
CacheManager
,
spring
提供了一个
CompositeCacheManager
的实现类,以组合设计模式的方式统一管理多个
CacheManager
实例。
CacheManager
部分类图如下:
上图中,
CacheManager
接口中只有两个方法
getCache(String)
和
getCacheNames()
,显然,这两个方法的作用就是跟据名称获得
Cache
实例以及获得所有被管理的缓存的名称列表。
上图中
CompositeCacheManager
与
ConcurrentMapCacheManager
类的作用在前文已经介绍过了。这里简单再说一下具体实现的方式:
CompositeCacheManager
中维护一个
CacheManager
列表,用户可以通过配置,把多个
CacheManager
配置到这个列表中,使得应用可以同时管理多个缓存管理器。这个类对于
getCache(String)
方法实现是通过遍历这个列表,匹配出
name
相同的
Cache
实例并返回。这个类还可以通过配置指定一个
boolean
的
fallbackToNoOpCache
标志属性,它的作用就是,当通过
getCache(string)
获取不到
Cache
实例时,是否不进行任何缓存操作。在默认情况或者
fallbackToNoOpCache
值为
false
时,在通过
getCache(string)
获取不到
Cache
实例时,业务层上可能会抛出运行时异常(比如提示“找不到
XXX
名称的
Cache
”)。但如果为
true
时,这时候不进行任何缓存操作也不抛异常,这种场景主要用于在不具备缓存条件的时候,在不改代码的情况下,禁用缓存。
spring
对于这种机制的实现,是通过上图中没有画出的两个特殊的类来实现的
: NoOpCacheManager
和
NoOpCache
类。这两个类分别是
CacheManager
类
Cache
类的子类,表示不进行任何缓存操作。
在
ConcurrentMapCacheManager
内置的缓存管理器中,可以通过配置指定一个
boolean
类型的
allowNullValues
属性,用于指定缓存中能否保存
null
值。因为该管理器是用于
spring
通过
Map
实现的内置缓存的管理器实现。在对应的
Cache
实现类
ConcurrentMapCache
中可以看到,它是通过
ConcurrentHashMap
保存所有建值对数据的。然而
ConcurrentHashMap
并不支持保存
null
值,直接在
ConcurrentHashMap
中
put
空值会抛空指针异常。然而,往缓存中保存空值有时候确实也是有必要的。比如,在从数据库查询某项数据时,因数据不存在,返回了
null
。这时候如果不把这个
null
值保存到缓存中去,那么下次再作查询时,缓存就无法命中,从而导致重复查询数据库,这就是所谓的缓存穿透。为了防止这种情况,这就要对
null
值做一个包装,把它包装成一个非
null
的而且在业务上认为是无效的对像保存到缓存里面。
ConcurrentMapCacheManager
中的
allowNullValues
就是用于指定能否缓存
null
,如果此值为
true
,将把自动把
null
包装成无效对像缓存起来,如果为
false
,那么需要开发人员自行从业务层上保证不往缓存中保存
null
数据。
在上面类图中,最重要的是类就是
AbstractCacheManager
抽象类了,它只对
CacheManager
提供了一个简单实现,并开放了一些比如
loadCaches()
之类的抽象方法,对于这个类的具体实现,由需要集成的具体的缓存厂商来实现。
AbstractCacheManager
类除了实现
CacheManager
接口之外,还实现了
srping
框架的
InitializingBean
接口,这使得此类型的
bean
在被
spring
初始化的时候,会自动调用
afterPropertiesSet()
方法,这个方法会调用此类的
initializeCaches()
的方法进行初始化。它的具体逻辑是通过用调用抽象方法
loadCaches()
获取它能管理的所有
Cache
实列列表,并遍历它,把它都添加到此实例的
cacheMap
属性集合中,同时把所有的
name
都统一加入到
cacheNames
集合中,以便方法
getCacheNames()
可以返回所有
cache
的名称集合。
抽象方法
loadCaches()
的作用是从具体的缓存实现中加载所有它能管理的
Cache
(比如调用
EhCache
相关的
api
加载他所有的
Cache
)。
在这个类中,最重要的方法就是
getCache(String)
,表示通过缓存名称获取缓存实例。它的实现逻辑是,先从
cacheMap
集合以
name
作为
key
查找
cache
,如果找不到,就调用
getMissingCache()
方法获取。这个
getMissingCache()
意图为
:
返回
loadCaches()
原来没有加载到的
Cache(
这里有一次重新加载的机会
)
。当然,
AbstractCacheManager
这个类并没有对这个方法做特别的实现,只是简单返回了
null
,具体的实现类可以覆盖这个方法。
这个类中还有其它的诸如
addCache()
之类的方法,就不介绍了。下面看看
spring
自带的
ehcache
实现。
4
、
spring cache
内置的
ehcache
支持方案:
springcache
模块通过提供
ehcache
相关的几个实现类对
ehcache
进行支持,采用的是适配器设计模式,相关类在
srping-cotext-suppoert-xxx.jar
包中如下类图:
图中可看出,最重要的两个类就是
EhCacheCache
和
EhCacheCacheManager
,分别是
Cache
和
AbstractCacheManager
的子类实现类。这两个类中,对于父类抽象的方法的实现,都是委托它类部的
Ehcache
及
net.sf.ehcache.CacheManager
通过调用
ehcache
相关
api
来完成。
然而,因为
ehcache
的配置参数比较多,为了方便开发人员简洁的配置
EhCacheCache
和
EhCacheCacheManager
实例。
spring
提供了两个对应的工厂类
:EhCacheFactoryBean
和
EhCacheManagerFactoryBean
。开发人员只要在配置文件中配置这两个类型的
bean
,就可以很方便的完成与
ehcache
的集成
。
4
、集成
memcache
方案提示:
spring
并没有提供对
memcache
的直接支持,需要我们自己实现,通过以上分析,可以想象,集成
memcache
的主要思路就是实现自己的
CacheManager
及
Cache
类。限于篇幅,在后面的文章中再给出具体的实现方案。
目前
JAD-CACHE
项目已在开源中国码云平台上开源,地址:
https://git.oschina.net/457049726/jad-cache
想了解更多信息的同学们可以扫以下二维码关注我的微信公众号: