diff --git a/spring-core/src/test/java/org/springframework/core/type/classreading/AsmAnnotationIntrospectionFailureTests.java b/spring-core/src/test/java/org/springframework/core/type/classreading/AsmAnnotationIntrospectionFailureTests.java new file mode 100644 index 00000000000..a8f8c9beff5 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/type/classreading/AsmAnnotationIntrospectionFailureTests.java @@ -0,0 +1,431 @@ +/* + * Copyright 2002-2021 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 + * + * https://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.type.classreading; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; +import java.util.Arrays; + +import org.junit.Test; + +import org.springframework.core.OverridingClassLoader; +import org.springframework.core.annotation.AliasFor; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.ReflectionUtils; + +import static org.junit.Assert.*; + +/** + * Tests that trigger annotation introspection failures when using Spring's + * ASM-based annotation processing and ensure that they are dealt with correctly. + * + *
This test class also contains tests that verify the behavior when using
+ * the JDK's standard reflection APIs for comparison with Spring's ASM-based
+ * annotation processing.
+ *
+ * @author Sam Brannen
+ * @since 5.1.21
+ */
+public class AsmAnnotationIntrospectionFailureTests {
+
+ private final ClassLoader standardClassLoader = getClass().getClassLoader();
+
+ private final FilteringClassLoader filteringClassLoader = new FilteringClassLoader(standardClassLoader);
+
+
+ @Test
+ public void jdkLoadableAnnotationsAndClassAttributes() throws Exception {
+ Annotation filteredAnnotation = retrieveAnnotationViaReflection(AnnotationWithClassAttributeClass.class.getName(),
+ FilteredAnnotation.class.getName(), standardClassLoader);
+ assertNotNull("@FilteredAnnotation", filteredAnnotation);
+ assertEquals(FilteredAnnotation.class, filteredAnnotation.annotationType());
+
+ Object text = getTextInAnnotationWithClassAttribute(standardClassLoader);
+ assertEquals("enigma", text);
+
+ Object clazz = getClazzInAnnotationWithClassAttribute(standardClassLoader);
+ assertEquals(FilteredType.class, clazz);
+ }
+
+ @Test
+ public void jdkNonLoadableAnnotationsAndClassAttributes() throws Exception {
+ Annotation filteredAnnotation = retrieveAnnotationViaReflection(AnnotationWithClassAttributeClass.class.getName(),
+ FilteredAnnotation.class.getName(), filteringClassLoader);
+ assertNull("JDK ignores annotations whose types cannot be loaded", filteredAnnotation);
+
+ Object text = getTextInAnnotationWithClassAttribute(filteringClassLoader);
+ assertEquals("JDK allows access to attributes unaffected by non-loadable types", "enigma", text);
+
+ // JDK throws TypeNotPresentException when accessing attributes with non-loadable types
+ Exception exception = assertThrows(TypeNotPresentException.class, () -> getClazzInAnnotationWithClassAttribute(filteringClassLoader));
+ assertCauseInstanceOfClassNotFoundException(exception);
+ }
+
+ @Test
+ public void springAsmNonLoadableAnnotationsAndClassAttributesWithDirectlyPresentAnnotations() throws Exception {
+ AnnotationMetadata annotationMetadata = annotationMetadata(AnnotationWithClassAttributeClass.class.getName(), filteringClassLoader);
+ assertNotNull("Spring scans all annotation metadata even if some types cannot be loaded", annotationMetadata);
+
+ Object filteredAnnotation = annotationMetadata.getAnnotationAttributes(FilteredAnnotation.class.getName());
+ assertNull("Spring ignores annotations whose types cannot be loaded", filteredAnnotation);
+
+ AnnotationAttributes annotationWithClassAttribute =
+ (AnnotationAttributes) annotationMetadata.getAnnotationAttributes(AnnotationWithClassAttribute.class.getName());
+ assertNotNull("@AnnotationWithClassAttribute", annotationWithClassAttribute);
+ assertEquals("Spring allows access to attributes unaffected by non-loadable types",
+ "enigma", annotationWithClassAttribute.getString("text"));
+
+ // Spring throws an IllegalArgumentException when accessing attributes with non-loadable types
+ Exception exception = assertThrows(IllegalArgumentException.class, () -> annotationWithClassAttribute.getClass("clazz"));
+ assertCauseInstanceOfClassNotFoundException(exception);
+ }
+
+ @Test
+ public void springAsmNonLoadableAnnotationsAndClassAttributesWithMergedComposedAnnotations() throws Exception {
+ AnnotationMetadata annotationMetadata = annotationMetadata(ComposedAnnotationClass.class.getName(), filteringClassLoader);
+ assertNotNull("Spring scans all annotation metadata even if some types cannot be loaded", annotationMetadata);
+
+ Object filteredAnnotation = annotationMetadata.getAnnotationAttributes(FilteredAnnotation.class.getName());
+ assertNull("Spring ignores annotations whose types cannot be loaded", filteredAnnotation);
+
+ AnnotationAttributes composedAnnotation =
+ (AnnotationAttributes) annotationMetadata.getAnnotationAttributes(ComposedAnnotation.class.getName());
+ assertNotNull("@ComposedAnnotation", composedAnnotation);
+
+ assertEquals("Spring allows access to attributes unaffected by non-loadable types",
+ "enigma", composedAnnotation.getString("text"));
+
+ // Spring throws an IllegalArgumentException when accessing attributes with non-loadable types
+ Exception exception = assertThrows(IllegalArgumentException.class, () -> composedAnnotation.getClass("example1"));
+ assertCauseInstanceOfClassNotFoundException(exception);
+ exception = assertThrows(IllegalArgumentException.class, () -> composedAnnotation.getClass("example2"));
+ assertCauseInstanceOfClassNotFoundException(exception);
+ }
+
+ @Test
+ public void jdkLoadableNestedAnnotationAttributes() throws Exception {
+ Annotation nestedAnnotationContainer = retrieveAnnotationViaReflection(NestedAnnotationContainerClass.class.getName(),
+ NestedAnnotationContainer.class.getName(), standardClassLoader);
+ assertNotNull("@NestedAnnotationContainer", nestedAnnotationContainer);
+ assertEquals(NestedAnnotationContainer.class, nestedAnnotationContainer.annotationType());
+
+ Object text = getTextInNestedAnnotationContainer(standardClassLoader);
+ assertEquals("enigma", text);
+
+ FilteredNestedAnnotation nested = (FilteredNestedAnnotation) getNestedInNestedAnnotationContainer(standardClassLoader);
+ assertNotNull("@FilteredNestedAnnotation", nested);
+ assertEquals("nested!", nested.value());
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void jdkNonLoadableNestedAnnotationAttributes() throws Exception {
+ Class> nestedAnnotationContainerClass = Class.forName(NestedAnnotationContainerClass.class.getName(), false, filteringClassLoader);
+ assertNotNull("A class annotated with @NestedAnnotationContainer", nestedAnnotationContainerClass);
+
+ Class extends Annotation> nestedAnnotationContainerType =
+ (Class extends Annotation>) Class.forName(NestedAnnotationContainer.class.getName(), false, filteringClassLoader);
+ assertNotNull("The @NestedAnnotationContainer class itself", nestedAnnotationContainerType);
+
+ // JDK throws NoClassDefFoundError when accessing any annotations on a class, if any
+ // annotation declared on the class has nested annotation attributes with non-loadable types.
+ // This is because the JDK eagerly instantiates all annotation types present on
+ // an annotated element even if certain annotation instances will never be used.
+ Error error = assertThrows(NoClassDefFoundError.class, () -> nestedAnnotationContainerClass.getAnnotation(nestedAnnotationContainerType));
+ assertCauseInstanceOfClassNotFoundException(error);
+ }
+
+ @Test
+ public void springAsmNonLoadableNestedAnnotationAttributes() throws Exception {
+ AnnotationMetadata annotationMetadata = annotationMetadata(NestedAnnotationContainerClass.class.getName(), filteringClassLoader);
+ assertNotNull("Spring scans all annotation metadata even if some types cannot be loaded", annotationMetadata);
+
+ // In contrast to the JDK, Spring actually allows access to annotation metadata
+ // on a class even if another annotation declared on the class has nested annotation
+ // attributes with non-loadable types.
+ //
+ // In this scenario we can access @AnnotationWithClassAttribute metadata, but we cannot
+ // access @NestedAnnotationContainer metadata (see assertThrows() below).
+ AnnotationAttributes annotationWithClassAttribute =
+ (AnnotationAttributes) annotationMetadata.getAnnotationAttributes(AnnotationWithClassAttribute.class.getName());
+ assertNotNull("@AnnotationWithClassAttribute", annotationWithClassAttribute);
+ assertEquals("enigma", annotationWithClassAttribute.getString("text"));
+
+ // Similar to the JDK, Spring throws NoClassDefFoundError when accessing annotation attributes
+ // if the annotation has nested annotation attributes with non-loadable types. This is because
+ // getAnnotationAttributes() delegates to AnnotationReadingVisitorUtils.convertClassValues()
+ // which indirectly delegates to AnnotationUtils.getAttributeMethods() which delegates to
+ // Class.getDeclaredMethods() (where the class is the annotation type), and this is
+ // equivalent to what the JDK does when preparing to instantiate all annotations present on the class.
+ Error error = assertThrows(NoClassDefFoundError.class, () -> annotationMetadata.getAnnotationAttributes(NestedAnnotationContainer.class.getName()));
+ assertCauseInstanceOfClassNotFoundException(error);
+ }
+
+ @Test
+ public void jdkLoadableEnumAttributes() throws Exception {
+ Annotation annotationWithEnumAttribute = retrieveAnnotationViaReflection(AnnotationWithEnumAttributeClass.class.getName(),
+ AnnotationWithEnumAttribute.class.getName(), standardClassLoader);
+ assertNotNull("@AnnotationWithEnumAttribute", annotationWithEnumAttribute);
+ assertEquals(AnnotationWithEnumAttribute.class, annotationWithEnumAttribute.annotationType());
+
+ Object color = getColorInAnnotationWithEnumAttribute(standardClassLoader);
+ assertEquals(Color.GREEN, color);
+
+ Object filteredEnum = getFilteredEnumInAnnotationWithEnumAttribute(standardClassLoader);
+ assertEquals(FilteredEnum.FOO, filteredEnum);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void jdkNonLoadableEnumAttributes() throws Exception {
+ Class> annotationWithEnumAttributeClass = Class.forName(AnnotationWithEnumAttributeClass.class.getName(), false, filteringClassLoader);
+ assertNotNull("A class annotated with @AnnotationWithEnumAttribute", annotationWithEnumAttributeClass);
+
+ Class extends Annotation> annotationWithEnumAttributeType =
+ (Class extends Annotation>) Class.forName(AnnotationWithEnumAttribute.class.getName(), false, filteringClassLoader);
+ assertNotNull("The @AnnotationWithEnumAttribute class itself", annotationWithEnumAttributeType);
+
+ // JDK throws NoClassDefFoundError when accessing any annotations on a class, if any
+ // annotation declared on the class has enum attributes with non-loadable types.
+ // This is because the JDK eagerly instantiates all annotation types present on
+ // an annotated element even if certain annotation instances will never be used.
+ Error error = assertThrows(NoClassDefFoundError.class, () -> annotationWithEnumAttributeClass.getAnnotation(annotationWithEnumAttributeType));
+ assertCauseInstanceOfClassNotFoundException(error);
+ }
+
+ @Test
+ public void springAsmNonLoadableEnumAttributes() throws Exception {
+ AnnotationMetadata annotationMetadata = annotationMetadata(AnnotationWithEnumAttributeClass.class.getName(), filteringClassLoader);
+ assertNotNull("Spring scans all annotation metadata even if some types cannot be loaded", annotationMetadata);
+
+ // In contrast to the JDK, Spring actually allows access to annotation metadata
+ // on a class even if another annotation declared on the class has enum attributes
+ // with non-loadable types.
+ //
+ // In this scenario we can access @AnnotationWithClassAttribute metadata, but we cannot
+ // access @AnnotationWithEnumAttribute metadata (see assertThrows() below).
+ AnnotationAttributes annotationWithClassAttribute =
+ (AnnotationAttributes) annotationMetadata.getAnnotationAttributes(AnnotationWithClassAttribute.class.getName());
+ assertNotNull("@AnnotationWithClassAttribute", annotationWithClassAttribute);
+ assertEquals("enigma", annotationWithClassAttribute.getString("text"));
+
+ // Similar to the JDK, Spring throws NoClassDefFoundError when accessing annotation
+ // attributes if the annotation has enum attributes with non-loadable types. This is because
+ // getAnnotationAttributes() delegates to AnnotationReadingVisitorUtils.convertClassValues()
+ // which indirectly delegates to AnnotationUtils.getAttributeMethods() which delegates to
+ // Class.getDeclaredMethods() (where the class is the annotation type), and this is
+ // equivalent to what the JDK does when preparing to instantiate all annotations present on the class.
+ Error error = assertThrows(NoClassDefFoundError.class, () -> annotationMetadata.getAnnotationAttributes(AnnotationWithEnumAttribute.class.getName()));
+ assertCauseInstanceOfClassNotFoundException(error);
+ }
+
+ private static Object getTextInAnnotationWithClassAttribute(ClassLoader classLoader) throws Exception {
+ Annotation annotation = retrieveAnnotationViaReflection(AnnotationWithClassAttributeClass.class.getName(),
+ AnnotationWithClassAttribute.class.getName(), classLoader);
+ return invokeAttributeMethod(annotation, "text");
+ }
+
+ private static Object getClazzInAnnotationWithClassAttribute(ClassLoader classLoader) throws Exception {
+ Annotation annotation = retrieveAnnotationViaReflection(AnnotationWithClassAttributeClass.class.getName(),
+ AnnotationWithClassAttribute.class.getName(), classLoader);
+ return invokeAttributeMethod(annotation, "clazz");
+ }
+
+ private static Object getTextInNestedAnnotationContainer(ClassLoader classLoader) throws Exception {
+ Annotation annotation = retrieveAnnotationViaReflection(NestedAnnotationContainerClass.class.getName(),
+ NestedAnnotationContainer.class.getName(), classLoader);
+ return invokeAttributeMethod(annotation, "text");
+ }
+
+ private static Object getNestedInNestedAnnotationContainer(ClassLoader classLoader) throws Exception {
+ Annotation annotation = retrieveAnnotationViaReflection(NestedAnnotationContainerClass.class.getName(),
+ NestedAnnotationContainer.class.getName(), classLoader);
+ return invokeAttributeMethod(annotation, "nested");
+ }
+
+ private static Object getColorInAnnotationWithEnumAttribute(ClassLoader classLoader) throws Exception {
+ Annotation annotation = retrieveAnnotationViaReflection(AnnotationWithEnumAttributeClass.class.getName(),
+ AnnotationWithEnumAttribute.class.getName(), classLoader);
+ return invokeAttributeMethod(annotation, "color");
+ }
+
+ private static Object getFilteredEnumInAnnotationWithEnumAttribute(ClassLoader classLoader) throws Exception {
+ Annotation annotation = retrieveAnnotationViaReflection(AnnotationWithEnumAttributeClass.class.getName(),
+ AnnotationWithEnumAttribute.class.getName(), classLoader);
+ return invokeAttributeMethod(annotation, "filteredEnum");
+ }
+
+ private static Annotation retrieveAnnotationViaReflection(String annotatedClassName, String annotationName, ClassLoader classLoader) throws Exception {
+ Class> annotatedClass = Class.forName(annotatedClassName, false, classLoader);
+ return Arrays.stream(annotatedClass.getAnnotations())
+ .filter(annotation -> annotation.annotationType().getName().equals(annotationName))
+ .findFirst()
+ .orElse(null);
+ }
+
+ private static Object invokeAttributeMethod(Annotation annotation, String attributeName) throws Exception {
+ Method method = annotation.annotationType().getMethod(attributeName);
+ method.setAccessible(true);
+ return ReflectionUtils.invokeMethod(method, annotation);
+ }
+
+ private static AnnotationMetadata annotationMetadata(String className, ClassLoader classLoader) {
+ try {
+ return new SimpleMetadataReaderFactory(classLoader).getMetadataReader(className).getAnnotationMetadata();
+ }
+ catch (Exception ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static