Java 的 @Cacheable 注解可以给方法添加缓存,但是最近在使用它的时候却发现对于某些方法不生效。
问题
对于某些方法 @Cacheable 注解不生效,多次调用不会命中缓存,例如:
@Cacheable("getSomeObject")
public SomeDTO getSomeObject(SomeVO someVO) {
// ...
}
但是对于另一些方法 @Cacheable 注解又是生效的。
排查
缩小问题范围
通过对比发现,对于参数为非 VO 对象的方法 @Cacheable 注解都是生效的:
@Cacheable("getSomeObject")
public SomeDTO getSomeObject(Long arg1, String arg2) {
// ...
}
而参数会影响缓存的 key,从而猜测问题与缓存的 key 相关。于是尝试对缓存不生效的方法手动指定 key:
@Cacheable("getSomeObject", key = "#someVO.Id")
public SomeDTO getSomeObject(SomeVO someVO) {
// ...
}
结果缓存成功生效了。
打开日志
通过配置项打开 @Cacheable 的日志:
logging.level.org.springframework.cache=TRACE
并在 logback.xml 中加上类似配置:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<logger name="org.springframework.cache" level="trace">
<appender-ref ref="STDOUT" />
</logger>
</configuration>
然后再次调用缓存不生效的方法就能看到类似日志:
2025-08-26 10:43:18.659 TRACE [http-nio-30020-exec-4] o.s.cache.interceptor.CacheInterceptor - No cache entry for key 'someVO(Id=..., name=..., ...)' in cache(s) [getSomeObject]
可以看到确实是在缓存中找不到 VO 对应的 key。
定位源码
通过日志中的文件路径 o.s.cache.interceptor.CacheInterceptor
找到处理 @Cacheable 注解的切面 org.springframework.cache.interceptor.CacheAspectSupport
:
@Nullable
private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
// ...
// Check if we have a cached item matching the conditions
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
// Collect puts from any @Cacheable miss, if no cached item is found
List<CachePutRequest> cachePutRequests = new ArrayList<>();
if (cacheHit == null) {
collectPutRequests(contexts.get(CacheableOperation.class), CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
}
// ...
// Process any collected put requests, either from @CachePut or a @Cacheable miss
for (CachePutRequest cachePutRequest : cachePutRequests) {
cachePutRequest.apply(cacheValue);
}
// ...
}
逻辑并不复杂,首先通过 findCachedItem()
查询缓存,若未命中则构造 cachePutRequests
,最后通过 cachePutRequest.apply()
将方法返回值写入缓存。
其中生成 key 的关键方法为:
public class SimpleKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return generateKey(params);
}
public static Object generateKey(Object... params) {
if (params.length == 0) {
return SimpleKey.EMPTY;
}
if (params.length == 1) {
Object param = params[0];
if (param != null && !param.getClass().isArray()) {
return param;
}
}
return new SimpleKey(params);
}
}
可以看到,当方法只有一个 VO 参数传入时,key 其实用的就是 VO 自身。
调试发现,findCachedItem()
方法中生成的 key 没有问题,和传入的 VO 一致。但是,最终 cachePutRequest.apply()
回写缓存时的 key 却变了,其中几个原本为 null 的字段被赋值了,从而导致写入缓存时的 key 和查询时的 key 不一致,才使得 @Cacheable 注解不生效。
此时,我仔细看了方法的实现,发现里面不起眼的地方包含了几处对 VO 的修改:
if (someVO.getSomeField() == null) {
someVO.setSomeField(...);
}
正是这几处修改导致了问题的产生。
总结
当方法仅传入一个参数时,@Cacheable 注解会使用该参数自身作为 key,如果在方法内部对参数进行了修改,会改变 @Cacheable 注解回写缓存值时的 key,使得写入缓存时的 key 和查询时的 key 不一致,从而导致 @Cacheable 注解不生效。应避免方法内部对参数进行修改,或手动指定 @Cacheable 注解的 key 来避免此问题。