From 89391fd94c93642cf67ca824c01ea669a652ed03 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 11 Mar 2026 13:52:19 +0100 Subject: [PATCH] Consistently ignore non-loadable annotation types with ClassFile/ASM Uses ClassFileAnnotationMetadata name for actual AnnotationMetadata. Moves JSR-305 dependency to compile-only for all spring-core tests. Closes gh-36432 --- spring-core/spring-core.gradle | 3 +- .../ClassFileAnnotationDelegate.java | 177 +++++++++ .../ClassFileAnnotationMetadata.java | 363 ++++++++++++------ .../classreading/ClassFileClassMetadata.java | 308 --------------- .../classreading/ClassFileMetadataReader.java | 7 +- .../classreading/ClassFileMethodMetadata.java | 4 +- .../AnnotatedElementUtilsTests.java | 30 +- .../annotation/AnnotationFilterTests.java | 7 - .../core/annotation/AnnotationUtilsTests.java | 6 - .../DefaultAnnotationMetadataTests.java | 9 +- .../SimpleAnnotationMetadataTests.java | 8 +- 11 files changed, 441 insertions(+), 481 deletions(-) create mode 100644 spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileAnnotationDelegate.java delete mode 100644 spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileClassMetadata.java diff --git a/spring-core/spring-core.gradle b/spring-core/spring-core.gradle index 12084363348..4c0df4f03d5 100644 --- a/spring-core/spring-core.gradle +++ b/spring-core/spring-core.gradle @@ -92,7 +92,7 @@ dependencies { optional("org.jetbrains.kotlin:kotlin-stdlib") optional("org.jetbrains.kotlinx:kotlinx-coroutines-core") optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") - testCompileOnly("com.github.ben-manes.caffeine:caffeine") + testCompileOnly("com.google.code.findbugs:jsr305") testFixturesImplementation("io.projectreactor:reactor-test") testFixturesImplementation("org.assertj:assertj-core") testFixturesImplementation("org.junit.jupiter:junit-jupiter") @@ -100,7 +100,6 @@ dependencies { testFixturesImplementation("org.xmlunit:xmlunit-assertj") testImplementation("com.fasterxml.jackson.core:jackson-databind") testImplementation("com.fasterxml.woodstox:woodstox-core") - testImplementation("com.google.code.findbugs:jsr305") testImplementation("com.squareup.okhttp3:mockwebserver3") testImplementation("io.projectreactor:reactor-test") testImplementation("io.projectreactor.tools:blockhound") diff --git a/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileAnnotationDelegate.java b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileAnnotationDelegate.java new file mode 100644 index 00000000000..79c56155697 --- /dev/null +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileAnnotationDelegate.java @@ -0,0 +1,177 @@ +/* + * Copyright 2002-present 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.classfile.Annotation; +import java.lang.classfile.AnnotationElement; +import java.lang.classfile.AnnotationValue; +import java.lang.classfile.attribute.RuntimeVisibleAnnotationsAttribute; +import java.lang.constant.ClassDesc; +import java.lang.reflect.Array; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.jspecify.annotations.Nullable; + +import org.springframework.core.annotation.AnnotationFilter; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.util.ClassUtils; + +/** + * Parse {@link RuntimeVisibleAnnotationsAttribute} into {@link MergedAnnotations} + * instances. + * + * @author Brian Clozel + * @author Juergen Hoeller + * @since 7.0 + */ +abstract class ClassFileAnnotationDelegate { + + static MergedAnnotations createMergedAnnotations( + String className, RuntimeVisibleAnnotationsAttribute annotationAttribute, @Nullable ClassLoader classLoader) { + + Set> annotations = annotationAttribute.annotations() + .stream() + .map(ann -> createMergedAnnotation(className, ann, classLoader)) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + return MergedAnnotations.of(annotations); + } + + @SuppressWarnings("unchecked") + private static @Nullable MergedAnnotation createMergedAnnotation( + String className, Annotation annotation, @Nullable ClassLoader classLoader) { + + String typeName = fromTypeDescriptor(annotation.className().stringValue()); + if (AnnotationFilter.PLAIN.matches(typeName)) { + return null; + } + try { + // Fail early when annotation type is not loadable (before resolving annotation values) + Class annotationType = (Class) ClassUtils.forName(typeName, classLoader); + Map attributes = new LinkedHashMap<>(4); + for (AnnotationElement element : annotation.elements()) { + Object annotationValue = readAnnotationValue(className, element.value(), classLoader); + if (annotationValue != null) { + attributes.put(element.name().stringValue(), annotationValue); + } + } + Map compactedAttributes = (attributes.isEmpty() ? Collections.emptyMap() : attributes); + return MergedAnnotation.of(classLoader, new Source(annotation), annotationType, compactedAttributes); + } + catch (ClassNotFoundException | LinkageError ex) { + // Non-loadable annotation type -> ignore. + return null; + } + } + + private static @Nullable Object readAnnotationValue( + String className, AnnotationValue elementValue, @Nullable ClassLoader classLoader) { + + switch (elementValue) { + case AnnotationValue.OfConstant constantValue -> { + return constantValue.resolvedValue(); + } + case AnnotationValue.OfAnnotation annotationValue -> { + return createMergedAnnotation(className, annotationValue.annotation(), classLoader); + } + case AnnotationValue.OfClass classValue -> { + return fromTypeDescriptor(classValue.className().stringValue()); + } + case AnnotationValue.OfEnum enumValue -> { + return parseEnum(enumValue, classLoader); + } + case AnnotationValue.OfArray arrayValue -> { + return parseArrayValue(className, classLoader, arrayValue); + } + } + } + + private static String fromTypeDescriptor(String descriptor) { + ClassDesc classDesc = ClassDesc.ofDescriptor(descriptor); + return (classDesc.isPrimitive() ? classDesc.displayName() : + classDesc.packageName() + "." + classDesc.displayName()); + } + + private static Object parseArrayValue(String className, @Nullable ClassLoader classLoader, AnnotationValue.OfArray arrayValue) { + if (arrayValue.values().isEmpty()) { + return new Object[0]; + } + Stream stream = arrayValue.values().stream(); + switch (arrayValue.values().getFirst()) { + case AnnotationValue.OfInt _ -> { + return stream.map(AnnotationValue.OfInt.class::cast).mapToInt(AnnotationValue.OfInt::intValue).toArray(); + } + case AnnotationValue.OfDouble _ -> { + return stream.map(AnnotationValue.OfDouble.class::cast).mapToDouble(AnnotationValue.OfDouble::doubleValue).toArray(); + } + case AnnotationValue.OfLong _ -> { + return stream.map(AnnotationValue.OfLong.class::cast).mapToLong(AnnotationValue.OfLong::longValue).toArray(); + } + default -> { + Class arrayElementType = resolveArrayElementType(arrayValue.values(), classLoader); + return stream + .map(rawValue -> readAnnotationValue(className, rawValue, classLoader)) + .toArray(length -> (Object[]) Array.newInstance(arrayElementType, length)); + } + } + } + + @SuppressWarnings("unchecked") + private static > Enum parseEnum(AnnotationValue.OfEnum enumValue, @Nullable ClassLoader classLoader) { + Class enumClass = (Class) loadEnumClass(enumValue, classLoader); + return Enum.valueOf(enumClass, enumValue.constantName().stringValue()); + } + + private static Class loadEnumClass(AnnotationValue.OfEnum enumValue, @Nullable ClassLoader classLoader) { + String className = fromTypeDescriptor(enumValue.className().stringValue()); + return ClassUtils.resolveClassName(className, classLoader); + } + + private static Class resolveArrayElementType(List values, @Nullable ClassLoader classLoader) { + AnnotationValue firstValue = values.getFirst(); + switch (firstValue) { + case AnnotationValue.OfConstant constantValue -> { + return constantValue.resolvedValue().getClass(); + } + case AnnotationValue.OfAnnotation _ -> { + return MergedAnnotation.class; + } + case AnnotationValue.OfClass _ -> { + return String.class; + } + case AnnotationValue.OfEnum enumValue -> { + return loadEnumClass(enumValue, classLoader); + } + default -> { + return Object.class; + } + } + } + + + record Source(Annotation entryName) { + } + +} diff --git a/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileAnnotationMetadata.java b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileAnnotationMetadata.java index 7577a65083f..95d3981d84c 100644 --- a/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileAnnotationMetadata.java +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileAnnotationMetadata.java @@ -16,165 +16,294 @@ package org.springframework.core.type.classreading; -import java.lang.classfile.Annotation; -import java.lang.classfile.AnnotationElement; -import java.lang.classfile.AnnotationValue; +import java.lang.classfile.AccessFlags; +import java.lang.classfile.ClassModel; +import java.lang.classfile.Interfaces; +import java.lang.classfile.MethodModel; +import java.lang.classfile.Superclass; +import java.lang.classfile.attribute.InnerClassInfo; +import java.lang.classfile.attribute.InnerClassesAttribute; +import java.lang.classfile.attribute.NestHostAttribute; import java.lang.classfile.attribute.RuntimeVisibleAnnotationsAttribute; -import java.lang.constant.ClassDesc; -import java.lang.reflect.Array; +import java.lang.classfile.constantpool.ClassEntry; +import java.lang.reflect.AccessFlag; import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.LinkedHashSet; import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; import org.jspecify.annotations.Nullable; -import org.springframework.core.annotation.AnnotationFilter; -import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.MethodMetadata; import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; /** - * Parse {@link RuntimeVisibleAnnotationsAttribute} into {@link MergedAnnotations} - * instances. + * {@link AnnotationMetadata} implementation that leverages + * the {@link java.lang.classfile.ClassFile} API. * * @author Brian Clozel + * @author Juergen Hoeller * @since 7.0 */ -abstract class ClassFileAnnotationMetadata { +final class ClassFileAnnotationMetadata implements AnnotationMetadata { - static MergedAnnotations createMergedAnnotations( - String className, RuntimeVisibleAnnotationsAttribute annotationAttribute, @Nullable ClassLoader classLoader) { + private final String className; - Set> annotations = annotationAttribute.annotations() - .stream() - .map(ann -> createMergedAnnotation(className, ann, classLoader)) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - return MergedAnnotations.of(annotations); + private final AccessFlags accessFlags; + + private final @Nullable String enclosingClassName; + + private final @Nullable String superClassName; + + private final boolean independentInnerClass; + + private final Set interfaceNames; + + private final Set memberClassNames; + + private final Set declaredMethods; + + private final MergedAnnotations mergedAnnotations; + + private @Nullable Set annotationTypes; + + + ClassFileAnnotationMetadata(String className, AccessFlags accessFlags, @Nullable String enclosingClassName, + @Nullable String superClassName, boolean independentInnerClass, Set interfaceNames, + Set memberClassNames, Set declaredMethods, MergedAnnotations mergedAnnotations) { + + this.className = className; + this.accessFlags = accessFlags; + this.enclosingClassName = enclosingClassName; + this.superClassName = (!className.endsWith(".package-info")) ? superClassName : null; + this.independentInnerClass = independentInnerClass; + this.interfaceNames = interfaceNames; + this.memberClassNames = memberClassNames; + this.declaredMethods = declaredMethods; + this.mergedAnnotations = mergedAnnotations; + } + + + @Override + public String getClassName() { + return this.className; + } + + @Override + public boolean isInterface() { + return this.accessFlags.has(AccessFlag.INTERFACE); + } + + @Override + public boolean isAnnotation() { + return this.accessFlags.has(AccessFlag.ANNOTATION); + } + + @Override + public boolean isAbstract() { + return this.accessFlags.has(AccessFlag.ABSTRACT); + } + + @Override + public boolean isFinal() { + return this.accessFlags.has(AccessFlag.FINAL); + } + + @Override + public boolean isIndependent() { + return (this.enclosingClassName == null || this.independentInnerClass); + } + + @Override + public @Nullable String getEnclosingClassName() { + return this.enclosingClassName; + } + + @Override + public @Nullable String getSuperClassName() { + return this.superClassName; + } + + @Override + public String[] getInterfaceNames() { + return StringUtils.toStringArray(this.interfaceNames); + } + + @Override + public String[] getMemberClassNames() { + return StringUtils.toStringArray(this.memberClassNames); } - @SuppressWarnings("unchecked") - private static @Nullable MergedAnnotation createMergedAnnotation( - String className, Annotation annotation, @Nullable ClassLoader classLoader) { + @Override + public MergedAnnotations getAnnotations() { + return this.mergedAnnotations; + } - String typeName = fromTypeDescriptor(annotation.className().stringValue()); - if (AnnotationFilter.PLAIN.matches(typeName)) { - return null; + @Override + public Set getAnnotationTypes() { + Set annotationTypes = this.annotationTypes; + if (annotationTypes == null) { + annotationTypes = Collections.unmodifiableSet( + AnnotationMetadata.super.getAnnotationTypes()); + this.annotationTypes = annotationTypes; } - Map attributes = new LinkedHashMap<>(4); - try { - for (AnnotationElement element : annotation.elements()) { - Object annotationValue = readAnnotationValue(className, element.value(), classLoader); - if (annotationValue != null) { - attributes.put(element.name().stringValue(), annotationValue); - } + return annotationTypes; + } + + @Override + public Set getAnnotatedMethods(String annotationName) { + Set result = new LinkedHashSet<>(4); + for (MethodMetadata annotatedMethod : this.declaredMethods) { + if (annotatedMethod.isAnnotated(annotationName)) { + result.add(annotatedMethod); } - Map compactedAttributes = (attributes.isEmpty() ? Collections.emptyMap() : attributes); - Class annotationType = (Class) ClassUtils.forName(typeName, classLoader); - return MergedAnnotation.of(classLoader, new Source(annotation), annotationType, compactedAttributes); - } - catch (ClassNotFoundException | LinkageError ex) { - return null; } + return Collections.unmodifiableSet(result); } - private static @Nullable Object readAnnotationValue( - String className, AnnotationValue elementValue, @Nullable ClassLoader classLoader) { + @Override + public Set getDeclaredMethods() { + return Collections.unmodifiableSet(this.declaredMethods); + } - switch (elementValue) { - case AnnotationValue.OfConstant constantValue -> { - return constantValue.resolvedValue(); - } - case AnnotationValue.OfAnnotation annotationValue -> { - return createMergedAnnotation(className, annotationValue.annotation(), classLoader); - } - case AnnotationValue.OfClass classValue -> { - return fromTypeDescriptor(classValue.className().stringValue()); - } - case AnnotationValue.OfEnum enumValue -> { - return parseEnum(enumValue, classLoader); - } - case AnnotationValue.OfArray arrayValue -> { - return parseArrayValue(className, classLoader, arrayValue); - } - } + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof ClassFileAnnotationMetadata that && this.className.equals(that.className))); } - private static String fromTypeDescriptor(String descriptor) { - ClassDesc classDesc = ClassDesc.ofDescriptor(descriptor); - return classDesc.isPrimitive() ? classDesc.displayName() : - classDesc.packageName() + "." + classDesc.displayName(); + @Override + public int hashCode() { + return this.className.hashCode(); } - private static Class loadClass(String className, @Nullable ClassLoader classLoader) { - String name = fromTypeDescriptor(className); - return ClassUtils.resolveClassName(name, classLoader); + @Override + public String toString() { + return this.className; } - private static Object parseArrayValue(String className, @Nullable ClassLoader classLoader, AnnotationValue.OfArray arrayValue) { - if (arrayValue.values().isEmpty()) { - return new Object[0]; - } - Stream stream = arrayValue.values().stream(); - switch (arrayValue.values().getFirst()) { - case AnnotationValue.OfInt _ -> { - return stream.map(AnnotationValue.OfInt.class::cast).mapToInt(AnnotationValue.OfInt::intValue).toArray(); - } - case AnnotationValue.OfDouble _ -> { - return stream.map(AnnotationValue.OfDouble.class::cast).mapToDouble(AnnotationValue.OfDouble::doubleValue).toArray(); - } - case AnnotationValue.OfLong _ -> { - return stream.map(AnnotationValue.OfLong.class::cast).mapToLong(AnnotationValue.OfLong::longValue).toArray(); - } - default -> { - Class arrayElementType = resolveArrayElementType(arrayValue.values(), classLoader); - return stream - .map(rawValue -> readAnnotationValue(className, rawValue, classLoader)) - .toArray(s -> (Object[]) Array.newInstance(arrayElementType, s)); + + static ClassFileAnnotationMetadata of(ClassModel classModel, @Nullable ClassLoader classLoader) { + Builder builder = new Builder(classLoader); + builder.classEntry(classModel.thisClass()); + String currentClassName = classModel.thisClass().name().stringValue(); + classModel.elementStream().forEach(classElement -> { + switch (classElement) { + case AccessFlags flags -> { + builder.accessFlags(flags); + } + case NestHostAttribute _ -> { + builder.enclosingClass(classModel.thisClass()); + } + case InnerClassesAttribute innerClasses -> { + builder.nestMembers(currentClassName, innerClasses); + } + case RuntimeVisibleAnnotationsAttribute annotationsAttribute -> { + builder.mergedAnnotations(ClassFileAnnotationDelegate.createMergedAnnotations( + ClassUtils.convertResourcePathToClassName(currentClassName), annotationsAttribute, classLoader)); + } + case Superclass superclass -> { + builder.superClass(superclass); + } + case Interfaces interfaces -> { + builder.interfaces(interfaces); + } + case MethodModel method -> { + builder.method(method); + } + default -> { + // ignore class element + } } - } + }); + return builder.build(); } - @SuppressWarnings("unchecked") - private static @Nullable > Enum parseEnum(AnnotationValue.OfEnum enumValue, @Nullable ClassLoader classLoader) { - String enumClassName = fromTypeDescriptor(enumValue.className().stringValue()); - try { - Class enumClass = (Class) ClassUtils.forName(enumClassName, classLoader); - return Enum.valueOf(enumClass, enumValue.constantName().stringValue()); + + static class Builder { + + private final ClassLoader classLoader; + + private String className; + + private AccessFlags accessFlags; + + private Set innerAccessFlags; + + private @Nullable String enclosingClassName; + + private @Nullable String superClassName; + + private Set interfaceNames = new LinkedHashSet<>(4); + + private Set memberClassNames = new LinkedHashSet<>(4); + + private Set declaredMethods = new LinkedHashSet<>(4); + + private MergedAnnotations mergedAnnotations = MergedAnnotations.of(Collections.emptySet()); + + public Builder(ClassLoader classLoader) { + this.classLoader = classLoader; } - catch (ClassNotFoundException | LinkageError ex) { - return null; + + void classEntry(ClassEntry classEntry) { + this.className = ClassUtils.convertResourcePathToClassName(classEntry.name().stringValue()); } - } - private static Class resolveArrayElementType(List values, @Nullable ClassLoader classLoader) { - AnnotationValue firstValue = values.getFirst(); - switch (firstValue) { - case AnnotationValue.OfConstant constantValue -> { - return constantValue.resolvedValue().getClass(); - } - case AnnotationValue.OfAnnotation _ -> { - return MergedAnnotation.class; - } - case AnnotationValue.OfClass _ -> { - return String.class; - } - case AnnotationValue.OfEnum enumValue -> { - return loadClass(enumValue.className().stringValue(), classLoader); + void accessFlags(AccessFlags accessFlags) { + this.accessFlags = accessFlags; + } + + void enclosingClass(ClassEntry thisClass) { + String thisClassName = thisClass.name().stringValue(); + int currentClassIndex = thisClassName.lastIndexOf('$'); + this.enclosingClassName = ClassUtils.convertResourcePathToClassName(thisClassName.substring(0, currentClassIndex)); + } + + void superClass(Superclass superClass) { + this.superClassName = ClassUtils.convertResourcePathToClassName(superClass.superclassEntry().name().stringValue()); + } + + void interfaces(Interfaces interfaces) { + for (ClassEntry entry : interfaces.interfaces()) { + this.interfaceNames.add(ClassUtils.convertResourcePathToClassName(entry.name().stringValue())); } - default -> { - return Object.class; + } + + void nestMembers(String currentClassName, InnerClassesAttribute innerClasses) { + for (InnerClassInfo classInfo : innerClasses.classes()) { + String innerClassName = classInfo.innerClass().name().stringValue(); + if (currentClassName.equals(innerClassName)) { + // the current class is an inner class + this.innerAccessFlags = classInfo.flags(); + } + classInfo.outerClass().ifPresent(outerClass -> { + if (outerClass.name().stringValue().equals(currentClassName)) { + // collecting data about actual inner classes + this.memberClassNames.add(ClassUtils.convertResourcePathToClassName(innerClassName)); + } + }); } } - } + void mergedAnnotations(MergedAnnotations mergedAnnotations) { + this.mergedAnnotations = mergedAnnotations; + } - record Source(Annotation entryName) { + void method(MethodModel method) { + ClassFileMethodMetadata classFileMethodMetadata = ClassFileMethodMetadata.of(method, this.classLoader); + if (!classFileMethodMetadata.isSynthetic() && !classFileMethodMetadata.isDefaultConstructor()) { + this.declaredMethods.add(classFileMethodMetadata); + } + } + + ClassFileAnnotationMetadata build() { + boolean independentInnerClass = (this.enclosingClassName != null) && this.innerAccessFlags.contains(AccessFlag.STATIC); + return new ClassFileAnnotationMetadata(this.className, this.accessFlags, this.enclosingClassName, this.superClassName, + independentInnerClass, this.interfaceNames, this.memberClassNames, this.declaredMethods, this.mergedAnnotations); + } } } diff --git a/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileClassMetadata.java b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileClassMetadata.java deleted file mode 100644 index 859d773f143..00000000000 --- a/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileClassMetadata.java +++ /dev/null @@ -1,308 +0,0 @@ -/* - * Copyright 2002-present 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.classfile.AccessFlags; -import java.lang.classfile.ClassModel; -import java.lang.classfile.Interfaces; -import java.lang.classfile.MethodModel; -import java.lang.classfile.Superclass; -import java.lang.classfile.attribute.InnerClassInfo; -import java.lang.classfile.attribute.InnerClassesAttribute; -import java.lang.classfile.attribute.NestHostAttribute; -import java.lang.classfile.attribute.RuntimeVisibleAnnotationsAttribute; -import java.lang.classfile.constantpool.ClassEntry; -import java.lang.reflect.AccessFlag; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.Set; - -import org.jspecify.annotations.Nullable; - -import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.core.type.AnnotationMetadata; -import org.springframework.core.type.MethodMetadata; -import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; - -/** - * {@link AnnotationMetadata} implementation that leverages - * the {@link java.lang.classfile.ClassFile} API. - * - * @author Brian Clozel - * @since 7.0 - */ -class ClassFileClassMetadata implements AnnotationMetadata { - - private final String className; - - private final AccessFlags accessFlags; - - private final @Nullable String enclosingClassName; - - private final @Nullable String superClassName; - - private final boolean independentInnerClass; - - private final Set interfaceNames; - - private final Set memberClassNames; - - private final Set declaredMethods; - - private final MergedAnnotations mergedAnnotations; - - private @Nullable Set annotationTypes; - - - ClassFileClassMetadata(String className, AccessFlags accessFlags, @Nullable String enclosingClassName, - @Nullable String superClassName, boolean independentInnerClass, Set interfaceNames, - Set memberClassNames, Set declaredMethods, MergedAnnotations mergedAnnotations) { - - this.className = className; - this.accessFlags = accessFlags; - this.enclosingClassName = enclosingClassName; - this.superClassName = (!className.endsWith(".package-info")) ? superClassName : null; - this.independentInnerClass = independentInnerClass; - this.interfaceNames = interfaceNames; - this.memberClassNames = memberClassNames; - this.declaredMethods = declaredMethods; - this.mergedAnnotations = mergedAnnotations; - } - - - @Override - public String getClassName() { - return this.className; - } - - @Override - public boolean isInterface() { - return this.accessFlags.has(AccessFlag.INTERFACE); - } - - @Override - public boolean isAnnotation() { - return this.accessFlags.has(AccessFlag.ANNOTATION); - } - - @Override - public boolean isAbstract() { - return this.accessFlags.has(AccessFlag.ABSTRACT); - } - - @Override - public boolean isFinal() { - return this.accessFlags.has(AccessFlag.FINAL); - } - - @Override - public boolean isIndependent() { - return (this.enclosingClassName == null || this.independentInnerClass); - } - - @Override - public @Nullable String getEnclosingClassName() { - return this.enclosingClassName; - } - - @Override - public @Nullable String getSuperClassName() { - return this.superClassName; - } - - @Override - public String[] getInterfaceNames() { - return StringUtils.toStringArray(this.interfaceNames); - } - - @Override - public String[] getMemberClassNames() { - return StringUtils.toStringArray(this.memberClassNames); - } - - @Override - public MergedAnnotations getAnnotations() { - return this.mergedAnnotations; - } - - @Override - public Set getAnnotationTypes() { - Set annotationTypes = this.annotationTypes; - if (annotationTypes == null) { - annotationTypes = Collections.unmodifiableSet( - AnnotationMetadata.super.getAnnotationTypes()); - this.annotationTypes = annotationTypes; - } - return annotationTypes; - } - - @Override - public Set getAnnotatedMethods(String annotationName) { - Set result = new LinkedHashSet<>(4); - for (MethodMetadata annotatedMethod : this.declaredMethods) { - if (annotatedMethod.isAnnotated(annotationName)) { - result.add(annotatedMethod); - } - } - return Collections.unmodifiableSet(result); - } - - @Override - public Set getDeclaredMethods() { - return Collections.unmodifiableSet(this.declaredMethods); - } - - - @Override - public boolean equals(@Nullable Object other) { - return (this == other || (other instanceof ClassFileClassMetadata that && this.className.equals(that.className))); - } - - @Override - public int hashCode() { - return this.className.hashCode(); - } - - @Override - public String toString() { - return this.className; - } - - - static ClassFileClassMetadata of(ClassModel classModel, @Nullable ClassLoader classLoader) { - Builder builder = new Builder(classLoader); - builder.classEntry(classModel.thisClass()); - String currentClassName = classModel.thisClass().name().stringValue(); - classModel.elementStream().forEach(classElement -> { - switch (classElement) { - case AccessFlags flags -> { - builder.accessFlags(flags); - } - case NestHostAttribute _ -> { - builder.enclosingClass(classModel.thisClass()); - } - case InnerClassesAttribute innerClasses -> { - builder.nestMembers(currentClassName, innerClasses); - } - case RuntimeVisibleAnnotationsAttribute annotationsAttribute -> { - builder.mergedAnnotations(ClassFileAnnotationMetadata.createMergedAnnotations( - ClassUtils.convertResourcePathToClassName(currentClassName), annotationsAttribute, classLoader)); - } - case Superclass superclass -> { - builder.superClass(superclass); - } - case Interfaces interfaces -> { - builder.interfaces(interfaces); - } - case MethodModel method -> { - builder.method(method); - } - default -> { - // ignore class element - } - } - }); - return builder.build(); - } - - - static class Builder { - - private final ClassLoader clasLoader; - - private String className; - - private AccessFlags accessFlags; - - private Set innerAccessFlags; - - private @Nullable String enclosingClassName; - - private @Nullable String superClassName; - - private Set interfaceNames = new LinkedHashSet<>(4); - - private Set memberClassNames = new LinkedHashSet<>(4); - - private Set declaredMethods = new LinkedHashSet<>(4); - - private MergedAnnotations mergedAnnotations = MergedAnnotations.of(Collections.emptySet()); - - public Builder(ClassLoader classLoader) { - this.clasLoader = classLoader; - } - - void classEntry(ClassEntry classEntry) { - this.className = ClassUtils.convertResourcePathToClassName(classEntry.name().stringValue()); - } - - void accessFlags(AccessFlags accessFlags) { - this.accessFlags = accessFlags; - } - - void enclosingClass(ClassEntry thisClass) { - String thisClassName = thisClass.name().stringValue(); - int currentClassIndex = thisClassName.lastIndexOf('$'); - this.enclosingClassName = ClassUtils.convertResourcePathToClassName(thisClassName.substring(0, currentClassIndex)); - } - - void superClass(Superclass superClass) { - this.superClassName = ClassUtils.convertResourcePathToClassName(superClass.superclassEntry().name().stringValue()); - } - - void interfaces(Interfaces interfaces) { - for (ClassEntry entry : interfaces.interfaces()) { - this.interfaceNames.add(ClassUtils.convertResourcePathToClassName(entry.name().stringValue())); - } - } - - void nestMembers(String currentClassName, InnerClassesAttribute innerClasses) { - for (InnerClassInfo classInfo : innerClasses.classes()) { - String innerClassName = classInfo.innerClass().name().stringValue(); - if (currentClassName.equals(innerClassName)) { - // the current class is an inner class - this.innerAccessFlags = classInfo.flags(); - } - classInfo.outerClass().ifPresent(outerClass -> { - if (outerClass.name().stringValue().equals(currentClassName)) { - // collecting data about actual inner classes - this.memberClassNames.add(ClassUtils.convertResourcePathToClassName(innerClassName)); - } - }); - } - } - - void mergedAnnotations(MergedAnnotations mergedAnnotations) { - this.mergedAnnotations = mergedAnnotations; - } - - void method(MethodModel method) { - ClassFileMethodMetadata classFileMethodMetadata = ClassFileMethodMetadata.of(method, this.clasLoader); - if (!classFileMethodMetadata.isSynthetic() && !classFileMethodMetadata.isDefaultConstructor()) { - this.declaredMethods.add(classFileMethodMetadata); - } - } - - ClassFileClassMetadata build() { - boolean independentInnerClass = (this.enclosingClassName != null) && this.innerAccessFlags.contains(AccessFlag.STATIC); - return new ClassFileClassMetadata(this.className, this.accessFlags, this.enclosingClassName, this.superClassName, - independentInnerClass, this.interfaceNames, this.memberClassNames, this.declaredMethods, this.mergedAnnotations); - } - } - -} diff --git a/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMetadataReader.java b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMetadataReader.java index c351928862b..9c2943d1a81 100644 --- a/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMetadataReader.java +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMetadataReader.java @@ -42,13 +42,12 @@ final class ClassFileMetadataReader implements MetadataReader { ClassFileMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException { this.resource = resource; - this.annotationMetadata = ClassFileClassMetadata.of(parseClassModel(resource), classLoader); + this.annotationMetadata = ClassFileAnnotationMetadata.of(parseClassModel(resource), classLoader); } private static ClassModel parseClassModel(Resource resource) throws IOException { - try (InputStream is = resource.getInputStream()) { - byte[] bytes = is.readAllBytes(); - return ClassFile.of().parse(bytes); + try (InputStream inputStream = resource.getInputStream()) { + return ClassFile.of().parse(inputStream.readAllBytes()); } } diff --git a/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMethodMetadata.java b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMethodMetadata.java index 98e7597be33..bc61f08101a 100644 --- a/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMethodMetadata.java +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMethodMetadata.java @@ -41,7 +41,7 @@ import org.springframework.util.ClassUtils; * @author Brian Clozel * @since 7.0 */ -class ClassFileMethodMetadata implements MethodMetadata { +final class ClassFileMethodMetadata implements MethodMetadata { private final String methodName; @@ -148,7 +148,7 @@ class ClassFileMethodMetadata implements MethodMetadata { MergedAnnotations annotations = methodModel.elementStream() .filter(element -> element instanceof RuntimeVisibleAnnotationsAttribute) .findFirst() - .map(element -> ClassFileAnnotationMetadata.createMergedAnnotations(methodName, (RuntimeVisibleAnnotationsAttribute) element, classLoader)) + .map(element -> ClassFileAnnotationDelegate.createMergedAnnotations(methodName, (RuntimeVisibleAnnotationsAttribute) element, classLoader)) .orElse(MergedAnnotations.of(Collections.emptyList())); return new ClassFileMethodMetadata(methodName, flags, declaringClassName, returnTypeName, source, annotations); } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java index 21fe0ec480d..5db262260b7 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java @@ -32,10 +32,6 @@ import java.util.List; import java.util.Set; import javax.annotation.RegEx; -import javax.annotation.Syntax; -import javax.annotation.concurrent.ThreadSafe; -import javax.annotation.meta.TypeQualifierNickname; -import javax.annotation.meta.When; import jakarta.annotation.Resource; import org.jspecify.annotations.Nullable; @@ -343,22 +339,6 @@ class AnnotatedElementUtilsTests { assertThat(attributes.get("value")).as("value for TxFromMultipleComposedAnnotations.").isEqualTo(asList("TxInheritedComposed", "TxComposed")); } - @Test - @SuppressWarnings("deprecation") - void getAllAnnotationAttributesOnLangType() { - MultiValueMap attributes = getAllAnnotationAttributes( - org.springframework.lang.NonNullApi.class, javax.annotation.Nonnull.class.getName()); - assertThat(attributes).as("Annotation attributes map for @Nonnull on @NonNullApi").isNotNull(); - assertThat(attributes.get("when")).as("value for @NonNullApi").isEqualTo(List.of(When.ALWAYS)); - } - - @Test - void getAllAnnotationAttributesOnJavaxType() { - MultiValueMap attributes = getAllAnnotationAttributes(RegEx.class, Syntax.class.getName()); - assertThat(attributes).as("Annotation attributes map for @Syntax on @RegEx").isNotNull(); - assertThat(attributes.get("when")).as("value for @RegEx").isEqualTo(List.of(When.ALWAYS)); - } - @Test void getMergedAnnotationAttributesOnClassWithLocalAnnotation() { Class element = TxConfig.class; @@ -848,19 +828,11 @@ class AnnotatedElementUtilsTests { } @Test - void javaxAnnotationTypeViaFindMergedAnnotation() { + void jakartaAnnotationTypeViaFindMergedAnnotation() { assertThat(findMergedAnnotation(ResourceHolder.class, Resource.class)).isEqualTo(ResourceHolder.class.getAnnotation(Resource.class)); assertThat(findMergedAnnotation(SpringAppConfigClass.class, Resource.class)).isEqualTo(SpringAppConfigClass.class.getAnnotation(Resource.class)); } - @Test - void javaxMetaAnnotationTypeViaFindMergedAnnotation() { - assertThat(findMergedAnnotation(ThreadSafe.class, Documented.class)) - .isEqualTo(ThreadSafe.class.getAnnotation(Documented.class)); - assertThat(findMergedAnnotation(ResourceHolder.class, TypeQualifierNickname.class)) - .isEqualTo(RegEx.class.getAnnotation(TypeQualifierNickname.class)); - } - @Test void nullableAnnotationTypeViaFindMergedAnnotation() throws Exception { Method method = TransactionalServiceImpl.class.getMethod("doIt"); diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationFilterTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationFilterTests.java index 6bf99d15784..beb5da689e7 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationFilterTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationFilterTests.java @@ -19,8 +19,6 @@ package org.springframework.core.annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import javax.annotation.concurrent.ThreadSafe; - import org.junit.jupiter.api.Test; import org.springframework.lang.Contract; @@ -83,11 +81,6 @@ class AnnotationFilterTests { assertThat(AnnotationFilter.JAVA.matches(Retention.class)).isTrue(); } - @Test - void javaWhenJavaxAnnotationReturnsTrue() { - assertThat(AnnotationFilter.JAVA.matches(ThreadSafe.class)).isTrue(); - } - @Test @SuppressWarnings("deprecation") void javaWhenSpringLangAnnotationReturnsFalse() { diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java index cce22111b1d..d66ee5fba82 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java @@ -30,10 +30,6 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; -import javax.annotation.RegEx; -import javax.annotation.Syntax; -import javax.annotation.concurrent.ThreadSafe; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -430,8 +426,6 @@ class AnnotationUtilsTests { @Test void isAnnotationMetaPresentForPlainType() { assertThat(isAnnotationMetaPresent(Order.class, Documented.class)).isTrue(); - assertThat(isAnnotationMetaPresent(ThreadSafe.class, Documented.class)).isTrue(); - assertThat(isAnnotationMetaPresent(RegEx.class, Syntax.class)).isTrue(); } @Test diff --git a/spring-core/src/test/java/org/springframework/core/type/classreading/DefaultAnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/classreading/DefaultAnnotationMetadataTests.java index a296ecf41e8..b91f23a45b4 100644 --- a/spring-core/src/test/java/org/springframework/core/type/classreading/DefaultAnnotationMetadataTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/classreading/DefaultAnnotationMetadataTests.java @@ -20,7 +20,6 @@ import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import com.github.benmanes.caffeine.cache.Caffeine; import org.junit.jupiter.api.Test; import org.springframework.core.type.AbstractAnnotationMetadataTests; @@ -53,16 +52,20 @@ class DefaultAnnotationMetadataTests extends AbstractAnnotationMetadataTests { @Test void getClassAttributeWhenUnknownClass() { var annotation = get(WithClassMissingFromClasspath.class).getAnnotations().get(ClassAttributes.class); - assertThat(annotation.getStringArray("types")).contains("com.github.benmanes.caffeine.cache.Caffeine"); + assertThat(annotation.getStringArray("types")).contains("javax.annotation.meta.When"); assertThatIllegalArgumentException().isThrownBy(() -> annotation.getClassArray("types")); } - @ClassAttributes(types = {Caffeine.class}) + + @ClassAttributes(types = {javax.annotation.meta.When.class}) + @javax.annotation.Nonnull(when = javax.annotation.meta.When.MAYBE) public static class WithClassMissingFromClasspath { } + @Retention(RetentionPolicy.RUNTIME) public @interface ClassAttributes { + Class[] types(); } diff --git a/spring-core/src/test/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataTests.java index 562754328d5..ad6a8b1e2cc 100644 --- a/spring-core/src/test/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataTests.java @@ -20,7 +20,6 @@ import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import com.github.benmanes.caffeine.cache.Caffeine; import org.junit.jupiter.api.Test; import org.springframework.core.type.AbstractAnnotationMetadataTests; @@ -53,17 +52,20 @@ class SimpleAnnotationMetadataTests extends AbstractAnnotationMetadataTests { @Test void getClassAttributeWhenUnknownClass() { var annotation = get(WithClassMissingFromClasspath.class).getAnnotations().get(ClassAttributes.class); - assertThat(annotation.getStringArray("types")).contains("com.github.benmanes.caffeine.cache.Caffeine"); + assertThat(annotation.getStringArray("types")).contains("javax.annotation.meta.When"); assertThatIllegalArgumentException().isThrownBy(() -> annotation.getClassArray("types")); } - @ClassAttributes(types = {Caffeine.class}) + + @ClassAttributes(types = {javax.annotation.meta.When.class}) + @javax.annotation.Nonnull(when = javax.annotation.meta.When.MAYBE) public static class WithClassMissingFromClasspath { } @Retention(RetentionPolicy.RUNTIME) public @interface ClassAttributes { + Class[] types(); }