Support searches for multiple merged composed annotations

Prior to this commit it was possible to search for the 1st merged
annotation above an annotated element. It was also possible to search
for annotation attributes aggregated from all annotations above an
annotated element; however, it was impossible to search for all
composed annotations above an annotated element and have the results as
synthesized merged annotations instead of a multi-map of attributes.

This commit introduces a new findAllMergedAnnotations() method in
AnnotatedElementUtils that finds all annotations of the specified type
within the annotation hierarchy above the supplied element. For each
such annotation found, it merges that annotation's attributes with
matching attributes from annotations in lower levels of the annotation
hierarchy and synthesizes the results back into an annotation of the
specified type. All such merged annotations are collected and returned
as a set.

Issue: SPR-13486
This commit is contained in:
Sam Brannen 2016-03-20 19:18:11 +01:00
parent 799736c571
commit a5139f3c66
2 changed files with 236 additions and 29 deletions

View File

@ -424,6 +424,7 @@ public class AnnotatedElementUtils {
* @param annotationType the annotation type to find
* @return the merged, synthesized {@code Annotation}, or {@code null} if not found
* @since 4.2
* @see #findAllMergedAnnotations(AnnotatedElement, Class)
* @see #findMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean)
* @see #getMergedAnnotationAttributes(AnnotatedElement, Class)
*/
@ -460,6 +461,41 @@ public class AnnotatedElementUtils {
return AnnotationUtils.synthesizeAnnotation(attributes, (Class<A>) attributes.annotationType(), element);
}
/**
* Find <strong>all</strong> annotations of the specified {@code annotationType}
* within the annotation hierarchy <em>above</em> the supplied {@code element};
* and for each annotation found, merge that annotation's attributes with
* <em>matching</em> attributes from annotations in lower levels of the annotation
* hierarchy, and synthesize the result back into an annotation of the specified
* {@code annotationType}.
* <p>{@link AliasFor @AliasFor} semantics are fully supported, both within a
* single annotation and within the annotation hierarchy.
* @param element the annotated element; never {@code null}
* @param annotationType the annotation type to find; never {@code null}
* @return the set of all merged, synthesized {@code Annotations} found, or an empty
* set if none were found
* @since 4.3
* @see #findMergedAnnotation(AnnotatedElement, Class)
*/
public static <A extends Annotation> Set<A> findAllMergedAnnotations(AnnotatedElement element,
Class<A> annotationType) {
Assert.notNull(element, "AnnotatedElement must not be null");
Assert.notNull(annotationType, "annotationType must not be null");
MergedAnnotationAttributesProcessor processor = new MergedAnnotationAttributesProcessor(annotationType, null,
false, false, true);
searchWithFindSemantics(element, annotationType, annotationType.getName(), processor);
Set<A> annotations = new LinkedHashSet<A>();
for (AnnotationAttributes attributes : processor.getAggregatedResults()) {
AnnotationUtils.postProcessAnnotationAttributes(element, attributes, false, false);
annotations.add(AnnotationUtils.synthesizeAnnotation(attributes, annotationType, element));
}
return annotations;
}
/**
* Find the first annotation of the specified {@code annotationType} within
* the annotation hierarchy <em>above</em> the supplied {@code element} and
@ -796,6 +832,8 @@ public class AnnotatedElementUtils {
// Locally declared annotations (ignoring @Inherited)
Annotation[] annotations = element.getDeclaredAnnotations();
List<T> aggregatedResults = processor.aggregates() ? new ArrayList<T>() : null;
// Search in local annotations
for (Annotation annotation : annotations) {
if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation) &&
@ -804,10 +842,15 @@ public class AnnotatedElementUtils {
metaDepth > 0)) {
T result = processor.process(element, annotation, metaDepth);
if (result != null) {
if (processor.aggregates() && metaDepth == 0) {
aggregatedResults.add(result);
}
else {
return result;
}
}
}
}
// Search in meta annotations on local annotations
for (Annotation annotation : annotations) {
@ -816,10 +859,19 @@ public class AnnotatedElementUtils {
annotation.annotationType(), annotationType, annotationName, processor, visited, metaDepth + 1);
if (result != null) {
processor.postProcess(annotation.annotationType(), annotation, result);
if (processor.aggregates() && metaDepth == 0) {
aggregatedResults.add(result);
}
else {
return result;
}
}
}
}
if (processor.aggregates()) {
processor.getAggregatedResults().addAll(0, aggregatedResults);
}
if (element instanceof Method) {
Method method = (Method) element;
@ -930,11 +982,16 @@ public class AnnotatedElementUtils {
* annotations, or all annotations discovered by the currently executing
* search. The term "target" in this context refers to a matching
* annotation (i.e., a specific annotation type that was found during
* the search). Returning a non-null value from the {@link #process}
* the search).
* <p>Returning a non-null value from the {@link #process}
* method instructs the search algorithm to stop searching further;
* whereas, returning {@code null} from the {@link #process} method
* instructs the search algorithm to continue searching for additional
* annotations.
* annotations. One exception to this rule applies to processors
* that {@linkplain #aggregates aggregate} results. If an aggregating
* processor returns a non-null value, that value will be added to the
* list of {@linkplain #getAggregatedResults aggregated results}
* and the search algorithm will continue.
* <p>Processors can optionally {@linkplain #postProcess post-process}
* the result of the {@link #process} method as the search algorithm
* goes back down the annotation hierarchy from an invocation of
@ -983,12 +1040,38 @@ public class AnnotatedElementUtils {
* @param result the result to post-process
*/
void postProcess(AnnotatedElement annotatedElement, Annotation annotation, T result);
}
/**
* {@link Processor} that {@linkplain #process processes} annotations
* but does not {@linkplain #postProcess post-process} results.
* Determine if this processor aggregates the results returned by {@link #process}.
* <p>If this method returns {@code true}, then {@link #getAggregatedResults()}
* must return a non-null value.
* <p>WARNING: aggregation is currently only supported for <em>find semantics</em>.
* @return {@code true} if this processor supports aggregated results
* @see #getAggregatedResults
* @since 4.3
*/
boolean aggregates();
/**
* Get the list of results aggregated by this processor.
* <p>NOTE: the processor does not aggregate the results itself.
* Rather, the search algorithm that uses this processor is responsible
* for asking this processor if it {@link #aggregates} results and then
* adding the post-processed results to the list returned by this
* method.
* <p>WARNING: aggregation is currently only supported for <em>find semantics</em>.
* @return the list of results aggregated by this processor; never
* {@code null} unless {@link #aggregates} returns {@code false}
* @see #aggregates
* @since 4.3
*/
List<T> getAggregatedResults();
}
/**
* {@link Processor} that {@linkplain #process(AnnotatedElement, Annotation, int)
* processes} annotations but does not {@linkplain #postProcess post-process} or
* {@linkplain #aggregates aggregate} results.
* @since 4.2
*/
private abstract static class SimpleAnnotationProcessor<T> implements Processor<T> {
@ -997,6 +1080,16 @@ public class AnnotatedElementUtils {
public final void postProcess(AnnotatedElement annotatedElement, Annotation annotation, T result) {
// no-op
}
@Override
public final boolean aggregates() {
return false;
}
@Override
public List<T> getAggregatedResults() {
throw new UnsupportedOperationException("SimpleAnnotationProcessor does not support aggregated results");
}
}
@ -1019,13 +1112,33 @@ public class AnnotatedElementUtils {
private final boolean nestedAnnotationsAsMap;
private final List<AnnotationAttributes> aggregatedResults;
MergedAnnotationAttributesProcessor(Class<? extends Annotation> annotationType, String annotationName,
boolean classValuesAsString, boolean nestedAnnotationsAsMap) {
this(annotationType, annotationName, classValuesAsString, nestedAnnotationsAsMap, false);
}
MergedAnnotationAttributesProcessor(Class<? extends Annotation> annotationType, String annotationName,
boolean classValuesAsString, boolean nestedAnnotationsAsMap, boolean aggregates) {
this.annotationType = annotationType;
this.annotationName = annotationName;
this.classValuesAsString = classValuesAsString;
this.nestedAnnotationsAsMap = nestedAnnotationsAsMap;
this.aggregatedResults = (aggregates ? new ArrayList<AnnotationAttributes>() : null);
}
@Override
public boolean aggregates() {
return this.aggregatedResults != null;
}
@Override
public List<AnnotationAttributes> getAggregatedResults() {
return this.aggregatedResults;
}
@Override

View File

@ -22,6 +22,9 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.Set;
import org.junit.Test;
@ -43,36 +46,76 @@ public class MultipleComposedAnnotationsOnSingleAnnotatedElementTests {
@Test
public void multipleComposedAnnotationsOnClass() {
assertMultipleComposedAnnotations(MultipleCachesClass.class);
assertMultipleComposedAnnotations(MultipleComposedCachesClass.class);
}
@Test
public void composedPlusLocalAnnotationsOnClass() {
assertMultipleComposedAnnotations(ComposedPlusLocalCachesClass.class);
}
@Test
public void multipleComposedAnnotationsOnInterface() {
assertMultipleComposedAnnotations(MultipleComposedCachesOnInterfaceClass.class);
}
@Test
public void composedCacheOnInterfaceAndLocalCacheOnClass() {
assertMultipleComposedAnnotations(ComposedCacheOnInterfaceAndLocalCacheClass.class);
}
@Test
public void multipleComposedAnnotationsOnMethod() throws Exception {
AnnotatedElement element = getClass().getDeclaredMethod("multipleCachesMethod");
AnnotatedElement element = getClass().getDeclaredMethod("multipleComposedCachesMethod");
assertMultipleComposedAnnotations(element);
}
@Test
public void composedPlusLocalAnnotationsOnMethod() throws Exception {
AnnotatedElement element = getClass().getDeclaredMethod("composedPlusLocalCachesMethod");
assertMultipleComposedAnnotations(element);
}
/**
* Bridge/bridged method setup code copied from
* {@link org.springframework.core.BridgeMethodResolverTests#testWithGenericParameter()}.
*/
@Test
public void multipleComposedAnnotationsBridgeMethod() throws NoSuchMethodException {
Method[] methods = StringGenericParameter.class.getMethods();
Method bridgeMethod = null;
Method bridgedMethod = null;
for (Method method : methods) {
if ("getFor".equals(method.getName()) && !method.getParameterTypes()[0].equals(Integer.class)) {
if (method.getReturnType().equals(Object.class)) {
bridgeMethod = method;
}
else {
bridgedMethod = method;
}
}
}
assertTrue(bridgeMethod != null && bridgeMethod.isBridge());
assertTrue(bridgedMethod != null && !bridgedMethod.isBridge());
assertMultipleComposedAnnotations(bridgeMethod);
}
private void assertMultipleComposedAnnotations(AnnotatedElement element) {
assertNotNull(element);
// Prerequisites
FooCache fooCache = element.getAnnotation(FooCache.class);
BarCache barCache = element.getAnnotation(BarCache.class);
assertNotNull(fooCache);
assertNotNull(barCache);
assertEquals("fooKey", fooCache.key());
assertEquals("barKey", barCache.key());
Set<Cacheable> cacheables = findAllMergedAnnotations(element, Cacheable.class);
assertNotNull(cacheables);
assertEquals(2, cacheables.size());
// Assert the status quo for finding the 1st merged annotation.
Cacheable cacheable = findMergedAnnotation(element, Cacheable.class);
assertNotNull(cacheable);
assertEquals("fooCache", cacheable.value());
assertEquals("fooKey", cacheable.key());
// TODO Introduce findMergedAnnotations(...) in AnnotatedElementUtils.
// assertEquals("barCache", cacheable.value());
// assertEquals("barKey", cacheable.key());
Iterator<Cacheable> iterator = cacheables.iterator();
Cacheable fooCacheable = iterator.next();
Cacheable barCacheable = iterator.next();
assertEquals("fooKey", fooCacheable.key());
assertEquals("fooCache", fooCacheable.value());
assertEquals("barKey", barCacheable.key());
assertEquals("barCache", barCacheable.value());
}
@ -86,7 +129,11 @@ public class MultipleComposedAnnotationsOnSingleAnnotatedElementTests {
@Inherited
@interface Cacheable {
String value();
@AliasFor("cacheName")
String value() default "";
@AliasFor("value")
String cacheName() default "";
String key() default "";
}
@ -113,13 +160,60 @@ public class MultipleComposedAnnotationsOnSingleAnnotatedElementTests {
@FooCache(key = "fooKey")
@BarCache(key = "barKey")
private static class MultipleCachesClass {
private static class MultipleComposedCachesClass {
}
@Cacheable(cacheName = "fooCache", key = "fooKey")
@BarCache(key = "barKey")
private static class ComposedPlusLocalCachesClass {
}
@FooCache(key = "fooKey")
@BarCache(key = "barKey")
private interface MultipleComposedCachesInterface {
}
private static class MultipleComposedCachesOnInterfaceClass implements MultipleComposedCachesInterface {
}
@Cacheable(cacheName = "fooCache", key = "fooKey")
private interface ComposedCacheInterface {
}
@BarCache(key = "barKey")
private static class ComposedCacheOnInterfaceAndLocalCacheClass implements ComposedCacheInterface {
}
@FooCache(key = "fooKey")
@BarCache(key = "barKey")
private void multipleCachesMethod() {
private void multipleComposedCachesMethod() {
}
@Cacheable(cacheName = "fooCache", key = "fooKey")
@BarCache(key = "barKey")
private void composedPlusLocalCachesMethod() {
}
public interface GenericParameter<T> {
T getFor(Class<T> cls);
}
@SuppressWarnings("unused")
private static class StringGenericParameter implements GenericParameter<String> {
@FooCache(key = "fooKey")
@BarCache(key = "barKey")
@Override
public String getFor(Class<String> cls) {
return "foo";
}
public String getFor(Integer integer) {
return "foo";
}
}
}