You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让Spring @Cacheable缓存作用域限定在@Scope("prototype")实例范围内

实现Prototype Bean的实例级缓存隔离

这个问题我之前也遇到过,原型Bean的Spring Cache默认共享缓存空间的坑确实容易踩。下面给你两种实用的解决方案,都能实现实例级的缓存隔离:


方法一:修改@Cacheable的Key,加入实例唯一标识

这是最简单的方案,不需要额外编写组件,只需要调整缓存Key的生成规则,把当前实例的唯一属性(比如你的sessionToken或uri)加入到Key中,这样不同实例的同参数请求就会生成不同的缓存Key,自然实现隔离。

修改后的代码示例

@Component
@Scope("prototype")
public class ExternalAppConnection {
    private final URI uri;
    private final String sessionToken;

    public ExternalAppConnection(final String uri, final String sessionToken) throws URISyntaxException {
        this.uri = new URI(uri);
        this.sessionToken = sessionToken;
    }

    // 给sessionToken添加公共getter,供SpEL表达式访问
    public String getSessionToken() {
        return sessionToken;
    }

    // 调整@Cacheable的key,加入当前实例的sessionToken
    @Cacheable(key = "{#appId, #root.target.sessionToken}")
    public AppInfoResponseDto getAppInfo(Integer appId) {
        String url = buildUrl("/api/appinfo/" + appId); // 注意这里补了个斜杠,避免URL拼接错误
        return exchangeRequestForObject(url, HttpMethod.GET, AppInfoResponseDto.class);
    }

    // 省略已实现的buildUrl和exchangeRequestForObject方法
}

原理说明

  • #root.target在SpEL表达式中代表当前调用方法的Bean实例,通过它可以访问实例的sessionToken属性(前提是有对应的getter方法)。
  • 生成的缓存Key会是[appId, sessionToken]的组合,不同实例的同appId请求会生成完全不同的Key,彻底避免缓存串用。

方法二:自定义CacheResolver,为每个实例分配独立缓存空间

如果你的类中有多个需要缓存的方法,或者希望每个实例拥有完全独立的缓存容器,这种方案更合适。通过自定义CacheResolver,为每个原型Bean实例创建专属的缓存空间。

步骤1:实现自定义CacheResolver

@Component
public class InstanceSpecificCacheResolver implements CacheResolver {
    private final CacheManager cacheManager;

    public InstanceSpecificCacheResolver(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    @Override
    public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
        // 确保当前目标对象是ExternalAppConnection实例
        if (!(context.getTarget() instanceof ExternalAppConnection)) {
            throw new IllegalArgumentException("该缓存解析器仅支持ExternalAppConnection实例");
        }

        ExternalAppConnection connection = (ExternalAppConnection) context.getTarget();
        // 用sessionToken拼接唯一缓存名,保证每个实例的缓存独立
        String uniqueCacheName = String.format("appInfo_cache_%s", connection.getSessionToken());

        // 动态创建缓存(ConcurrentMapCacheManager默认支持动态创建,其他缓存管理器需按需调整)
        Cache cache = cacheManager.getCache(uniqueCacheName);
        if (cache == null) {
            if (cacheManager instanceof ConcurrentMapCacheManager) {
                ((ConcurrentMapCacheManager) cacheManager).createCache(uniqueCacheName);
                cache = cacheManager.getCache(uniqueCacheName);
            } else {
                throw new IllegalStateException("当前CacheManager不支持动态创建缓存");
            }
        }

        return Collections.singletonList(cache);
    }
}

步骤2:在@Cacheable中指定自定义CacheResolver

@Component
@Scope("prototype")
public class ExternalAppConnection {
    // 省略属性、构造方法和getter

    @Cacheable(cacheResolver = "instanceSpecificCacheResolver")
    public AppInfoResponseDto getAppInfo(Integer appId) {
        // 方法体不变
    }
}

原理说明

  • 每次调用缓存方法时,CacheResolver会获取当前的Bean实例,生成专属的缓存名,相当于给每个实例分配了独立的缓存容器。
  • 这种方式的优势是:多个缓存方法可以复用同一个CacheResolver,不需要逐个调整Key规则。

额外注意事项

  1. 唯一标识的选择:如果sessionToken存在重复风险,可以结合uri一起作为标识(比如key = "{#appId, #root.target.uri, #root.target.sessionToken}"),确保Key的绝对唯一性。
  2. 缓存过期策略:原型Bean销毁时Spring不会自动清理对应的缓存,建议给缓存设置合理的过期时间(比如使用Caffeine缓存时,可通过expireAfterWrite配置),避免内存泄漏。
  3. 缓存管理器兼容性:自定义CacheResolver的动态创建缓存逻辑,需要对应CacheManager支持(比如ConcurrentMapCacheManager默认支持,Redis缓存管理器需提前配置好缓存名前缀或动态创建规则)。

内容的提问来源于stack exchange,提问作者CDelaney

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.01 02:27:45