1 概述
在Spring体系中,要实现缓存非常容易,只需要在需要缓存的方法上加上@Cacheable注解即可。这是因为提供了缓存的默认实现,但这个实现不支持缓存过期,也不支持设置缓存总量的大小。需要了解一下这个实现的原理,搞清楚为什么有这些限制,是否有方法能够克服这些局限。
2 实现原理
2.1 取缓存过程
在Spring中,当一个方法加上@Cacheable注解的时候,若这个方法被调用,会被CacheInterceptor拦截器进行拦截,在拦截器里增加先取缓存的操作,只有当缓存取不到的时候,才会访问原方法的逻辑。主要的逻辑在CacheInterceptor的父类CacheAspectSupport实现。
java
// 代码来源:org.springframework.cache.interceptor.CacheInterceptor
public Object invoke(final MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
CacheOperationInvoker aopAllianceInvoker = () -> {
try {
return invocation.proceed();
}
catch (Throwable ex) {
throw new CacheOperationInvoker.ThrowableWrapper(ex);
}
};
Object target = invocation.getThis();
Assert.state(target != null, "Target must not be null");
try {
// 1. 执行execute()方法进行取缓存操作,CacheInterceptor继承于CacheAspectSupport,execute()方法由CacheAspectSupport提供
return execute(aopAllianceInvoker, target, method, invocation.getArguments());
}
catch (CacheOperationInvoker.ThrowableWrapper th) {
throw th.getOriginal();
}
}
// 代码来源:org.springframework.cache.interceptor.CacheAspectSupport
private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
// 省略部分代码
// 2. 从缓存中取数据
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
List<CachePutRequest> cachePutRequests = new ArrayList<>(1);
if (cacheHit == null) {
collectPutRequests(contexts.get(CacheableOperation.class),
CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
}
Object cacheValue;
Object returnValue;
if (cacheHit != null && !hasCachePut(contexts)) {
cacheValue = cacheHit.get();
returnValue = wrapCacheValue(method, cacheValue);
}
else {
returnValue = invokeOperation(invoker);
cacheValue = unwrapReturnValue(returnValue);
}
// 省略部分代码
return returnValue;
}
private Cache.ValueWrapper findCachedItem(Collection<CacheOperationContext> contexts) {
Object result = CacheOperationExpressionEvaluator.NO_RESULT;
for (CacheOperationContext context : contexts) {
if (isConditionPassing(context, result)) {
// 3. 构造缓存key
Object key = generateKey(context, result);
// 4. 根据key取缓存对象
Cache.ValueWrapper cached = findInCaches(context, key);
if (cached != null) {
return cached;
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No cache entry for key '" + key + "' in cache(s) " + context.getCacheNames());
}
}
}
}
return null;
}
private Cache.ValueWrapper findInCaches(CacheOperationContext context, Object key) {
// 5. 遍历缓存,默认的Cache为org.springframework.cache.concurrent.ConcurrentMapCache
for (Cache cache : context.getCaches()) {
// 6. 从缓存中取数据,CacheAspectSupport继承于AbstractCacheInvoker,doGet由AbstractCacheInvoker提供
Cache.ValueWrapper wrapper = doGet(cache, key);
if (wrapper != null) {
if (logger.isTraceEnabled()) {
logger.trace("Cache entry for key '" + key + "' found in cache '" + cache.getName() + "'");
}
return wrapper;
}
}
return null;
}
// 代码来源:org.springframework.cache.interceptor.AbstractCacheInvoker
protected Cache.ValueWrapper doGet(Cache cache, Object key) {
try {
// 7. 根据key取缓存数据
return cache.get(key);
}
catch (RuntimeException ex) {
getErrorHandler().handleCacheGetError(ex, cache, key);
return null;
}
}
从上面代码可以看到,这中间没有留可以扩展的地方,数据缓存在第5步取到的ConcurrentMapCache,该类实现了org.springframework.cache.Cache接口,提供get()、put()、evict()等标准的缓存接口。那这个ConcurrentMapCache是如何来的呢?
2.2 Cache的创建
在默认的实现中,ConcurrentMapCache是在ConcurrentMapCacheManager中创建的,如下面代码:
java
public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware {
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);
@Override
public Cache getCache(String name) {
Cache cache = this.cacheMap.get(name);
if (cache == null && this.dynamic) {
cache = this.cacheMap.computeIfAbsent(name, this::createConcurrentMapCache);
}
return cache;
}
protected Cache createConcurrentMapCache(String name) {
SerializationDelegate actualSerialization = (isStoreByValue() ? this.serialization : null);
return new ConcurrentMapCache(name, new ConcurrentHashMap<>(256), isAllowNullValues(), actualSerialization);
}
// 省略其它代码
}
ConcurrentMapCacheManager维护了一个ConcurrentMap,key为缓存的名称,value是ConcurrentMapCache。
在创建ConcurrentMapCache的时候,直接使用的是new ConcurrentMapCache()的方式,这代表着无法通过更换ConcurrentMapCache的实现,这里并没有留扩展的地方。
在创建ConcurrentMapCache的时候,直接new ConcurrentHashMap<>(256),说明实际的缓存数据是存储在ConcurrentHashMap里,ConcurrentHashMap属于无界Map(最大2^30),ConcurrentHashMap也没有为数据设置过期时间的功能。这对于缓存来说,比较危险,无法控制其大小,直到内存不够用而OOM。
2.3 CacheManager的创建
CacheManager后面的部分都无法扩展,则看看CacheManager是从哪里来的。CacheAspectSupport实现了SmartInitializingSingleton接口,Spring对实现了SmartInitializingSingleton接口的类,在创建bean完成之后会触发调用该接口的实现afterSingletonsInstantiated()。
java
// 源码位置:org.springframework.cache.interceptor.CacheAspectSupport
public void afterSingletonsInstantiated() {
if (getCacheResolver() == null) {
Assert.state(this.beanFactory != null, "CacheResolver or BeanFactory must be set on cache aspect");
try {
// 1. 设置CacheManager,这个CacheManager是通过beanFactory取得的,说明前面已经进行bean注册
setCacheManager(this.beanFactory.getBean(CacheManager.class));
}
// 省略部分代码
}
this.initialized = true;
}
public void setCacheManager(CacheManager cacheManager) {
// 2. CacheManager设置到了SimpleCacheResolver里面
this.cacheResolver = SingletonSupplier.of(new SimpleCacheResolver(cacheManager));
}
// 源码位置:org.springframework.cache.interceptor.CacheAspectSupport.CacheOperationContext
public CacheOperationContext(CacheOperationMetadata metadata, Object[] args, Object target) {
this.metadata = metadata;
this.args = extractArgs(metadata.method, args);
this.target = target;
// 3. 在CacheAspectSupport中构建CacheOperationContext的时候,使用了cacheResolver获取caches
// 注意上面获取缓存的时候,是从context取出来的,此处就是context里caches的初始化
this.caches = CacheAspectSupport.this.getCaches(this, metadata.cacheResolver);
this.cacheNames = prepareCacheNames(this.caches);
}
// 源码位置:org.springframework.cache.interceptor.CacheAspectSupport
protected Collection<? extends Cache> getCaches(CacheOperationInvocationContext<CacheOperation> context, CacheResolver cacheResolver) {
// 4. 通过CacheResolver获取到caches
Collection<? extends Cache> caches = cacheResolver.resolveCaches(context);
if (caches.isEmpty()) {
throw new IllegalStateException("No cache could be resolved for '" +
context.getOperation() + "' using resolver '" + cacheResolver +
"'. At least one cache should be provided per cache operation.");
}
return caches;
}
// 源码位置:org.springframework.cache.interceptor.AbstractCacheResolver
// SimpleCacheResolver继承于AbstractCacheResolver,上面代码已经通过SimpleCacheResolver设置CacheManager
public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
Collection<String> cacheNames = getCacheNames(context);
if (cacheNames == null) {
return Collections.emptyList();
}
Collection<Cache> result = new ArrayList<>(cacheNames.size());
for (String cacheName : cacheNames) {
// 5. 由CacheManager提供Cache对象
Cache cache = getCacheManager().getCache(cacheName);
if (cache == null) {
throw new IllegalArgumentException("Cannot find cache named '" +
cacheName + "' for " + context.getOperation());
}
result.add(cache);
}
return result;
}
从上面代码可以看出,afterSingletonsInstantiated()实现的时候,设置了一个已经注册的CacheManager实例,由这个实例给CacheOperationContext提供Cache对象。那这个CacheManager实例是在哪里注入的呢?
java
// 源码位置:org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class SimpleCacheConfiguration {
@Bean
ConcurrentMapCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCustomizers cacheManagerCustomizers) {
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
List<String> cacheNames = cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
cacheManager.setCacheNames(cacheNames);
}
return cacheManagerCustomizers.customize(cacheManager);
}
}
从上面代码可以看到,默认的ConcurrentMapCacheManager是在SimpleCacheConfiguration注册成bean的,由它来提供缓存的管理。
既然这个bean是注入的,上面还有@ConditionalOnMissingBean(CacheManager.class),那就代表着可以通过自定义一个CacheManager来替换掉默认的实现。只替换CacheManager的实现,不需要修改@Cacheable的相关实现,就能够增加缓存的功能,如缓存过期时间、缓存大小限制等。
3 小结
Spring提供的@Cacheable的默认实现不支持缓存过期时间、缓存大小限制,是因为用了ConcurrentMapCacheManager进行管理缓存,该manager是采用ConcurrentHashMap来存储缓存数据的,无法设置缓存过期时间,也不能设置容量大小。但Spring对ConcurrentMapCacheManager采用的是注入的方式,可以通过自定义CacheManager来更换默认的ConcurrentMapCacheManager,从而达到增加缓存功能的目的。