跳至内容
返回

不要修改 Java 中带有 @Cacheable 注解的方法参数

发布于:  at  12:38 下午

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 来避免此问题。



上一篇
为什么用 iptables 封禁 k8s NodePort 不生效
下一篇
jq 的精度问题