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
This commit is contained in:
Stephane Nicoll
2014-01-10 16:24:51 +01:00
parent 906321dcdd
commit 81c208098f
25 changed files with 407 additions and 38 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011 the original author or authors.
* Copyright 2011-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -90,6 +90,32 @@ public class AnnotationCacheOperationSourceTests {
assertTrue(next.getCacheNames().contains("bar"));
}
@Test
public void testCustomKeyGenerator() {
Collection<CacheOperation> ops = getOps("customKeyGenerator");
assertEquals(1, ops.size());
CacheOperation cacheOperation = ops.iterator().next();
assertEquals("Custom key generator not set", "custom", cacheOperation.getKeyGenerator());
}
@Test
public void testCustomKeyGeneratorInherited() {
Collection<CacheOperation> ops = getOps("customKeyGeneratorInherited");
assertEquals(1, ops.size());
CacheOperation cacheOperation = ops.iterator().next();
assertEquals("Custom key generator not set", "custom", cacheOperation.getKeyGenerator());
}
@Test
public void testKeyAndKeyGeneratorCannotBeSetTogether() {
try {
getOps("invalidKeyAndKeyGeneratorSet");
fail("Should have failed to parse @Cacheable annotation");
} catch (IllegalStateException e) {
// expected
}
}
private static class AnnotatedClass {
@Cacheable("test")
public void singular() {
@@ -100,13 +126,16 @@ public class AnnotationCacheOperationSourceTests {
public void multiple() {
}
@Caching(cacheable = { @Cacheable("test") }, evict = { @CacheEvict("test") })
@Caching(cacheable = {@Cacheable("test")}, evict = {@CacheEvict("test")})
public void caching() {
}
@Cacheable(value = "test", keyGenerator = "custom")
public void customKeyGenerator() {
}
@EvictFoo
public void singleStereotype() {
}
@EvictFoo
@@ -115,9 +144,17 @@ public class AnnotationCacheOperationSourceTests {
public void multipleStereotype() {
}
@Caching(cacheable = { @Cacheable(value = "test", key = "a"), @Cacheable(value = "test", key = "b") })
@Caching(cacheable = {@Cacheable(value = "test", key = "a"), @Cacheable(value = "test", key = "b")})
public void multipleCaching() {
}
@CacheableFooCustomKeyGenerator
public void customKeyGeneratorInherited() {
}
@Cacheable(value = "test", key = "#root.methodName", keyGenerator = "custom")
public void invalidKeyAndKeyGeneratorSet() {
}
}
@Retention(RetentionPolicy.RUNTIME)
@@ -126,6 +163,12 @@ public class AnnotationCacheOperationSourceTests {
public @interface CacheableFoo {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Cacheable(value = "foo", keyGenerator = "custom")
public @interface CacheableFooCustomKeyGenerator {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@CacheEvict(value = "foo")

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,8 +16,7 @@
package org.springframework.cache.config;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.util.Collection;
@@ -26,6 +25,7 @@ import java.util.UUID;
import org.junit.Before;
import org.junit.Test;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.context.ApplicationContext;
@@ -569,6 +569,28 @@ public abstract class AbstractAnnotationTests {
testRootVars(ccs);
}
@Test
public void testCustomKeyGenerator() {
Object param = new Object();
Object r1 = cs.customKeyGenerator(param);
assertSame(r1, cs.customKeyGenerator(param));
Cache cache = cm.getCache("default");
// Checks that the custom keyGenerator was used
Object expectedKey = SomeCustomKeyGenerator.generateKey("customKeyGenerator", param);
assertNotNull(cache.get(expectedKey));
}
@Test
public void testUnknownCustomKeyGenerator() {
try {
Object param = new Object();
cs.unknownCustomKeyGenerator(param);
fail("should have failed with NoSuchBeanDefinitionException");
} catch (NoSuchBeanDefinitionException e) {
// expected
}
}
@Test
public void testNullArg() throws Exception {
testNullArg(cs);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -107,6 +107,18 @@ public class AnnotatedClassCacheableService implements CacheableService<Object>
return counter.getAndIncrement();
}
@Override
@Cacheable(value = "default", keyGenerator = "customKyeGenerator")
public Object customKeyGenerator(Object arg1) {
return counter.getAndIncrement();
}
@Override
@Cacheable(value = "default", keyGenerator = "unknownBeanName")
public Object unknownCustomKeyGenerator(Object arg1) {
return counter.getAndIncrement();
}
@Override
@CachePut("default")
public Object update(Object arg1) {

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cache.config;
import static org.junit.Assert.*;
import org.junit.Test;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.context.support.GenericXmlApplicationContext;
/**
* AOP advice specific parsing tests.
*
* @author Stephane Nicoll
*/
public class CacheAdviceParserTests {
@Test
public void keyAndKeyGeneratorCannotBeSetTogether() {
try {
new GenericXmlApplicationContext("/org/springframework/cache/config/cache-advice-invalid.xml");
fail("Should have failed to load context, one advise define both a key and a key generator");
} catch (BeanDefinitionStoreException e) { // TODO better exception handling
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -58,6 +58,10 @@ public interface CacheableService<T> {
T rootVars(Object arg1);
T customKeyGenerator(Object arg1);
T unknownCustomKeyGenerator(Object arg1);
T throwChecked(Object arg1) throws Exception;
T throwUnchecked(Object arg1);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -109,6 +109,18 @@ public class DefaultCacheableService implements CacheableService<Long> {
return counter.getAndIncrement();
}
@Override
@Cacheable(value = "default", keyGenerator = "customKeyGenerator")
public Long customKeyGenerator(Object arg1) {
return counter.getAndIncrement();
}
@Override
@Cacheable(value = "default", keyGenerator = "unknownBeanName")
public Long unknownCustomKeyGenerator(Object arg1) {
return counter.getAndIncrement();
}
@Override
@CachePut("default")
public Long update(Object arg1) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -52,7 +52,7 @@ public class EnableCachingTests extends AbstractAnnotationTests {
@Test
public void testKeyStrategy() throws Exception {
CacheInterceptor ci = ctx.getBean(CacheInterceptor.class);
assertSame(ctx.getBean(KeyGenerator.class), ci.getKeyGenerator());
assertSame(ctx.getBean("keyGenerator", KeyGenerator.class), ci.getKeyGenerator());
}
// --- local tests -------
@@ -140,6 +140,11 @@ public class EnableCachingTests extends AbstractAnnotationTests {
public KeyGenerator keyGenerator() {
return new SomeKeyGenerator();
}
@Bean
public KeyGenerator customKeyGenerator() {
return new SomeCustomKeyGenerator();
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cache.config;
import org.springframework.cache.interceptor.KeyGenerator;
import java.lang.reflect.Method;
/**
* A custom {@link KeyGenerator} that exposes the algorithm used to compute the key
* for convenience in tests scenario.
*
* @author Stephane Nicoll
*/
final class SomeCustomKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return generateKey(method.getName(), params);
}
/**
* @see #generate(Object, java.lang.reflect.Method, Object...)
*/
static Object generateKey(String methodName, Object... params) {
final StringBuilder sb = new StringBuilder(methodName);
for (Object param : params) {
sb.append(param);
}
return sb.toString();
}
}