提交 1d30bf83 编写于 作者: S Sam Brannen

Favor 'local' annotations over inherited ones

Prior to this commit, the implementations of findAnnotation() in
AnnotationUtils and getAnnotationAttributes() in AnnotatedElementUtils
favored inherited annotations and inherited composed annotations over
composed annotations that are declared closer to the starting class
passed to these methods.

This commit addresses this issue as follows:

- Refactored AnnotationUtils to use getDeclaredAnnotation() and
  getDeclaredAnnotations() instead of getAnnotation() and
  getAnnotations() where appropriate.

- AnnotatedElementUtils.doProcess() supports a traverseClassHierarchy
  flag to control whether the class hierarchy should be traversed,
  using getDeclaredAnnotations() instead of getAnnotations() if the
  flag is true.

- Overhauled Javadoc in AnnotatedElementUtils.

Issue: SPR-11475
上级 0616cbcc
...@@ -26,6 +26,8 @@ import java.util.Set; ...@@ -26,6 +26,8 @@ import java.util.Set;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import static org.springframework.core.annotation.AnnotationUtils.*;
/** /**
* Utility class used to collect all annotation values including those declared on * Utility class used to collect all annotation values including those declared on
* meta-annotations. * meta-annotations.
...@@ -39,14 +41,16 @@ public class AnnotatedElementUtils { ...@@ -39,14 +41,16 @@ public class AnnotatedElementUtils {
public static Set<String> getMetaAnnotationTypes(AnnotatedElement element, String annotationType) { public static Set<String> getMetaAnnotationTypes(AnnotatedElement element, String annotationType) {
final Set<String> types = new LinkedHashSet<String>(); final Set<String> types = new LinkedHashSet<String>();
process(element, annotationType, new Processor<Object>() { process(element, annotationType, true, new Processor<Object>() {
@Override @Override
public Object process(Annotation annotation, int depth) { public Object process(Annotation annotation, int metaDepth) {
if (depth > 0) { if (metaDepth > 0) {
types.add(annotation.annotationType().getName()); types.add(annotation.annotationType().getName());
} }
return null; return null;
} }
@Override @Override
public void postProcess(Annotation annotation, Object result) { public void postProcess(Annotation annotation, Object result) {
} }
...@@ -55,14 +59,16 @@ public class AnnotatedElementUtils { ...@@ -55,14 +59,16 @@ public class AnnotatedElementUtils {
} }
public static boolean hasMetaAnnotationTypes(AnnotatedElement element, String annotationType) { public static boolean hasMetaAnnotationTypes(AnnotatedElement element, String annotationType) {
return Boolean.TRUE.equals(process(element, annotationType, new Processor<Boolean>() { return Boolean.TRUE.equals(process(element, annotationType, true, new Processor<Boolean>() {
@Override @Override
public Boolean process(Annotation annotation, int depth) { public Boolean process(Annotation annotation, int metaDepth) {
if (depth > 0) { if (metaDepth > 0) {
return true; return Boolean.TRUE;
} }
return null; return null;
} }
@Override @Override
public void postProcess(Annotation annotation, Boolean result) { public void postProcess(Annotation annotation, Boolean result) {
} }
...@@ -70,11 +76,13 @@ public class AnnotatedElementUtils { ...@@ -70,11 +76,13 @@ public class AnnotatedElementUtils {
} }
public static boolean isAnnotated(AnnotatedElement element, String annotationType) { public static boolean isAnnotated(AnnotatedElement element, String annotationType) {
return Boolean.TRUE.equals(process(element, annotationType, new Processor<Boolean>() { return Boolean.TRUE.equals(process(element, annotationType, true, new Processor<Boolean>() {
@Override @Override
public Boolean process(Annotation annotation, int depth) { public Boolean process(Annotation annotation, int metaDepth) {
return true; return Boolean.TRUE;
} }
@Override @Override
public void postProcess(Annotation annotation, Boolean result) { public void postProcess(Annotation annotation, Boolean result) {
} }
...@@ -85,20 +93,21 @@ public class AnnotatedElementUtils { ...@@ -85,20 +93,21 @@ public class AnnotatedElementUtils {
return getAnnotationAttributes(element, annotationType, false, false); return getAnnotationAttributes(element, annotationType, false, false);
} }
public static AnnotationAttributes getAnnotationAttributes( public static AnnotationAttributes getAnnotationAttributes(AnnotatedElement element, String annotationType,
AnnotatedElement element, String annotationType,
final boolean classValuesAsString, final boolean nestedAnnotationsAsMap) { final boolean classValuesAsString, final boolean nestedAnnotationsAsMap) {
return process(element, annotationType, new Processor<AnnotationAttributes>() { return process(element, annotationType, true, new Processor<AnnotationAttributes>() {
@Override @Override
public AnnotationAttributes process(Annotation annotation, int depth) { public AnnotationAttributes process(Annotation annotation, int metaDepth) {
return AnnotationUtils.getAnnotationAttributes(annotation, classValuesAsString, nestedAnnotationsAsMap); return AnnotationUtils.getAnnotationAttributes(annotation, classValuesAsString, nestedAnnotationsAsMap);
} }
@Override @Override
public void postProcess(Annotation annotation, AnnotationAttributes result) { public void postProcess(Annotation annotation, AnnotationAttributes result) {
for (String key : result.keySet()) { for (String key : result.keySet()) {
if (!"value".equals(key)) { if (!VALUE.equals(key)) {
Object value = AnnotationUtils.getValue(annotation, key); Object value = getValue(annotation, key);
if (value != null) { if (value != null) {
result.put(key, value); result.put(key, value);
} }
...@@ -108,28 +117,33 @@ public class AnnotatedElementUtils { ...@@ -108,28 +117,33 @@ public class AnnotatedElementUtils {
}); });
} }
public static MultiValueMap<String, Object> getAllAnnotationAttributes( public static MultiValueMap<String, Object> getAllAnnotationAttributes(AnnotatedElement element,
AnnotatedElement element, final String annotationType, final String annotationType) {
final boolean classValuesAsString, final boolean nestedAnnotationsAsMap) { return getAllAnnotationAttributes(element, annotationType, false, false);
}
public static MultiValueMap<String, Object> getAllAnnotationAttributes(AnnotatedElement element,
final String annotationType, final boolean classValuesAsString, final boolean nestedAnnotationsAsMap) {
final MultiValueMap<String, Object> attributes = new LinkedMultiValueMap<String, Object>(); final MultiValueMap<String, Object> attributes = new LinkedMultiValueMap<String, Object>();
process(element, annotationType, new Processor<Void>() { process(element, annotationType, false, new Processor<Void>() {
@Override @Override
public Void process(Annotation annotation, int depth) { public Void process(Annotation annotation, int metaDepth) {
if (annotation.annotationType().getName().equals(annotationType)) { if (annotation.annotationType().getName().equals(annotationType)) {
for (Map.Entry<String, Object> entry : for (Map.Entry<String, Object> entry : AnnotationUtils.getAnnotationAttributes(annotation,
AnnotationUtils.getAnnotationAttributes( classValuesAsString, nestedAnnotationsAsMap).entrySet()) {
annotation, classValuesAsString, nestedAnnotationsAsMap).entrySet()) {
attributes.add(entry.getKey(), entry.getValue()); attributes.add(entry.getKey(), entry.getValue());
} }
} }
return null; return null;
} }
@Override @Override
public void postProcess(Annotation annotation, Void result) { public void postProcess(Annotation annotation, Void result) {
for (String key : attributes.keySet()) { for (String key : attributes.keySet()) {
if (!"value".equals(key)) { if (!VALUE.equals(key)) {
Object value = AnnotationUtils.getValue(annotation, key); Object value = getValue(annotation, key);
if (value != null) { if (value != null) {
attributes.add(key, value); attributes.add(key, value);
} }
...@@ -141,45 +155,91 @@ public class AnnotatedElementUtils { ...@@ -141,45 +155,91 @@ public class AnnotatedElementUtils {
} }
/** /**
* Process all annotations of the specified annotation type and recursively all * Process all annotations of the specified {@code annotationType} and
* meta-annotations on the specified type. * recursively all meta-annotations on the specified {@code element}.
*
* <p>If the {@code traverseClassHierarchy} flag is {@code true} and the sought
* annotation is neither <em>directly present</em> on the given element nor
* present on the given element as a meta-annotation, then the algorithm will
* recursively search through the class hierarchy of the given element.
*
* @param element the annotated element * @param element the annotated element
* @param annotationType the annotation type to find. Only items of the specified * @param annotationType the annotation type to find
* type or meta-annotations of the specified type will be processed. * @param traverseClassHierarchy whether or not to traverse up the class
* @param processor the processor * hierarchy recursively
* @param processor the processor to delegate to
* @return the result of the processor * @return the result of the processor
*/ */
private static <T> T process(AnnotatedElement element, String annotationType, Processor<T> processor) { private static <T> T process(AnnotatedElement element, String annotationType, boolean traverseClassHierarchy,
return doProcess(element, annotationType, processor, new HashSet<AnnotatedElement>(), 0); Processor<T> processor) {
return doProcess(element, annotationType, traverseClassHierarchy, processor, new HashSet<AnnotatedElement>(), 0);
} }
private static <T> T doProcess(AnnotatedElement element, String annotationType, /**
Processor<T> processor, Set<AnnotatedElement> visited, int depth) { * Perform the search algorithm for the {@link #process} method, avoiding
* endless recursion by tracking which annotated elements have already been
* <em>visited</em>.
*
* <p>The {@code metaDepth} parameter represents the depth of the annotation
* relative to the initial element. For example, an annotation that is
* <em>present</em> on the element will have a depth of 0; a meta-annotation
* will have a depth of 1; and a meta-meta-annotation will have a depth of 2.
*
* @param element the annotated element
* @param annotationType the annotation type to find
* @param traverseClassHierarchy whether or not to traverse up the class
* hierarchy recursively
* @param processor the processor to delegate to
* @param visited the set of annotated elements that have already been visited
* @param metaDepth the depth of the annotation relative to the initial element
* @return the result of the processor
*/
private static <T> T doProcess(AnnotatedElement element, String annotationType, boolean traverseClassHierarchy,
Processor<T> processor, Set<AnnotatedElement> visited, int metaDepth) {
if (visited.add(element)) { if (visited.add(element)) {
for (Annotation annotation : element.getAnnotations()) {
if (annotation.annotationType().getName().equals(annotationType) || depth > 0) { Annotation[] annotations = traverseClassHierarchy ? element.getDeclaredAnnotations()
T result = processor.process(annotation, depth); : element.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation.annotationType().getName().equals(annotationType) || metaDepth > 0) {
T result = processor.process(annotation, metaDepth);
if (result != null) { if (result != null) {
return result; return result;
} }
result = doProcess(annotation.annotationType(), annotationType, processor, visited, depth + 1); result = doProcess(annotation.annotationType(), annotationType, traverseClassHierarchy, processor,
visited, metaDepth + 1);
if (result != null) { if (result != null) {
processor.postProcess(annotation, result); processor.postProcess(annotation, result);
return result; return result;
} }
} }
} }
for (Annotation annotation : element.getAnnotations()) {
if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) { for (Annotation annotation : annotations) {
T result = doProcess(annotation.annotationType(), annotationType, processor, visited, depth); if (!isInJavaLangAnnotationPackage(annotation)) {
T result = doProcess(annotation.annotationType(), annotationType, traverseClassHierarchy,
processor, visited, metaDepth);
if (result != null) { if (result != null) {
processor.postProcess(annotation, result); processor.postProcess(annotation, result);
return result; return result;
} }
} }
} }
if (traverseClassHierarchy && element instanceof Class) {
Class<?> superclass = ((Class<?>) element).getSuperclass();
if (superclass != null && !superclass.equals(Object.class)) {
T result = doProcess(superclass, annotationType, traverseClassHierarchy, processor, visited,
metaDepth);
if (result != null) {
return result;
}
}
}
} }
return null; return null;
} }
...@@ -192,13 +252,18 @@ public class AnnotatedElementUtils { ...@@ -192,13 +252,18 @@ public class AnnotatedElementUtils {
/** /**
* Called to process the annotation. * Called to process the annotation.
*
* <p>The {@code metaDepth} parameter represents the depth of the
* annotation relative to the initial element. For example, an annotation
* that is <em>present</em> on the element will have a depth of 0; a
* meta-annotation will have a depth of 1; and a meta-meta-annotation
* will have a depth of 2.
*
* @param annotation the annotation to process * @param annotation the annotation to process
* @param depth the depth of the annotation relative to the initial match. * @param metaDepth the depth of the annotation relative to the initial element
* For example, a matched annotation will have a depth of 0, a meta-annotation
* 1 and a meta-meta-annotation 2
* @return the result of the processing or {@code null} to continue * @return the result of the processing or {@code null} to continue
*/ */
T process(Annotation annotation, int depth); T process(Annotation annotation, int metaDepth);
void postProcess(Annotation annotation, T result); void postProcess(Annotation annotation, T result);
} }
......
...@@ -278,7 +278,7 @@ public abstract class AnnotationUtils { ...@@ -278,7 +278,7 @@ public abstract class AnnotationUtils {
Set<Annotation> visited) { Set<Annotation> visited) {
Assert.notNull(clazz, "Class must not be null"); Assert.notNull(clazz, "Class must not be null");
A annotation = clazz.getAnnotation(annotationType); A annotation = clazz.getDeclaredAnnotation(annotationType);
if (annotation != null) { if (annotation != null) {
return annotation; return annotation;
} }
...@@ -288,7 +288,7 @@ public abstract class AnnotationUtils { ...@@ -288,7 +288,7 @@ public abstract class AnnotationUtils {
return annotation; return annotation;
} }
} }
for (Annotation ann : clazz.getAnnotations()) { for (Annotation ann : clazz.getDeclaredAnnotations()) {
if (!isInJavaLangAnnotationPackage(ann) && visited.add(ann)) { if (!isInJavaLangAnnotationPackage(ann) && visited.add(ann)) {
annotation = findAnnotation(ann.annotationType(), annotationType, visited); annotation = findAnnotation(ann.annotationType(), annotationType, visited);
if (annotation != null) { if (annotation != null) {
......
...@@ -22,10 +22,13 @@ import java.lang.annotation.Inherited; ...@@ -22,10 +22,13 @@ import java.lang.annotation.Inherited;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import java.util.Arrays;
import org.junit.Test; import org.junit.Test;
import org.springframework.util.MultiValueMap;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.springframework.core.annotation.AnnotatedElementUtils.*;
/** /**
* Unit tests for {@link AnnotatedElementUtils}. * Unit tests for {@link AnnotatedElementUtils}.
...@@ -35,37 +38,82 @@ import static org.junit.Assert.*; ...@@ -35,37 +38,82 @@ import static org.junit.Assert.*;
*/ */
public class AnnotatedElementUtilsTests { public class AnnotatedElementUtilsTests {
@Test
public void getAllAnnotationAttributesOnClassWithLocalAnnotation() {
MultiValueMap<String, Object> attributes = getAllAnnotationAttributes(TxConfig.class,
Transactional.class.getName());
assertNotNull("Annotation attributes map for @Transactional on TxConfig", attributes);
assertEquals("value for TxConfig.", Arrays.asList("TxConfig"), attributes.get("value"));
}
/**
* If the "value" entry contains both "DerivedTxConfig" AND "TxConfig", then
* the algorithm is accidentally picking up shadowed annotations of the same
* type within the class hierarchy. Such undesirable behavior would cause the
* logic in {@link org.springframework.context.annotation.ProfileCondition}
* to fail.
*
* @see org.springframework.core.env.EnvironmentIntegrationTests#mostSpecificDerivedClassDrivesEnvironment_withDevEnvAndDerivedDevConfigClass
*/
@Test
public void getAllAnnotationAttributesOnClassWithLocalAnnotationThatShadowsAnnotationFromSuperclass() {
MultiValueMap<String, Object> attributes = getAllAnnotationAttributes(DerivedTxConfig.class,
Transactional.class.getName());
assertNotNull("Annotation attributes map for @Transactional on DerivedTxConfig", attributes);
assertEquals("value for DerivedTxConfig.", Arrays.asList("DerivedTxConfig"), attributes.get("value"));
}
/**
* Note: this functionality is required by {@link org.springframework.context.annotation.ProfileCondition}.
*
* @see org.springframework.core.env.EnvironmentIntegrationTests
*/
@Test
public void getAllAnnotationAttributesOnClassWithMultipleComposedAnnotations() {
MultiValueMap<String, Object> attributes = getAllAnnotationAttributes(TxFromMultipleComposedAnnotations.class,
Transactional.class.getName());
assertNotNull("Annotation attributes map for @Transactional on TxFromMultipleComposedAnnotations", attributes);
assertEquals("value for TxFromMultipleComposedAnnotations.", Arrays.asList("TxComposed1", "TxComposed2"),
attributes.get("value"));
}
@Test
public void getAnnotationAttributesOnClassWithLocalAnnotation() {
AnnotationAttributes attributes = getAnnotationAttributes(TxConfig.class, Transactional.class.getName());
assertNotNull("Annotation attributes for @Transactional on TxConfig", attributes);
assertEquals("value for TxConfig.", "TxConfig", attributes.getString("value"));
}
@Test
public void getAnnotationAttributesOnClassWithLocalAnnotationThatShadowsAnnotationFromSuperclass() {
AnnotationAttributes attributes = getAnnotationAttributes(DerivedTxConfig.class, Transactional.class.getName());
assertNotNull("Annotation attributes for @Transactional on DerivedTxConfig", attributes);
assertEquals("value for DerivedTxConfig.", "DerivedTxConfig", attributes.getString("value"));
}
@Test @Test
public void getAnnotationAttributesOnMetaCycleAnnotatedClassWithMissingTargetMetaAnnotation() { public void getAnnotationAttributesOnMetaCycleAnnotatedClassWithMissingTargetMetaAnnotation() {
AnnotationAttributes attributes = AnnotatedElementUtils.getAnnotationAttributes(MetaCycleAnnotatedClass.class, AnnotationAttributes attributes = getAnnotationAttributes(MetaCycleAnnotatedClass.class,
Transactional.class.getName()); Transactional.class.getName());
assertNull("Should not find annotation attributes for @Transactional on MetaCycleAnnotatedClass", attributes); assertNull("Should not find annotation attributes for @Transactional on MetaCycleAnnotatedClass", attributes);
} }
@Test @Test
public void getAnnotationAttributesFavorsInheritedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { public void getAnnotationAttributesFavorsInheritedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() {
AnnotationAttributes attributes = AnnotatedElementUtils.getAnnotationAttributes( AnnotationAttributes attributes = getAnnotationAttributes(SubSubClassWithInheritedAnnotation.class,
SubSubClassWithInheritedAnnotation.class, Transactional.class.getName()); Transactional.class.getName());
assertNotNull(attributes); assertNotNull("AnnotationAttributes for @Transactional on SubSubClassWithInheritedAnnotation", attributes);
assertEquals("readOnly flag for SubSubClassWithInheritedAnnotation.", true, attributes.getBoolean("readOnly"));
// By inspecting SubSubClassWithInheritedAnnotation, one might expect that the
// readOnly flag should be true, since the immediate superclass is annotated with
// @Composed2; however, with the current implementation the readOnly flag will be
// false since @Transactional is declared as @Inherited.
assertFalse("readOnly flag for SubSubClassWithInheritedAnnotation", attributes.getBoolean("readOnly"));
} }
@Test @Test
public void getAnnotationAttributesFavorsInheritedComposedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { public void getAnnotationAttributesFavorsInheritedComposedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() {
AnnotationAttributes attributes = AnnotatedElementUtils.getAnnotationAttributes( AnnotationAttributes attributes = getAnnotationAttributes(SubSubClassWithInheritedComposedAnnotation.class,
SubSubClassWithInheritedComposedAnnotation.class, Transactional.class.getName()); Transactional.class.getName());
assertNotNull(attributes); assertNotNull("AnnotationAttributtes for @Transactional on SubSubClassWithInheritedComposedAnnotation.",
attributes);
// By inspecting SubSubClassWithInheritedComposedAnnotation, one might expect that assertEquals("readOnly flag for SubSubClassWithInheritedComposedAnnotation.", true,
// the readOnly flag should be true, since the immediate superclass is annotated attributes.getBoolean("readOnly"));
// with @Composed2; however, with the current implementation the readOnly flag
// will be false since @Composed1 is declared as @Inherited.
assertFalse("readOnly flag", attributes.getBoolean("readOnly"));
} }
...@@ -96,12 +144,16 @@ public class AnnotatedElementUtilsTests { ...@@ -96,12 +144,16 @@ public class AnnotatedElementUtilsTests {
static class MetaCycleAnnotatedClass { static class MetaCycleAnnotatedClass {
} }
// -------------------------------------------------------------------------
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE) @Target(ElementType.TYPE)
@Documented @Documented
@Inherited @Inherited
@interface Transactional { @interface Transactional {
String value() default "";
boolean readOnly() default false; boolean readOnly() default false;
} }
...@@ -120,6 +172,18 @@ public class AnnotatedElementUtilsTests { ...@@ -120,6 +172,18 @@ public class AnnotatedElementUtilsTests {
@interface Composed2 { @interface Composed2 {
} }
@Transactional("TxComposed1")
@Retention(RetentionPolicy.RUNTIME)
@interface TxComposed1 {
}
@Transactional("TxComposed2")
@Retention(RetentionPolicy.RUNTIME)
@interface TxComposed2 {
}
// -------------------------------------------------------------------------
@Transactional @Transactional
static class ClassWithInheritedAnnotation { static class ClassWithInheritedAnnotation {
} }
...@@ -142,4 +206,17 @@ public class AnnotatedElementUtilsTests { ...@@ -142,4 +206,17 @@ public class AnnotatedElementUtilsTests {
static class SubSubClassWithInheritedComposedAnnotation extends SubClassWithInheritedComposedAnnotation { static class SubSubClassWithInheritedComposedAnnotation extends SubClassWithInheritedComposedAnnotation {
} }
@Transactional("TxConfig")
static class TxConfig {
}
@Transactional("DerivedTxConfig")
static class DerivedTxConfig extends TxConfig {
}
@TxComposed1
@TxComposed2
static class TxFromMultipleComposedAnnotations {
}
} }
...@@ -111,30 +111,26 @@ public class AnnotationUtilsTests { ...@@ -111,30 +111,26 @@ public class AnnotationUtilsTests {
assertEquals("meta1", component.value()); assertEquals("meta1", component.value());
} }
/**
* @since 4.0.3
*/
@Test @Test
public void findAnnotationFavorsInheritedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { public void findAnnotationFavorsInheritedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() {
Transactional transactional = AnnotationUtils.findAnnotation(SubSubClassWithInheritedAnnotation.class, Transactional transactional = AnnotationUtils.findAnnotation(SubSubClassWithInheritedAnnotation.class,
Transactional.class); Transactional.class);
assertNotNull(transactional); assertNotNull(transactional);
assertTrue("readOnly flag for SubSubClassWithInheritedAnnotation", transactional.readOnly());
// By inspecting SubSubClassWithInheritedAnnotation, one might expect that the
// readOnly flag should be true, since the immediate superclass is annotated with
// @Composed2; however, with the current implementation the readOnly flag will be
// false since @Transactional is declared as @Inherited.
assertFalse("readOnly flag for SubSubClassWithInheritedAnnotation", transactional.readOnly());
} }
/**
* @since 4.0.3
*/
@Test @Test
public void findAnnotationFavorsInheritedComposedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { public void findAnnotationFavorsInheritedComposedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() {
Component component = AnnotationUtils.findAnnotation(SubSubClassWithInheritedMetaAnnotation.class, Component component = AnnotationUtils.findAnnotation(SubSubClassWithInheritedMetaAnnotation.class,
Component.class); Component.class);
assertNotNull(component); assertNotNull(component);
assertEquals("meta2", component.value());
// By inspecting SubSubClassWithInheritedMetaAnnotation, one might expect that
// "meta2" should be found, since the immediate superclass is annotated with
// @Meta2; however, with the current implementation "meta1" will be found since
// @Meta1 is declared as @Inherited.
assertEquals("meta1", component.value());
} }
@Test @Test
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册