Skip to content

Commit eea230f

Browse files
committed
Allow @cACHEpUT 'key' SpEL to refer to #result
Allow the @cACHEpUT 'key' SpEL to refer to the result of the method call via a '#result' variable. This change is particularly useful when working with JPA entities that have generated @id values since the ID will often not be available until the entity has been saved. Issue: SPR-10664
1 parent f75d4e1 commit eea230f

File tree

11 files changed

+188
-15
lines changed

11 files changed

+188
-15
lines changed

spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,11 @@ public Object multiConditionalCacheAndEvict(Object arg1) {
163163
public Object multiUpdate(Object arg1) {
164164
return arg1;
165165
}
166+
167+
@Override
168+
@CachePut(value="primary", key="#result.id")
169+
public TestEntity putRefersToResult(TestEntity arg1) {
170+
arg1.setId(Long.MIN_VALUE);
171+
return arg1;
172+
}
166173
}

spring-aspects/src/test/java/org/springframework/cache/config/CacheableService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,6 @@ public interface CacheableService<T> {
7070
T multiConditionalCacheAndEvict(Object arg1);
7171

7272
T multiUpdate(Object arg1);
73+
74+
TestEntity putRefersToResult(TestEntity arg1);
7375
}

spring-aspects/src/test/java/org/springframework/cache/config/DefaultCacheableService.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,4 +171,11 @@ public Long multiConditionalCacheAndEvict(Object arg1) {
171171
public Long multiUpdate(Object arg1) {
172172
return Long.valueOf(arg1.toString());
173173
}
174+
175+
@Override
176+
@CachePut(value="primary", key="#result.id")
177+
public TestEntity putRefersToResult(TestEntity arg1) {
178+
arg1.setId(Long.MIN_VALUE);
179+
return arg1;
180+
}
174181
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2002-2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cache.config;
18+
19+
import org.springframework.util.ObjectUtils;
20+
21+
/**
22+
* Simple test entity for use with caching tests.
23+
*
24+
* @author Michael Plšd
25+
*/
26+
public class TestEntity {
27+
28+
private Long id;
29+
30+
public Long getId() {
31+
return this.id;
32+
}
33+
34+
public void setId(Long id) {
35+
this.id = id;
36+
}
37+
38+
@Override
39+
public int hashCode() {
40+
return ObjectUtils.nullSafeHashCode(this.id);
41+
}
42+
43+
@Override
44+
public boolean equals(Object obj) {
45+
if (obj == this) {
46+
return true;
47+
}
48+
if (obj == null) {
49+
return false;
50+
}
51+
if (obj instanceof TestEntity) {
52+
return ObjectUtils.nullSafeEquals(this.id, ((TestEntity) obj).id);
53+
}
54+
return false;
55+
}
56+
}

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

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -148,15 +148,15 @@ private Object execute(Invoker invoker, CacheOperationContexts contexts) {
148148
// Process any early evictions
149149
processCacheEvicts(contexts.get(CacheEvictOperation.class), true, ExpressionEvaluator.NO_RESULT);
150150

151-
// Collect puts, either explicit @CachePuts or from a @Cachable miss
151+
// Collect puts from any @Cachable miss
152152
List<CachePutRequest> cachePutRequests = new ArrayList<CachePutRequest>();
153-
collectPutRequests(contexts.get(CachePutOperation.class), cachePutRequests, false);
154-
collectPutRequests(contexts.get(CacheableOperation.class), cachePutRequests, true);
153+
collectPutRequests(contexts.get(CacheableOperation.class),
154+
ExpressionEvaluator.NO_RESULT, cachePutRequests, true);
155155

156156
ValueWrapper result = null;
157157

158158
// We only attempt to get a cached result if there are no put requests
159-
if(cachePutRequests.isEmpty()) {
159+
if(cachePutRequests.isEmpty() && contexts.get(CachePutOperation.class).isEmpty()) {
160160
result = findCachedResult(contexts.get(CacheableOperation.class));
161161
}
162162

@@ -165,6 +165,10 @@ private Object execute(Invoker invoker, CacheOperationContexts contexts) {
165165
result = new SimpleValueWrapper(invoker.invoke());
166166
}
167167

168+
// Collect any explicit @CachePuts
169+
collectPutRequests(contexts.get(CachePutOperation.class), result.get(),
170+
cachePutRequests, false);
171+
168172
// Process any collected put requests, either from @CachePut or a @Cacheable miss
169173
for (CachePutRequest cachePutRequest : cachePutRequests) {
170174
cachePutRequest.apply(result.get());
@@ -182,21 +186,21 @@ private void processCacheEvicts(Collection<CacheOperationContext> contexts,
182186
CacheEvictOperation operation = (CacheEvictOperation) context.operation;
183187
if (beforeInvocation == operation.isBeforeInvocation() &&
184188
isConditionPassing(context, result)) {
185-
performCacheEvict(context, operation);
189+
performCacheEvict(context, operation, result);
186190
}
187191
}
188192
}
189193

190194
private void performCacheEvict(CacheOperationContext context,
191-
CacheEvictOperation operation) {
195+
CacheEvictOperation operation, Object result) {
192196
Object key = null;
193197
for (Cache cache : context.getCaches()) {
194198
if (operation.isCacheWide()) {
195199
logInvalidating(context, operation, null);
196200
cache.clear();
197201
} else {
198202
if(key == null) {
199-
key = context.generateKey();
203+
key = context.generateKey(result);
200204
}
201205
logInvalidating(context, operation, key);
202206
cache.evict(key);
@@ -213,10 +217,11 @@ private void logInvalidating(CacheOperationContext context,
213217
}
214218
}
215219

216-
private void collectPutRequests(Collection<CacheOperationContext> contexts, Collection<CachePutRequest> putRequests, boolean whenNotInCache) {
220+
private void collectPutRequests(Collection<CacheOperationContext> contexts,
221+
Object result, Collection<CachePutRequest> putRequests, boolean whenNotInCache) {
217222
for (CacheOperationContext context : contexts) {
218-
if (isConditionPassing(context, ExpressionEvaluator.NO_RESULT)) {
219-
Object key = generateKey(context);
223+
if (isConditionPassing(context, result)) {
224+
Object key = generateKey(context, result);
220225
if (!whenNotInCache || findInCaches(context, key) == null) {
221226
putRequests.add(new CachePutRequest(context, key));
222227
}
@@ -229,7 +234,8 @@ private Cache.ValueWrapper findCachedResult(Collection<CacheOperationContext> co
229234
for (CacheOperationContext context : contexts) {
230235
if (isConditionPassing(context, ExpressionEvaluator.NO_RESULT)) {
231236
if(result == null) {
232-
result = findInCaches(context, generateKey(context));
237+
result = findInCaches(context,
238+
generateKey(context, ExpressionEvaluator.NO_RESULT));
233239
}
234240
}
235241
}
@@ -254,8 +260,8 @@ private boolean isConditionPassing(CacheOperationContext context, Object result)
254260
return passing;
255261
}
256262

257-
private Object generateKey(CacheOperationContext context) {
258-
Object key = context.generateKey();
263+
private Object generateKey(CacheOperationContext context, Object result) {
264+
Object key = context.generateKey(result);
259265
Assert.notNull(key, "Null key returned for cache operation (maybe you "
260266
+ "are using named params on classes without debug info?) "
261267
+ context.operation);
@@ -393,9 +399,9 @@ else if (this.operation instanceof CachePutOperation) {
393399
* Computes the key for the given caching operation.
394400
* @return generated key (null if none can be generated)
395401
*/
396-
protected Object generateKey() {
402+
protected Object generateKey(Object result) {
397403
if (StringUtils.hasText(this.operation.getKey())) {
398-
EvaluationContext evaluationContext = createEvaluationContext(ExpressionEvaluator.NO_RESULT);
404+
EvaluationContext evaluationContext = createEvaluationContext(result);
399405
return CacheAspectSupport.this.evaluator.key(this.operation.getKey(), this.method, evaluationContext);
400406
}
401407
return CacheAspectSupport.this.keyGenerator.generate(this.target, this.method, this.args);

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,16 @@ public void testMultiPut(CacheableService<?> service) {
367367
assertSame(r2, secondary.get(o).get());
368368
}
369369

370+
public void testPutRefersToResult(CacheableService<?> service) throws Exception {
371+
Long id = Long.MIN_VALUE;
372+
TestEntity entity = new TestEntity();
373+
Cache primary = cm.getCache("primary");
374+
assertNull(primary.get(id));
375+
assertNull(entity.getId());
376+
service.putRefersToResult(entity);
377+
assertSame(entity, primary.get(id).get());
378+
}
379+
370380
public void testMultiCacheAndEvict(CacheableService<?> service) {
371381
String methodName = "multiCacheAndEvict";
372382

@@ -621,6 +631,16 @@ public void testClassMultiPut() {
621631
testMultiPut(ccs);
622632
}
623633

634+
@Test
635+
public void testPutRefersToResult() throws Exception {
636+
testPutRefersToResult(cs);
637+
}
638+
639+
@Test
640+
public void testClassPutRefersToResult() throws Exception {
641+
testPutRefersToResult(ccs);
642+
}
643+
624644
@Test
625645
public void testMultiCacheAndEvict() {
626646
testMultiCacheAndEvict(cs);

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
public class AnnotatedClassCacheableService implements CacheableService<Object> {
3232

3333
private final AtomicLong counter = new AtomicLong();
34+
3435
public static final AtomicLong nullInvocations = new AtomicLong();
3536

3637
@Override
@@ -164,4 +165,11 @@ public Object multiConditionalCacheAndEvict(Object arg1) {
164165
public Object multiUpdate(Object arg1) {
165166
return arg1;
166167
}
168+
169+
@Override
170+
@CachePut(value="primary", key="#result.id")
171+
public TestEntity putRefersToResult(TestEntity arg1) {
172+
arg1.setId(Long.MIN_VALUE);
173+
return arg1;
174+
}
167175
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,6 @@ public interface CacheableService<T> {
7070
T multiConditionalCacheAndEvict(Object arg1);
7171

7272
T multiUpdate(Object arg1);
73+
74+
TestEntity putRefersToResult(TestEntity arg1);
7375
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,11 @@ public Long multiConditionalCacheAndEvict(Object arg1) {
170170
public Long multiUpdate(Object arg1) {
171171
return Long.valueOf(arg1.toString());
172172
}
173+
174+
@Override
175+
@CachePut(value="primary", key="#result.id")
176+
public TestEntity putRefersToResult(TestEntity arg1) {
177+
arg1.setId(Long.MIN_VALUE);
178+
return arg1;
179+
}
173180
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2002-2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cache.config;
18+
19+
import org.springframework.util.ObjectUtils;
20+
21+
/**
22+
* Simple test entity for use with caching tests.
23+
*
24+
* @author Michael Plšd
25+
*/
26+
public class TestEntity {
27+
28+
private Long id;
29+
30+
public Long getId() {
31+
return this.id;
32+
}
33+
34+
public void setId(Long id) {
35+
this.id = id;
36+
}
37+
38+
@Override
39+
public int hashCode() {
40+
return ObjectUtils.nullSafeHashCode(this.id);
41+
}
42+
43+
@Override
44+
public boolean equals(Object obj) {
45+
if (obj == this) {
46+
return true;
47+
}
48+
if (obj == null) {
49+
return false;
50+
}
51+
if (obj instanceof TestEntity) {
52+
return ObjectUtils.nullSafeEquals(this.id, ((TestEntity) obj).id);
53+
}
54+
return false;
55+
}
56+
}

0 commit comments

Comments
 (0)