Support @Cache* as merged composed annotations

Prior to this commit, @Cacheable, @CacheEvict, @CachePut, and @Caching
could be used to create custom stereotype annotations with hardcoded
values for their attributes; however, it was not possible to create
composed annotations with attribute overrides.

This commit addresses this issue by refactoring
SpringCacheAnnotationParser to use the newly introduced
findAllMergedAnnotations() method in AnnotatedElementUtils. As a
result, @Cacheable, @CacheEvict, @CachePut, and @Caching can now be
used to create custom composed annotations with attribute overrides
configured via @AliasFor.

Issue: SPR-13475
This commit is contained in:
Sam Brannen
2016-03-21 12:53:14 +01:00
parent 2a715e9c61
commit 59c88eb3c0
2 changed files with 49 additions and 56 deletions

View File

@@ -27,6 +27,7 @@ import org.springframework.cache.interceptor.CacheEvictOperation;
import org.springframework.cache.interceptor.CacheOperation;
import org.springframework.cache.interceptor.CachePutOperation;
import org.springframework.cache.interceptor.CacheableOperation;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -61,29 +62,29 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria
protected Collection<CacheOperation> parseCacheAnnotations(DefaultCacheConfig cachingConfig, AnnotatedElement ae) {
Collection<CacheOperation> ops = null;
Collection<Cacheable> cacheables = getAnnotations(ae, Cacheable.class);
if (cacheables != null) {
Collection<Cacheable> cacheables = AnnotatedElementUtils.findAllMergedAnnotations(ae, Cacheable.class);
if (!cacheables.isEmpty()) {
ops = lazyInit(ops);
for (Cacheable cacheable : cacheables) {
ops.add(parseCacheableAnnotation(ae, cachingConfig, cacheable));
}
}
Collection<CacheEvict> evicts = getAnnotations(ae, CacheEvict.class);
if (evicts != null) {
Collection<CacheEvict> evicts = AnnotatedElementUtils.findAllMergedAnnotations(ae, CacheEvict.class);
if (!evicts.isEmpty()) {
ops = lazyInit(ops);
for (CacheEvict evict : evicts) {
ops.add(parseEvictAnnotation(ae, cachingConfig, evict));
}
}
Collection<CachePut> puts = getAnnotations(ae, CachePut.class);
if (puts != null) {
Collection<CachePut> puts = AnnotatedElementUtils.findAllMergedAnnotations(ae, CachePut.class);
if (!puts.isEmpty()) {
ops = lazyInit(ops);
for (CachePut put : puts) {
ops.add(parsePutAnnotation(ae, cachingConfig, put));
}
}
Collection<Caching> cachings = getAnnotations(ae, Caching.class);
if (cachings != null) {
Collection<Caching> cachings = AnnotatedElementUtils.findAllMergedAnnotations(ae, Caching.class);
if (!cachings.isEmpty()) {
ops = lazyInit(ops);
for (Caching caching : cachings) {
ops.addAll(parseCachingAnnotation(ae, cachingConfig, caching));
@@ -198,26 +199,6 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria
return new DefaultCacheConfig();
}
private <A extends Annotation> Collection<A> getAnnotations(AnnotatedElement ae, Class<A> annotationType) {
Collection<A> anns = new ArrayList<A>(1);
// look at raw annotation
A ann = ae.getAnnotation(annotationType);
if (ann != null) {
anns.add(AnnotationUtils.synthesizeAnnotation(ann, ae));
}
// scan meta-annotations
for (Annotation metaAnn : ae.getAnnotations()) {
ann = metaAnn.annotationType().getAnnotation(annotationType);
if (ann != null) {
anns.add(AnnotationUtils.synthesizeAnnotation(ann, ae));
}
}
return (!anns.isEmpty() ? anns : null);
}
/**
* Validates the specified {@link CacheOperation}.
* <p>Throws an {@link IllegalStateException} if the state of the operation is

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2016 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.
@@ -26,7 +26,6 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
@@ -105,34 +104,46 @@ public class AnnotationCacheOperationSourceTests {
assertTrue(next.getCacheNames().contains("bar"));
}
// TODO [SPR-13475] Enable test once @Cache* is supported as a composed annotation.
@Ignore("Disabled until SPR-13475 is resolved")
@Test
public void singleComposedAnnotation() throws Exception {
Collection<CacheOperation> ops = getOps(AnnotatedClass.class, "singleComposed", 1);
CacheOperation cacheOperation = ops.iterator().next();
assertThat(cacheOperation, instanceOf(CacheableOperation.class));
assertThat(cacheOperation.getCacheNames(), equalTo(Collections.singleton("composed")));
}
// TODO [SPR-13475] Enable test once @Cache* is supported as a composed annotation.
@Ignore("Disabled until SPR-13475 is resolved")
@Test
public void multipleComposedAnnotations() throws Exception {
Collection<CacheOperation> ops = getOps(AnnotatedClass.class, "multipleComposed", 3);
Collection<CacheOperation> ops = getOps(AnnotatedClass.class, "singleComposed", 2);
Iterator<CacheOperation> it = ops.iterator();
CacheOperation cacheOperation = it.next();
assertThat(cacheOperation, instanceOf(CacheableOperation.class));
assertThat(cacheOperation.getCacheNames(), equalTo(Collections.singleton("directly declared")));
assertThat(cacheOperation.getKey(), equalTo(""));
cacheOperation = it.next();
assertThat(cacheOperation, instanceOf(CacheableOperation.class));
assertThat(cacheOperation.getCacheNames(), equalTo(Collections.singleton("composedCache")));
assertThat(cacheOperation.getKey(), equalTo("composedKey"));
}
@Test
public void multipleComposedAnnotations() throws Exception {
Collection<CacheOperation> ops = getOps(AnnotatedClass.class, "multipleComposed", 4);
Iterator<CacheOperation> it = ops.iterator();
CacheOperation cacheOperation = it.next();
assertThat(cacheOperation, instanceOf(CacheableOperation.class));
assertThat(cacheOperation.getCacheNames(), equalTo(Collections.singleton("directly declared")));
assertThat(cacheOperation.getKey(), equalTo(""));
cacheOperation = it.next();
assertThat(cacheOperation, instanceOf(CacheableOperation.class));
assertThat(cacheOperation.getCacheNames(), equalTo(Collections.singleton("composedCache")));
assertThat(cacheOperation.getKey(), equalTo("composedKey"));
cacheOperation = it.next();
assertThat(cacheOperation, instanceOf(CacheableOperation.class));
assertThat(cacheOperation.getCacheNames(), equalTo(Collections.singleton("foo")));
assertThat(cacheOperation.getKey(), equalTo(""));
cacheOperation = it.next();
assertThat(cacheOperation, instanceOf(CacheEvictOperation.class));
assertThat(cacheOperation.getCacheNames(), equalTo(Collections.singleton("composedCache")));
assertThat(cacheOperation.getCacheNames(), equalTo(Collections.singleton("composedCacheEvict")));
assertThat(cacheOperation.getKey(), equalTo("composedEvictionKey"));
}
@Test
@@ -309,13 +320,15 @@ public class AnnotationCacheOperationSourceTests {
public void multipleStereotype() {
}
@ComposedCacheable("composed")
@Cacheable("directly declared")
@ComposedCacheable(cacheNames = "composedCache", key = "composedKey")
public void singleComposed() {
}
@Cacheable("directly declared")
@ComposedCacheable(cacheNames = "composedCache", key = "composedKey")
@CacheableFoo
@ComposedCacheEvict(cacheNames = "composedCache", key = "composedKey")
@ComposedCacheEvict(cacheNames = "composedCacheEvict", key = "composedEvictionKey")
public void multipleComposed() {
}
@@ -443,38 +456,37 @@ public class AnnotationCacheOperationSourceTests {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@CacheConfig(keyGenerator = "classKeyGenerator",
cacheManager = "classCacheManager", cacheResolver = "classCacheResolver")
@CacheConfig(keyGenerator = "classKeyGenerator", cacheManager = "classCacheManager", cacheResolver = "classCacheResolver")
public @interface CacheConfigFoo {
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
@Cacheable(cacheNames = "shadowed cache name", key = "shadowed key")
public @interface ComposedCacheable {
@interface ComposedCacheable {
@AliasFor(annotation = Cacheable.class, attribute = "cacheNames")
@AliasFor(annotation = Cacheable.class)
String[] value() default {};
@AliasFor(annotation = Cacheable.class, attribute = "cacheNames")
@AliasFor(annotation = Cacheable.class)
String[] cacheNames() default {};
@AliasFor(annotation = Cacheable.class, attribute = "key")
@AliasFor(annotation = Cacheable.class)
String key() default "";
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
@CacheEvict(cacheNames = "shadowed cache name", key = "shadowed key")
public @interface ComposedCacheEvict {
@interface ComposedCacheEvict {
@AliasFor(annotation = Cacheable.class, attribute = "cacheNames")
@AliasFor(annotation = CacheEvict.class)
String[] value() default {};
@AliasFor(annotation = Cacheable.class, attribute = "cacheNames")
@AliasFor(annotation = CacheEvict.class)
String[] cacheNames() default {};
@AliasFor(annotation = Cacheable.class, attribute = "key")
@AliasFor(annotation = CacheEvict.class)
String key() default "";
}