如何让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规则。
额外注意事项
- 唯一标识的选择:如果
sessionToken存在重复风险,可以结合uri一起作为标识(比如key = "{#appId, #root.target.uri, #root.target.sessionToken}"),确保Key的绝对唯一性。 - 缓存过期策略:原型Bean销毁时Spring不会自动清理对应的缓存,建议给缓存设置合理的过期时间(比如使用Caffeine缓存时,可通过
expireAfterWrite配置),避免内存泄漏。 - 缓存管理器兼容性:自定义
CacheResolver的动态创建缓存逻辑,需要对应CacheManager支持(比如ConcurrentMapCacheManager默认支持,Redis缓存管理器需提前配置好缓存名前缀或动态创建规则)。
内容的提问来源于stack exchange,提问作者CDelaney
相关产品推荐
相关产品推荐

