Skip to content

Commit 806c835

Browse files
lgxbslgxsnicoll
authored andcommitted
Evaluate key only if necessary
Prior to this commit a @cACHEpUT operation would fail if the key expression is invalid, but guarded with an unless condition as the former was evaluated too early. This commit makes sure that key for a put is only evaluated if the put operation is active. Note that this does not apply for @Cacheable as the key needs to be computed early to determine if a matching entry exists in the cache. See gh-22769
1 parent fd17df9 commit 806c835

File tree

7 files changed

+93
-16
lines changed

7 files changed

+93
-16
lines changed

spring-context/src/main/java/org/springframework/cache/annotation/CacheEvict.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@
119119
* <p>The SpEL expression evaluates against a dedicated context that provides the
120120
* following meta-data:
121121
* <ul>
122+
* <li>{@code #result} for a reference to the result of the method invocation, which
123+
* can only be used if {@link #beforeInvocation()} is {@code false}. For supported
124+
* wrappers such as {@code Optional}, {@code #result} refers to the actual object,
125+
* not the wrapper</li>
122126
* <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for
123127
* references to the {@link java.lang.reflect.Method method}, target object, and
124128
* affected cache(s) respectively.</li>

spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@
131131
* <p>The SpEL expression evaluates against a dedicated context that provides the
132132
* following meta-data:
133133
* <ul>
134+
* <li>{@code #result} for a reference to the result of the method invocation. For
135+
* supported wrappers such as {@code Optional}, {@code #result} refers to the actual
136+
* object, not the wrapper</li>
134137
* <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for
135138
* references to the {@link java.lang.reflect.Method method}, target object, and
136139
* affected cache(s) respectively.</li>

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

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.ArrayList;
2222
import java.util.Collection;
2323
import java.util.Collections;
24+
import java.util.LinkedList;
2425
import java.util.List;
2526
import java.util.Map;
2627
import java.util.Optional;
@@ -401,13 +402,6 @@ private Object execute(final CacheOperationInvoker invoker, Method method, Cache
401402
// Check if we have a cached item matching the conditions
402403
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
403404

404-
// Collect puts from any @Cacheable miss, if no cached item is found
405-
List<CachePutRequest> cachePutRequests = new ArrayList<>();
406-
if (cacheHit == null) {
407-
collectPutRequests(contexts.get(CacheableOperation.class),
408-
CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
409-
}
410-
411405
Object cacheValue;
412406
Object returnValue;
413407

@@ -422,6 +416,12 @@ private Object execute(final CacheOperationInvoker invoker, Method method, Cache
422416
cacheValue = unwrapReturnValue(returnValue);
423417
}
424418

419+
// Collect puts from any @Cacheable miss, if no cached item is found
420+
List<CachePutRequest> cachePutRequests = new LinkedList<>();
421+
if (cacheHit == null) {
422+
collectPutRequests(contexts.get(CacheableOperation.class), cacheValue, cachePutRequests);
423+
}
424+
425425
// Collect any explicit @CachePuts
426426
collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
427427

@@ -558,7 +558,7 @@ private void collectPutRequests(Collection<CacheOperationContext> contexts,
558558
@Nullable Object result, Collection<CachePutRequest> putRequests) {
559559

560560
for (CacheOperationContext context : contexts) {
561-
if (isConditionPassing(context, result)) {
561+
if (isConditionPassing(context, result) && context.canPutToCache(result)) {
562562
Object key = generateKey(context, result);
563563
putRequests.add(new CachePutRequest(context, key));
564564
}
@@ -832,10 +832,8 @@ public CachePutRequest(CacheOperationContext context, Object key) {
832832
}
833833

834834
public void apply(@Nullable Object result) {
835-
if (this.context.canPutToCache(result)) {
836-
for (Cache cache : this.context.getCaches()) {
837-
doPut(cache, this.key, result);
838-
}
835+
for (Cache cache : this.context.getCaches()) {
836+
doPut(cache, this.key, result);
839837
}
840838
}
841839
}

spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/AbstractCacheAnnotationTests.java

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2022 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.
@@ -31,10 +31,13 @@
3131
import org.springframework.context.testfixture.cache.beans.AnnotatedClassCacheableService;
3232
import org.springframework.context.testfixture.cache.beans.CacheableService;
3333
import org.springframework.context.testfixture.cache.beans.TestEntity;
34+
import org.springframework.expression.spel.SpelEvaluationException;
3435

3536
import static org.assertj.core.api.Assertions.assertThat;
3637
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
3738
import static org.assertj.core.api.Assertions.assertThatIOException;
39+
import static org.junit.jupiter.api.Assertions.assertEquals;
40+
import static org.junit.jupiter.api.Assertions.assertNull;
3841

3942
/**
4043
* Abstract cache annotation tests (containing several reusable methods).
@@ -504,6 +507,29 @@ protected void testPutRefersToResult(CacheableService<?> service) {
504507
assertThat(primary.get(id).get()).isSameAs(entity);
505508
}
506509

510+
public void testPutRefersToNullResult(CacheableService<?> service) {
511+
Long id = Long.MIN_VALUE;
512+
TestEntity entity = new TestEntity();
513+
Cache primary = this.cm.getCache("primary");
514+
assertNull(primary.get(id));
515+
try {
516+
service.putRefersToNullResult(entity);
517+
} catch (Exception e) {
518+
assertEquals(SpelEvaluationException.class, e.getClass());
519+
assertEquals("EL1007E: Property or field 'id' cannot be found on null", e.getMessage());
520+
}
521+
assertNull(primary.get(id));
522+
}
523+
524+
public void testPutRefersToNullResultWithUnless(CacheableService<?> service) {
525+
Long id = Long.MIN_VALUE;
526+
TestEntity entity = new TestEntity();
527+
Cache primary = this.cm.getCache("primary");
528+
assertNull(primary.get(id));
529+
service.putRefersToNullResultWithUnless(entity);
530+
assertNull(primary.get(id));
531+
}
532+
507533
protected void testMultiCacheAndEvict(CacheableService<?> service) {
508534
String methodName = "multiCacheAndEvict";
509535

@@ -854,6 +880,26 @@ public void testClassPutRefersToResult() {
854880
testPutRefersToResult(this.ccs);
855881
}
856882

883+
@Test
884+
public void testPutRefersToNullResult() throws Exception {
885+
testPutRefersToNullResult(this.cs);
886+
}
887+
888+
@Test
889+
public void testClassPutRefersToNullResult() throws Exception {
890+
testPutRefersToNullResult(this.ccs);
891+
}
892+
893+
@Test
894+
public void testPutRefersToNullResultWithUnless() throws Exception {
895+
testPutRefersToNullResultWithUnless(this.cs);
896+
}
897+
898+
@Test
899+
public void testClassPutRefersToNullResultWithUnless() throws Exception {
900+
testPutRefersToNullResultWithUnless(this.ccs);
901+
}
902+
857903
@Test
858904
public void testMultiCacheAndEvict() {
859905
testMultiCacheAndEvict(this.cs);

spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/AnnotatedClassCacheableService.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2022 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.
@@ -235,4 +235,15 @@ public TestEntity putRefersToResult(TestEntity arg1) {
235235
return arg1;
236236
}
237237

238+
@Override
239+
@CachePut(cacheNames = "primary", key = "#result.id")
240+
public TestEntity putRefersToNullResult(TestEntity arg1) {
241+
return null;
242+
}
243+
244+
@Override
245+
@CachePut(cacheNames = "primary", key = "#result.id", unless = "#result == null")
246+
public TestEntity putRefersToNullResultWithUnless(TestEntity arg1) {
247+
return null;
248+
}
238249
}

spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/CacheableService.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2022 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.
@@ -93,4 +93,8 @@ public interface CacheableService<T> {
9393

9494
TestEntity putRefersToResult(TestEntity arg1);
9595

96+
TestEntity putRefersToNullResult(TestEntity arg1);
97+
98+
TestEntity putRefersToNullResultWithUnless(TestEntity arg1);
99+
96100
}

spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/DefaultCacheableService.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2022 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.
@@ -243,4 +243,15 @@ public TestEntity putRefersToResult(TestEntity arg1) {
243243
return arg1;
244244
}
245245

246+
@Override
247+
@CachePut(cacheNames = "primary", key = "#result.id")
248+
public TestEntity putRefersToNullResult(TestEntity arg1) {
249+
return null;
250+
}
251+
252+
@Override
253+
@CachePut(cacheNames = "primary", key = "#result.id", unless = "#result == null")
254+
public TestEntity putRefersToNullResultWithUnless(TestEntity arg1) {
255+
return null;
256+
}
246257
}

0 commit comments

Comments
 (0)