Spring的@Cacheable取缓存默认实现

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,从而达到增加缓存功能的目的。

相关推荐
聆风吟º3 小时前
【Spring Boot 报错已解决】别让端口配置卡壳!Spring Boot “Binding to target failed” 报错解决思路
android·java·spring boot
我是华为OD~HR~栗栗呀3 小时前
华为od-22届考研-C++面经
java·前端·c++·python·华为od·华为·面试
码住懒羊羊3 小时前
【Linux】操作系统&进程概念
java·linux·redis
我是华为OD~HR~栗栗呀3 小时前
华为OD, 测试面经
java·c++·python·华为od·华为·面试
我是华为OD~HR~栗栗呀5 小时前
华为OD-23届-测试面经
java·前端·c++·python·华为od·华为·面试
yy7634966685 小时前
WPF 之 简单高效的Revit多语言支持方案
java·大数据·linux·服务器·wpf
我是华为OD~HR~栗栗呀5 小时前
华为od面经-23届-Java面经
java·c语言·c++·python·华为od·华为·面试
青云交10 小时前
Java 大视界 -- 基于 Java 的大数据机器学习模型在图像识别中的迁移学习与模型优化
java·大数据·迁移学习·图像识别·模型优化·deeplearning4j·机器学习模型
2501_9098008110 小时前
Java 集合框架之 Set 接口
java·set接口