Skip to content

Commit 81c2080

Browse files
committed
Custom KeyGenerator
This commit adds an extra parameter to the base @Cache method annotations: keyGenerator. This parameter holds the name of the KeyGenerator bean to use to compute the key for that specific caching endpoint. This gives therefore a third way to customize the key. These are: 1. Default KeyGenerator (global for all endpoints) 2. The 'key' attribute of the annotation, giving the SpEL expression to use 3. The 'keyGenerator' attribute of the annotation The annotation attributes are therefore exclusive. Trying to specify them both will result in an IllegalStateException. The KeyGenerator to use for a given operation is cached on startup so that multiple calls to it does not resolve the instance to use over and over again. Issue: SPR-10629
1 parent 906321d commit 81c2080

25 files changed

+407
-38
lines changed

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2014 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.
@@ -105,6 +105,18 @@ public Object rootVars(Object arg1) {
105105
return counter.getAndIncrement();
106106
}
107107

108+
@Override
109+
@Cacheable(value = "default", keyGenerator = "customKyeGenerator")
110+
public Object customKeyGenerator(Object arg1) {
111+
return counter.getAndIncrement();
112+
}
113+
114+
@Override
115+
@Cacheable(value = "default", keyGenerator = "unknownBeanName")
116+
public Object unknownCustomKeyGenerator(Object arg1) {
117+
return counter.getAndIncrement();
118+
}
119+
108120
@Override
109121
@CachePut("default")
110122
public Object update(Object arg1) {

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2014 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.
@@ -58,6 +58,10 @@ public interface CacheableService<T> {
5858

5959
T rootVars(Object arg1);
6060

61+
T customKeyGenerator(Object arg1);
62+
63+
T unknownCustomKeyGenerator(Object arg1);
64+
6165
T throwChecked(Object arg1) throws Exception;
6266

6367
T throwUnchecked(Object arg1);

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2014 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.
@@ -109,6 +109,18 @@ public Long rootVars(Object arg1) {
109109
return counter.getAndIncrement();
110110
}
111111

112+
@Override
113+
@Cacheable(value = "default", keyGenerator = "customKeyGenerator")
114+
public Long customKeyGenerator(Object arg1) {
115+
return counter.getAndIncrement();
116+
}
117+
118+
@Override
119+
@Cacheable(value = "default", keyGenerator = "unknownBeanName")
120+
public Long unknownCustomKeyGenerator(Object arg1) {
121+
return counter.getAndIncrement();
122+
}
123+
112124
@Override
113125
@CachePut("default")
114126
public Long update(Object arg1) {
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2002-2014 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.cache.interceptor.KeyGenerator;
20+
21+
import java.lang.reflect.Method;
22+
23+
/**
24+
* A custom {@link KeyGenerator} that exposes the algorithm used to compute the key
25+
* for convenience in tests scenario.
26+
*
27+
* @author Stephane Nicoll
28+
*/
29+
final class SomeCustomKeyGenerator implements KeyGenerator {
30+
31+
@Override
32+
public Object generate(Object target, Method method, Object... params) {
33+
return generateKey(method.getName(), params);
34+
}
35+
36+
/**
37+
* @see #generate(Object, java.lang.reflect.Method, Object...)
38+
*/
39+
static Object generateKey(String methodName, Object... params) {
40+
final StringBuilder sb = new StringBuilder(methodName);
41+
for (Object param : params) {
42+
sb.append(param);
43+
}
44+
return sb.toString();
45+
}
46+
}

spring-aspects/src/test/java/org/springframework/cache/config/annotation-cache-aspectj.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
</property>
4444
</bean>
4545

46+
<bean id="customKeyGenerator" class="org.springframework.cache.config.SomeCustomKeyGenerator" />
47+
4648
<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor"/>
4749

4850
<bean id="service" class="org.springframework.cache.config.DefaultCacheableService"/>

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2014 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.
@@ -45,10 +45,17 @@
4545

4646
/**
4747
* Spring Expression Language (SpEL) attribute for computing the key dynamically.
48-
* <p>Default is "", meaning all method parameters are considered as a key.
48+
* <p>Default is "", meaning all method parameters are considered as a key, unless
49+
* a custom {@link #keyGenerator()} has been set.
4950
*/
5051
String key() default "";
5152

53+
/**
54+
* The bean name of the custom {@link org.springframework.cache.interceptor.KeyGenerator} to use.
55+
* <p>Mutually exclusive with the {@link #key()} attribute.
56+
*/
57+
String keyGenerator() default "";
58+
5259
/**
5360
* Spring Expression Language (SpEL) attribute used for conditioning the method caching.
5461
* <p>Default is "", meaning the method is always cached.

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2014 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.
@@ -50,10 +50,17 @@
5050

5151
/**
5252
* Spring Expression Language (SpEL) attribute for computing the key dynamically.
53-
* <p>Default is "", meaning all method parameters are considered as a key.
53+
* <p>Default is "", meaning all method parameters are considered as a key, unless
54+
* a custom {@link #keyGenerator()} has been set.
5455
*/
5556
String key() default "";
5657

58+
/**
59+
* The bean name of the custom {@link org.springframework.cache.interceptor.KeyGenerator} to use.
60+
* <p>Mutually exclusive with the {@link #key()} attribute.
61+
*/
62+
String keyGenerator() default "";
63+
5764
/**
5865
* Spring Expression Language (SpEL) attribute used for conditioning the cache update.
5966
* <p>Default is "", meaning the method result is always cached.

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2014 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.
@@ -48,10 +48,17 @@
4848

4949
/**
5050
* Spring Expression Language (SpEL) attribute for computing the key dynamically.
51-
* <p>Default is "", meaning all method parameters are considered as a key.
51+
* <p>Default is "", meaning all method parameters are considered as a key, unless
52+
* a custom {@link #keyGenerator()} has been set.
5253
*/
5354
String key() default "";
5455

56+
/**
57+
* The bean name of the custom {@link org.springframework.cache.interceptor.KeyGenerator} to use.
58+
* <p>Mutually exclusive with the {@link #key()} attribute.
59+
*/
60+
String keyGenerator() default "";
61+
5562
/**
5663
* Spring Expression Language (SpEL) attribute used for conditioning the method caching.
5764
* <p>Default is "", meaning the method is always cached.

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2014 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.
@@ -27,6 +27,7 @@
2727
import org.springframework.cache.interceptor.CachePutOperation;
2828
import org.springframework.cache.interceptor.CacheableOperation;
2929
import org.springframework.util.ObjectUtils;
30+
import org.springframework.util.StringUtils;
3031

3132
/**
3233
* Strategy implementation for parsing Spring's {@link Caching}, {@link Cacheable},
@@ -86,7 +87,10 @@ CacheableOperation parseCacheableAnnotation(AnnotatedElement ae, Cacheable cachi
8687
cuo.setCondition(caching.condition());
8788
cuo.setUnless(caching.unless());
8889
cuo.setKey(caching.key());
90+
cuo.setKeyGenerator(caching.keyGenerator());
8991
cuo.setName(ae.toString());
92+
93+
checkKeySourceConsistency(ae, caching.key(), caching.keyGenerator());
9094
return cuo;
9195
}
9296

@@ -95,9 +99,12 @@ CacheEvictOperation parseEvictAnnotation(AnnotatedElement ae, CacheEvict caching
9599
ceo.setCacheNames(caching.value());
96100
ceo.setCondition(caching.condition());
97101
ceo.setKey(caching.key());
102+
ceo.setKeyGenerator(caching.keyGenerator());
98103
ceo.setCacheWide(caching.allEntries());
99104
ceo.setBeforeInvocation(caching.beforeInvocation());
100105
ceo.setName(ae.toString());
106+
107+
checkKeySourceConsistency(ae, caching.key(), caching.keyGenerator());
101108
return ceo;
102109
}
103110

@@ -107,7 +114,10 @@ CacheOperation parseUpdateAnnotation(AnnotatedElement ae, CachePut caching) {
107114
cuo.setCondition(caching.condition());
108115
cuo.setUnless(caching.unless());
109116
cuo.setKey(caching.key());
117+
cuo.setKeyGenerator(caching.keyGenerator());
110118
cuo.setName(ae.toString());
119+
120+
checkKeySourceConsistency(ae, caching.key(), caching.keyGenerator());
111121
return cuo;
112122
}
113123

@@ -159,6 +169,15 @@ private <T extends Annotation> Collection<T> getAnnotations(AnnotatedElement ae,
159169
return (anns.isEmpty() ? null : anns);
160170
}
161171

172+
private void checkKeySourceConsistency(AnnotatedElement ae, String key, String keyGenerator) {
173+
if (StringUtils.hasText(key) && StringUtils.hasText(keyGenerator)) {
174+
throw new IllegalStateException("Invalid cache annotation configuration on '"
175+
+ ae.toString() + "'. Both 'key' and 'keyGenerator' attributes have been set. " +
176+
"These attributes are mutually exclusive: either set the SpEL expression used to" +
177+
"compute the key at runtime or set the name of the KeyGenerator bean to use.");
178+
}
179+
}
180+
162181
@Override
163182
public boolean equals(Object other) {
164183
return (this == other || other instanceof SpringCacheAnnotationParser);

spring-context/src/main/java/org/springframework/cache/config/CacheAdviceParser.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2014 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.
@@ -168,7 +168,7 @@ private RootBeanDefinition parseDefinitionSource(Element definition, ParserConte
168168

169169
private static String getAttributeValue(Element element, String attributeName, String defaultValue) {
170170
String attribute = element.getAttribute(attributeName);
171-
if(StringUtils.hasText(attribute)) {
171+
if (StringUtils.hasText(attribute)) {
172172
return attribute.trim();
173173
}
174174
return defaultValue;
@@ -184,6 +184,8 @@ private static class Props {
184184

185185
private String key;
186186

187+
private String keyGenerator;
188+
187189
private String condition;
188190

189191
private String method;
@@ -194,6 +196,7 @@ private static class Props {
194196
Props(Element root) {
195197
String defaultCache = root.getAttribute("cache");
196198
key = root.getAttribute("key");
199+
keyGenerator = root.getAttribute("key-generator");
197200
condition = root.getAttribute("condition");
198201
method = root.getAttribute(METHOD_ATTRIBUTE);
199202

@@ -218,8 +221,16 @@ <T extends CacheOperation> T merge(Element element, ReaderContext readerCtx, T o
218221
op.setCacheNames(localCaches);
219222

220223
op.setKey(getAttributeValue(element, "key", this.key));
224+
op.setKeyGenerator(getAttributeValue(element, "key-generator", this.keyGenerator));
221225
op.setCondition(getAttributeValue(element, "condition", this.condition));
222226

227+
if (StringUtils.hasText(op.getKey()) && StringUtils.hasText(op.getKeyGenerator())) {
228+
throw new IllegalStateException("Invalid cache advice configuration on '"
229+
+ element.toString() + "'. Both 'key' and 'keyGenerator' attributes have been set. " +
230+
"These attributes are mutually exclusive: either set the SpEL expression used to" +
231+
"compute the key at runtime or set the name of the KeyGenerator bean to use.");
232+
}
233+
223234
return op;
224235
}
225236

0 commit comments

Comments
 (0)