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:
parent
799736c571
commit
a5139f3c66
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue