Skip to content

Commit b510bc3

Browse files
committed
Reuse generated key for get+put within same cacheable operation
Closes gh-31789
1 parent b6364e3 commit b510bc3

File tree

2 files changed

+46
-6
lines changed

2 files changed

+46
-6
lines changed

spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,7 @@ private void performCacheEvicts(List<CacheOperationContext> contexts, @Nullable
616616
for (CacheOperationContext context : contexts) {
617617
CacheEvictOperation operation = (CacheEvictOperation) context.metadata.operation;
618618
if (isConditionPassing(context, result)) {
619-
Object key = null;
619+
Object key = context.getGeneratedKey();
620620
for (Cache cache : context.getCaches()) {
621621
if (operation.isCacheWide()) {
622622
logInvalidating(context, operation, null);
@@ -673,7 +673,7 @@ private Object generateKey(CacheOperationContext context, @Nullable Object resul
673673
throw new IllegalArgumentException("""
674674
Null key returned for cache operation [%s]. If you are using named parameters, \
675675
ensure that the compiler uses the '-parameters' flag."""
676-
.formatted(context.metadata.operation));
676+
.formatted(context.metadata.operation));
677677
}
678678
if (logger.isTraceEnabled()) {
679679
logger.trace("Computed cache key '" + key + "' for operation " + context.metadata.operation);
@@ -798,6 +798,9 @@ protected class CacheOperationContext implements CacheOperationInvocationContext
798798
@Nullable
799799
private Boolean conditionPassing;
800800

801+
@Nullable
802+
private Object key;
803+
801804
public CacheOperationContext(CacheOperationMetadata metadata, Object[] args, Object target) {
802805
this.metadata = metadata;
803806
this.args = extractArgs(metadata.method, args);
@@ -873,9 +876,17 @@ else if (this.metadata.operation instanceof CachePutOperation cachePutOperation)
873876
protected Object generateKey(@Nullable Object result) {
874877
if (StringUtils.hasText(this.metadata.operation.getKey())) {
875878
EvaluationContext evaluationContext = createEvaluationContext(result);
876-
return evaluator.key(this.metadata.operation.getKey(), this.metadata.methodKey, evaluationContext);
879+
this.key = evaluator.key(this.metadata.operation.getKey(), this.metadata.methodKey, evaluationContext);
880+
}
881+
else {
882+
this.key = this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args);
877883
}
878-
return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args);
884+
return this.key;
885+
}
886+
887+
@Nullable
888+
protected Object getGeneratedKey() {
889+
return this.key;
879890
}
880891

881892
private EvaluationContext createEvaluationContext(@Nullable Object result) {
@@ -969,7 +980,10 @@ public Object apply(@Nullable Object result) {
969980

970981
public void performCachePut(@Nullable Object value) {
971982
if (this.context.canPutToCache(value)) {
972-
Object key = generateKey(this.context, value);
983+
Object key = this.context.getGeneratedKey();
984+
if (key == null) {
985+
key = generateKey(this.context, value);
986+
}
973987
if (logger.isTraceEnabled()) {
974988
logger.trace("Creating cache entry for key '" + key + "' in cache(s) " +
975989
this.context.getCacheNames());

spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,11 +16,16 @@
1616

1717
package org.springframework.cache.config;
1818

19+
import java.util.ArrayList;
20+
import java.util.Collection;
21+
import java.util.List;
22+
1923
import org.junit.jupiter.api.Test;
2024

2125
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
2226
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
2327
import org.springframework.cache.CacheManager;
28+
import org.springframework.cache.annotation.Cacheable;
2429
import org.springframework.cache.annotation.CachingConfigurer;
2530
import org.springframework.cache.annotation.EnableCaching;
2631
import org.springframework.cache.interceptor.CacheErrorHandler;
@@ -139,6 +144,17 @@ void bothSetOnlyResolverIsUsed() {
139144
context.close();
140145
}
141146

147+
@Test
148+
void mutableKey() {
149+
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
150+
ctx.register(EnableCachingConfig.class, ServiceWithMutableKey.class);
151+
ctx.refresh();
152+
153+
ServiceWithMutableKey service = ctx.getBean(ServiceWithMutableKey.class);
154+
String result = service.find(new ArrayList<>(List.of("id")));
155+
assertThat(service.find(new ArrayList<>(List.of("id")))).isSameAs(result);
156+
}
157+
142158

143159
@Configuration
144160
@EnableCaching
@@ -277,4 +293,14 @@ public CacheResolver cacheResolver() {
277293
}
278294
}
279295

296+
297+
static class ServiceWithMutableKey {
298+
299+
@Cacheable(value = "testCache", keyGenerator = "customKeyGenerator")
300+
public String find(Collection<String> id) {
301+
id.add("other");
302+
return id.toString();
303+
}
304+
}
305+
280306
}

0 commit comments

Comments
 (0)