Java’s @Cacheable annotation can add caching to methods, but recently I found that it does not work for certain methods.
Problem
For some methods, the @Cacheable annotation does not take effect, and multiple calls will not hit the cache, for example:
@Cacheable("getSomeObject")
public SomeDTO getSomeObject(SomeVO someVO) {
// ...
}
But for other methods, the @Cacheable annotation works fine.
Troubleshooting
Narrowing the problem scope
By comparison, it was found that for methods whose parameters are not VO objects, the @Cacheable annotation always works:
@Cacheable("getSomeObject")
public SomeDTO getSomeObject(Long arg1, String arg2) {
// ...
}
Since parameters affect the cache key, it was suspected that the problem was related to the cache key. So I tried manually specifying the key for the method where caching failed:
@Cacheable("getSomeObject", key = "#someVO.Id")
public SomeDTO getSomeObject(SomeVO someVO) {
// ...
}
As a result, caching worked successfully.
Enabling logs
Enable @Cacheable logs through configuration:
logging.level.org.springframework.cache=TRACE
And add a similar configuration in 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>
Then call the method that failed to cache again, and you will see similar logs:
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]
It can be seen that indeed no corresponding key for the VO was found in the cache.
Locating source code
From the file path in the logs o.s.cache.interceptor.CacheInterceptor
, I found the aspect handling the @Cacheable annotation 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);
}
// ...
}
The logic is not complicated. First, it queries the cache through findCachedItem()
. If not hit, it constructs cachePutRequests
. Finally, it writes the method return value into the cache through cachePutRequest.apply()
.
The key generation method is:
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);
}
}
It can be seen that when the method only has one VO parameter, the key used is actually the VO itself.
Debugging found that the key generated in findCachedItem()
was correct and consistent with the VO passed in. However, when finally writing back to the cache in cachePutRequest.apply()
, the key had changed — several fields that were originally null were assigned values. This caused the key used when writing to the cache to differ from the one used when querying, making the @Cacheable annotation ineffective.
At this point, I carefully examined the method implementation and found inconspicuous code where the VO was modified:
if (someVO.getSomeField() == null) {
someVO.setSomeField(...);
}
It was precisely these modifications that caused the problem.
Summary
When a method only takes one parameter, the @Cacheable annotation uses that parameter itself as the key. If the parameter is modified inside the method, it changes the key used when writing back to the cache, making the key inconsistent with the one used during lookup, thus causing the @Cacheable annotation to fail. To avoid this problem, you should either avoid modifying the parameter inside the method or manually specify the key for the @Cacheable annotation.