Support reading nested annotations via ASM

Background

  Spring 3.1 introduced the @ComponentScan annotation, which can accept
  an optional array of include and/or exclude @Filter annotations, e.g.

     @ComponentScan(
         basePackages = "com.acme.app",
         includeFilters = { @Filter(MyStereotype.class), ... }
     )
     @Configuration
     public class AppConfig { ... }

  @ComponentScan and other annotations related to @Configuration class
  processing such as @Import, @ImportResource and the @Enable*
  annotations are parsed using reflection in certain code paths, e.g.
  when registered directly against AnnotationConfigApplicationContext,
  and via ASM in other code paths, e.g. when a @Configuration class is
  discovered via an XML bean definition or when included via the
  @Import annotation.

  The ASM-based approach is designed to avoid premature classloading of
  user types and is instrumental in providing tooling support (STS, etc).

  Prior to this commit, the ASM-based routines for reading annotation
  attributes were unable to recurse into nested annotations, such as in
  the @Filter example above. Prior to Spring 3.1 this was not a problem,
  because prior to @ComponentScan, there were no cases of nested
  annotations in the framework.

  This limitation manifested itself in cases where users encounter
  the ASM-based annotation parsing code paths AND declare
  @ComponentScan annotations with explicit nested @Filter annotations.
  In these cases, the 'includeFilters' and 'excludeFilters' attributes
  are simply empty where they should be populated, causing the framework
  to ignore the filter directives and provide incorrect results from
  component scanning.

  The purpose of this change then, is to introduce the capability on the
  ASM side to recurse into nested annotations and annotation arrays. The
  challenge in doing so is that the nested annotations themselves cannot
  be realized as annotation instances, so must be represented as a
  nested Map (or, as described below, the new AnnotationAttributes type).

  Furthermore, the reflection-based annotation parsing must also be
  updated to treat nested annotations in a similar fashion; even though
  the reflection-based approach has no problem accessing nested
  annotations (it just works out of the box), for substitutability
  against the AnnotationMetadata SPI, both ASM- and reflection-based
  implementations should return the same results in any case. Therefore,
  the reflection-based StandardAnnotationMetadata has also been updated
  with an optional 'nestedAnnotationsAsMap' constructor argument that is
  false by default to preserve compatibility in the rare case that
  StandardAnnotationMetadata is being used outside the core framework.
  Within the framework, all uses of StandardAnnotationMetadata have been
  updated to set this new flag to true, meaning that nested annotation
  results will be consistent regardless the parsing approach used.

  Spr9031Tests corners this bug and demonstrates that nested @Filter
  annotations can be parsed and read in both the ASM- and
  reflection-based paths.

Major changes

 - AnnotationAttributes has been introduced as a concrete
   LinkedHashMap<String, Object> to be used anywhere annotation
   attributes are accessed, providing error reporting on attribute
   lookup and convenient type-safe access to common annotation types
   such as String, String[], boolean, int, and nested annotation and
   annotation arrays, with the latter two also returned as
   AnnotationAttributes instances.

 - AnnotationUtils#getAnnotationAttributes methods now return
   AnnotationAttributes instances, even though for binary compatibility
   the signatures of these methods have been preserved as returning
   Map<String, Object>.

 - AnnotationAttributes#forMap provides a convenient mechanism for
   adapting any Map<String, Object> into an AnnotationAttributes
   instance. In the case that the Map is already actually of
   type AnnotationAttributes, it is simply casted and returned.
   Otherwise, the map is supplied to the AnnotationAttributes(Map)
   constructor and wrapped in common collections style.

 - The protected MetadataUtils#attributesFor(Metadata, Class) provides
   further convenience in the many locations throughout the
   .context.annotation packagage that depend on annotation attribute
   introspection.

 - ASM-based core.type.classreading package reworked

   Specifically, AnnotationAttributesReadingVisitor has been enhanced to
   support recursive reading of annotations and annotation arrays, for
   example in @ComponentScan's nested array of @Filter annotations,
   ensuring that nested AnnotationAttributes objects are populated as
   described above.

   AnnotationAttributesReadingVisitor has also been refactored for
   clarity, being broken up into several additional ASM
   AnnotationVisitor implementations. Given that all types are
   package-private here, these changes represent no risk to binary
   compatibility.

 - Reflection-based StandardAnnotationMetadata updated

   As described above, the 'nestedAnnotationsAsMap' constructor argument
   has been added, and all framework-internal uses of this class have
   been updated to set this flag to true.

Issue: SPR-7979, SPR-8719, SPR-9031
This commit is contained in:
Chris Beams
2011-04-06 15:41:08 +08:00
parent 905d17d444
commit d9f7fdd120
17 changed files with 918 additions and 196 deletions

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2002-2012 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.core.annotation;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import org.junit.Test;
/**
* Unit tests for {@link AnnotationAttributes}.
*
* @author Chris Beams
* @since 3.1.1
*/
public class AnnotationAttributesTests {
enum Color { RED, WHITE, BLUE }
@Test
public void testTypeSafeAttributeAccess() {
AnnotationAttributes a = new AnnotationAttributes();
a.put("name", "dave");
a.put("names", new String[] { "dave", "frank", "hal" });
a.put("bool1", true);
a.put("bool2", false);
a.put("color", Color.RED);
a.put("clazz", Integer.class);
a.put("classes", new Class<?>[] { Number.class, Short.class, Integer.class });
a.put("number", 42);
a.put("numbers", new int[] { 42, 43 });
AnnotationAttributes anno = new AnnotationAttributes();
anno.put("value", 10);
anno.put("name", "algernon");
a.put("anno", anno);
a.put("annoArray", new AnnotationAttributes[] { anno });
assertThat(a.getString("name"), equalTo("dave"));
assertThat(a.getStringArray("names"), equalTo(new String[] { "dave", "frank", "hal" }));
assertThat(a.getBoolean("bool1"), equalTo(true));
assertThat(a.getBoolean("bool2"), equalTo(false));
assertThat(a.getEnum("color", Color.class), equalTo(Color.RED));
assertTrue(a.getClass("clazz", Number.class).equals(Integer.class));
assertThat(a.getClassArray("classes"), equalTo(new Class[] { Number.class, Short.class, Integer.class }));
assertThat(a.getInt("number"), equalTo(42));
assertThat(a.getAnnotation("anno").getInt("value"), equalTo(10));
assertThat(a.getAnnotationArray("annoArray")[0].getString("name"), equalTo("algernon"));
}
@Test
public void getEnum_emptyAttributeName() {
AnnotationAttributes a = new AnnotationAttributes();
a.put("color", "RED");
try {
a.getEnum("", Color.class);
fail();
} catch (IllegalArgumentException ex) {
assertThat(ex.getMessage(), equalTo("attributeName must not be null or empty"));
}
try {
a.getEnum(null, Color.class);
fail();
} catch (IllegalArgumentException ex) {
assertThat(ex.getMessage(), equalTo("attributeName must not be null or empty"));
}
}
@Test
public void getEnum_notFound() {
AnnotationAttributes a = new AnnotationAttributes();
a.put("color", "RED");
try {
a.getEnum("colour", Color.class);
fail();
} catch (IllegalArgumentException ex) {
assertThat(ex.getMessage(), equalTo("Attribute 'colour' not found"));
}
}
@Test
public void getEnum_typeMismatch() {
AnnotationAttributes a = new AnnotationAttributes();
a.put("color", "RED");
try {
a.getEnum("color", Color.class);
fail();
} catch (IllegalArgumentException ex) {
String expected =
"Attribute 'color' is of type [String], but [Color] was expected";
assertThat(ex.getMessage().substring(0, expected.length()), equalTo(expected));
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2009 the original author or authors.
* Copyright 2002-2012 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,99 +16,203 @@
package org.springframework.core.type;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Map;
import java.util.Set;
import junit.framework.TestCase;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
import org.springframework.stereotype.Component;
/**
* Unit tests demonstrating that the reflection-based {@link StandardAnnotationMetadata}
* and ASM-based {@code AnnotationMetadataReadingVisitor} produce identical output.
*
* @author Juergen Hoeller
* @author Chris Beams
*/
public class AnnotationMetadataTests extends TestCase {
public class AnnotationMetadataTests {
@Test
public void testStandardAnnotationMetadata() throws IOException {
StandardAnnotationMetadata annInfo = new StandardAnnotationMetadata(AnnotatedComponent.class);
doTestAnnotationInfo(annInfo);
doTestMethodAnnotationInfo(annInfo);
AnnotationMetadata metadata = new StandardAnnotationMetadata(AnnotatedComponent.class, true);
doTestAnnotationInfo(metadata);
doTestMethodAnnotationInfo(metadata);
}
@Test
public void testAsmAnnotationMetadata() throws IOException {
MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory();
MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(AnnotatedComponent.class.getName());
doTestAnnotationInfo(metadataReader.getAnnotationMetadata());
doTestMethodAnnotationInfo(metadataReader.getAnnotationMetadata());
AnnotationMetadata metadata = metadataReader.getAnnotationMetadata();
doTestAnnotationInfo(metadata);
doTestMethodAnnotationInfo(metadata);
}
/**
* In order to preserve backward-compatibility, {@link StandardAnnotationMetadata}
* defaults to return nested annotations and annotation arrays as actual
* Annotation instances. It is recommended for compatibility with ASM-based
* AnnotationMetadata implementations to set the 'nestedAnnotationsAsMap' flag to
* 'true' as is done in the main test above.
*/
@Test
public void testStandardAnnotationMetadata_nestedAnnotationsAsMap_false() throws IOException {
AnnotationMetadata metadata = new StandardAnnotationMetadata(AnnotatedComponent.class);
AnnotationAttributes specialAttrs = (AnnotationAttributes) metadata.getAnnotationAttributes(SpecialAttr.class.getName());
Annotation[] nestedAnnoArray = (Annotation[])specialAttrs.get("nestedAnnoArray");
assertThat(nestedAnnoArray[0], instanceOf(NestedAnno.class));
}
private void doTestAnnotationInfo(AnnotationMetadata metadata) {
assertEquals(AnnotatedComponent.class.getName(), metadata.getClassName());
assertFalse(metadata.isInterface());
assertFalse(metadata.isAbstract());
assertTrue(metadata.isConcrete());
assertTrue(metadata.hasSuperClass());
assertEquals(Object.class.getName(), metadata.getSuperClassName());
assertEquals(1, metadata.getInterfaceNames().length);
assertEquals(Serializable.class.getName(), metadata.getInterfaceNames()[0]);
assertThat(metadata.getClassName(), is(AnnotatedComponent.class.getName()));
assertThat(metadata.isInterface(), is(false));
assertThat(metadata.isAbstract(), is(false));
assertThat(metadata.isConcrete(), is(true));
assertThat(metadata.hasSuperClass(), is(true));
assertThat(metadata.getSuperClassName(), is(Object.class.getName()));
assertThat(metadata.getInterfaceNames().length, is(1));
assertThat(metadata.getInterfaceNames()[0], is(Serializable.class.getName()));
assertTrue(metadata.hasAnnotation(Component.class.getName()));
assertTrue(metadata.hasAnnotation(Scope.class.getName()));
assertTrue(metadata.hasAnnotation(SpecialAttr.class.getName()));
assertEquals(3, metadata.getAnnotationTypes().size());
assertTrue(metadata.getAnnotationTypes().contains(Component.class.getName()));
assertTrue(metadata.getAnnotationTypes().contains(Scope.class.getName()));
assertTrue(metadata.getAnnotationTypes().contains(SpecialAttr.class.getName()));
assertThat(metadata.hasAnnotation(Component.class.getName()), is(true));
assertThat(metadata.hasAnnotation(Scope.class.getName()), is(true));
assertThat(metadata.hasAnnotation(SpecialAttr.class.getName()), is(true));
assertThat(metadata.getAnnotationTypes().size(), is(3));
assertThat(metadata.getAnnotationTypes().contains(Component.class.getName()), is(true));
assertThat(metadata.getAnnotationTypes().contains(Scope.class.getName()), is(true));
assertThat(metadata.getAnnotationTypes().contains(SpecialAttr.class.getName()), is(true));
Map<String, Object> compAttrs = metadata.getAnnotationAttributes(Component.class.getName());
assertEquals(1, compAttrs.size());
assertEquals("myName", compAttrs.get("value"));
Map<String, Object> scopeAttrs = metadata.getAnnotationAttributes(Scope.class.getName());
assertEquals(1, scopeAttrs.size());
assertEquals("myScope", scopeAttrs.get("value"));
Map<String, Object> specialAttrs = metadata.getAnnotationAttributes(SpecialAttr.class.getName());
assertEquals(2, specialAttrs.size());
assertEquals(String.class, specialAttrs.get("clazz"));
assertEquals(Thread.State.NEW, specialAttrs.get("state"));
Map<String, Object> specialAttrsString = metadata.getAnnotationAttributes(SpecialAttr.class.getName(), true);
assertEquals(String.class.getName(), specialAttrsString .get("clazz"));
assertEquals(Thread.State.NEW, specialAttrsString.get("state"));
AnnotationAttributes compAttrs = (AnnotationAttributes) metadata.getAnnotationAttributes(Component.class.getName());
assertThat(compAttrs.size(), is(1));
assertThat(compAttrs.getString("value"), is("myName"));
AnnotationAttributes scopeAttrs = (AnnotationAttributes) metadata.getAnnotationAttributes(Scope.class.getName());
assertThat(scopeAttrs.size(), is(1));
assertThat(scopeAttrs.getString("value"), is("myScope"));
{ // perform tests with classValuesAsString = false (the default)
AnnotationAttributes specialAttrs = (AnnotationAttributes) metadata.getAnnotationAttributes(SpecialAttr.class.getName());
assertThat(specialAttrs.size(), is(6));
assertTrue(String.class.isAssignableFrom(specialAttrs.getClass("clazz", Object.class)));
assertThat(specialAttrs.getEnum("state", Thread.State.class), is(Thread.State.NEW));
AnnotationAttributes nestedAnno = specialAttrs.getAnnotation("nestedAnno");
assertThat("na", is(nestedAnno.getString("value")));
assertThat(nestedAnno.getEnum("anEnum", SomeEnum.class), is(SomeEnum.LABEL1));
assertArrayEquals(new Class[]{String.class}, (Class[])nestedAnno.get("classArray"));
AnnotationAttributes[] nestedAnnoArray = specialAttrs.getAnnotationArray("nestedAnnoArray");
assertThat(nestedAnnoArray.length, is(2));
assertThat(nestedAnnoArray[0].getString("value"), is("default"));
assertThat(nestedAnnoArray[0].getEnum("anEnum", SomeEnum.class), is(SomeEnum.DEFAULT));
assertArrayEquals(new Class[]{Void.class}, (Class[])nestedAnnoArray[0].get("classArray"));
assertThat(nestedAnnoArray[1].getString("value"), is("na1"));
assertThat(nestedAnnoArray[1].getEnum("anEnum", SomeEnum.class), is(SomeEnum.LABEL2));
assertArrayEquals(new Class[]{Number.class}, (Class[])nestedAnnoArray[1].get("classArray"));
assertArrayEquals(new Class[]{Number.class}, nestedAnnoArray[1].getClassArray("classArray"));
AnnotationAttributes optional = specialAttrs.getAnnotation("optional");
assertThat(optional.getString("value"), is("optional"));
assertThat(optional.getEnum("anEnum", SomeEnum.class), is(SomeEnum.DEFAULT));
assertArrayEquals(new Class[]{Void.class}, (Class[])optional.get("classArray"));
assertArrayEquals(new Class[]{Void.class}, optional.getClassArray("classArray"));
AnnotationAttributes[] optionalArray = specialAttrs.getAnnotationArray("optionalArray");
assertThat(optionalArray.length, is(1));
assertThat(optionalArray[0].getString("value"), is("optional"));
assertThat(optionalArray[0].getEnum("anEnum", SomeEnum.class), is(SomeEnum.DEFAULT));
assertArrayEquals(new Class[]{Void.class}, (Class[])optionalArray[0].get("classArray"));
assertArrayEquals(new Class[]{Void.class}, optionalArray[0].getClassArray("classArray"));
}
{ // perform tests with classValuesAsString = true
AnnotationAttributes specialAttrs = (AnnotationAttributes) metadata.getAnnotationAttributes(SpecialAttr.class.getName(), true);
assertThat(specialAttrs.size(), is(6));
assertThat(specialAttrs.get("clazz"), is((Object)String.class.getName()));
assertThat(specialAttrs.getString("clazz"), is(String.class.getName()));
AnnotationAttributes nestedAnno = specialAttrs.getAnnotation("nestedAnno");
assertArrayEquals(new String[]{String.class.getName()}, (String[])nestedAnno.getStringArray("classArray"));
assertArrayEquals(new String[]{String.class.getName()}, nestedAnno.getStringArray("classArray"));
AnnotationAttributes[] nestedAnnoArray = specialAttrs.getAnnotationArray("nestedAnnoArray");
assertArrayEquals(new String[]{Void.class.getName()}, (String[])nestedAnnoArray[0].get("classArray"));
assertArrayEquals(new String[]{Void.class.getName()}, nestedAnnoArray[0].getStringArray("classArray"));
assertArrayEquals(new String[]{Number.class.getName()}, (String[])nestedAnnoArray[1].get("classArray"));
assertArrayEquals(new String[]{Number.class.getName()}, nestedAnnoArray[1].getStringArray("classArray"));
AnnotationAttributes optional = specialAttrs.getAnnotation("optional");
assertArrayEquals(new String[]{Void.class.getName()}, (String[])optional.get("classArray"));
assertArrayEquals(new String[]{Void.class.getName()}, optional.getStringArray("classArray"));
AnnotationAttributes[] optionalArray = specialAttrs.getAnnotationArray("optionalArray");
assertArrayEquals(new String[]{Void.class.getName()}, (String[])optionalArray[0].get("classArray"));
assertArrayEquals(new String[]{Void.class.getName()}, optionalArray[0].getStringArray("classArray"));
}
}
private void doTestMethodAnnotationInfo(AnnotationMetadata classMetadata) {
Set<MethodMetadata> methods = classMetadata.getAnnotatedMethods(Autowired.class.getName());
assertEquals(1, methods.size());
assertThat(methods.size(), is(1));
for (MethodMetadata methodMetadata : methods) {
assertTrue(methodMetadata.isAnnotated(Autowired.class.getName()));
assertThat(methodMetadata.isAnnotated(Autowired.class.getName()), is(true));
}
}
public static enum SomeEnum {
LABEL1, LABEL2, DEFAULT;
}
@Target({})
@Retention(RetentionPolicy.RUNTIME)
public @interface NestedAnno {
String value() default "default";
SomeEnum anEnum() default SomeEnum.DEFAULT;
Class<?>[] classArray() default Void.class;
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface SpecialAttr {
Class clazz();
Class<?> clazz();
Thread.State state();
NestedAnno nestedAnno();
NestedAnno[] nestedAnnoArray();
NestedAnno optional() default @NestedAnno(value="optional", anEnum=SomeEnum.DEFAULT, classArray=Void.class);
NestedAnno[] optionalArray() default {@NestedAnno(value="optional", anEnum=SomeEnum.DEFAULT, classArray=Void.class)};
}
@Component("myName")
@Scope("myScope")
@SpecialAttr(clazz = String.class, state = Thread.State.NEW)
@SpecialAttr(clazz = String.class, state = Thread.State.NEW,
nestedAnno = @NestedAnno(value = "na", anEnum = SomeEnum.LABEL1, classArray = {String.class}),
nestedAnnoArray = {
@NestedAnno,
@NestedAnno(value = "na1", anEnum = SomeEnum.LABEL2, classArray = {Number.class})
})
@SuppressWarnings({"serial", "unused"})
private static class AnnotatedComponent implements Serializable {
@Autowired