diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java index 437e6fe58b..65559ead92 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java @@ -84,7 +84,6 @@ import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.MethodMetadata; import org.springframework.core.type.classreading.MetadataReaderFactory; -import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; import org.springframework.javapoet.ClassName; import org.springframework.javapoet.CodeBlock; import org.springframework.util.Assert; @@ -271,7 +270,7 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA "AutowiredAnnotationBeanPostProcessor requires a ConfigurableListableBeanFactory: " + beanFactory); } this.beanFactory = clbf; - this.metadataReaderFactory = new SimpleMetadataReaderFactory(clbf.getBeanClassLoader()); + this.metadataReaderFactory = MetadataReaderFactory.create(clbf.getBeanClassLoader()); } diff --git a/spring-core/spring-core.gradle b/spring-core/spring-core.gradle index e138da582f..5ac17dd889 100644 --- a/spring-core/spring-core.gradle +++ b/spring-core/spring-core.gradle @@ -11,7 +11,7 @@ apply plugin: "kotlin" apply plugin: "kotlinx-serialization" multiRelease { - releaseVersions 21 + releaseVersions 21, 24 } def javapoetVersion = "1.13.0" @@ -20,6 +20,7 @@ def objenesisVersion = "3.4" configurations { java21Api.extendsFrom(api) java21Implementation.extendsFrom(implementation) + java24Api.extendsFrom(api) javapoet objenesis graalvm diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/CachingMetadataReaderFactory.java b/spring-core/src/main/java/org/springframework/core/type/classreading/CachingMetadataReaderFactory.java index 6c8241a5d6..d46a3ff7e8 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/CachingMetadataReaderFactory.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/CachingMetadataReaderFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 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. @@ -36,11 +36,13 @@ import org.springframework.core.io.ResourceLoader; * @author Costin Leau * @since 2.5 */ -public class CachingMetadataReaderFactory extends SimpleMetadataReaderFactory { +public class CachingMetadataReaderFactory implements MetadataReaderFactory { /** Default maximum number of entries for a local MetadataReader cache: 256. */ public static final int DEFAULT_CACHE_LIMIT = 256; + private final MetadataReaderFactory delegate; + /** MetadataReader cache: either local or shared at the ResourceLoader level. */ private @Nullable Map metadataReaderCache; @@ -50,7 +52,7 @@ public class CachingMetadataReaderFactory extends SimpleMetadataReaderFactory { * using a local resource cache. */ public CachingMetadataReaderFactory() { - super(); + this.delegate = MetadataReaderFactory.create((ClassLoader) null); setCacheLimit(DEFAULT_CACHE_LIMIT); } @@ -60,7 +62,7 @@ public class CachingMetadataReaderFactory extends SimpleMetadataReaderFactory { * @param classLoader the ClassLoader to use */ public CachingMetadataReaderFactory(@Nullable ClassLoader classLoader) { - super(classLoader); + this.delegate = MetadataReaderFactory.create(classLoader); setCacheLimit(DEFAULT_CACHE_LIMIT); } @@ -72,7 +74,7 @@ public class CachingMetadataReaderFactory extends SimpleMetadataReaderFactory { * @see DefaultResourceLoader#getResourceCache */ public CachingMetadataReaderFactory(@Nullable ResourceLoader resourceLoader) { - super(resourceLoader); + this.delegate = MetadataReaderFactory.create(resourceLoader); if (resourceLoader instanceof DefaultResourceLoader defaultResourceLoader) { this.metadataReaderCache = defaultResourceLoader.getResourceCache(MetadataReader.class); } @@ -81,7 +83,6 @@ public class CachingMetadataReaderFactory extends SimpleMetadataReaderFactory { } } - /** * Specify the maximum number of entries for the MetadataReader cache. *

Default is 256 for a local cache, whereas a shared cache is @@ -112,6 +113,10 @@ public class CachingMetadataReaderFactory extends SimpleMetadataReaderFactory { } } + @Override + public MetadataReader getMetadataReader(String className) throws IOException { + return this.delegate.getMetadataReader(className); + } @Override public MetadataReader getMetadataReader(Resource resource) throws IOException { @@ -119,7 +124,7 @@ public class CachingMetadataReaderFactory extends SimpleMetadataReaderFactory { // No synchronization necessary... MetadataReader metadataReader = this.metadataReaderCache.get(resource); if (metadataReader == null) { - metadataReader = super.getMetadataReader(resource); + metadataReader = this.delegate.getMetadataReader(resource); this.metadataReaderCache.put(resource, metadataReader); } return metadataReader; @@ -128,14 +133,14 @@ public class CachingMetadataReaderFactory extends SimpleMetadataReaderFactory { synchronized (this.metadataReaderCache) { MetadataReader metadataReader = this.metadataReaderCache.get(resource); if (metadataReader == null) { - metadataReader = super.getMetadataReader(resource); + metadataReader = this.delegate.getMetadataReader(resource); this.metadataReaderCache.put(resource, metadataReader); } return metadataReader; } } else { - return super.getMetadataReader(resource); + return this.delegate.getMetadataReader(resource); } } diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactory.java b/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactory.java index 4eddbfa6cd..ecae86b611 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactory.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 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. @@ -18,13 +18,17 @@ package org.springframework.core.type.classreading; import java.io.IOException; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; /** * Factory interface for {@link MetadataReader} instances. * Allows for caching a MetadataReader per original resource. * * @author Juergen Hoeller + * @author Brian Clozel * @since 2.5 * @see SimpleMetadataReaderFactory * @see CachingMetadataReaderFactory @@ -49,4 +53,23 @@ public interface MetadataReaderFactory { */ MetadataReader getMetadataReader(Resource resource) throws IOException; + /** + * Create a default {@link MetadataReaderFactory} implementation that's suitable + * for the current JVM. + * @return a new factory instance + * @since 7.0 + */ + static MetadataReaderFactory create(@Nullable ResourceLoader resourceLoader) { + return MetadataReaderFactoryDelegate.create(resourceLoader); + } + + /** + * Create a default {@link MetadataReaderFactory} implementation that's suitable + * for the current JVM. + * @return a new factory instance + * @since 7.0 + */ + static MetadataReaderFactory create(@Nullable ClassLoader classLoader) { + return MetadataReaderFactoryDelegate.create(classLoader); + } } diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactoryDelegate.java b/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactoryDelegate.java new file mode 100644 index 0000000000..dfa317e94b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactoryDelegate.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2025 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 org.jspecify.annotations.Nullable; + +import org.springframework.core.io.ResourceLoader; + +/** + * Internal delegate for instantiating {@link MetadataReaderFactory} implementations. + * For JDK < 24, the {@link SimpleMetadataReaderFactory} is being used. + * + * @author Brian Clozel + * @since 7.0 + * @see MetadataReaderFactory + */ +abstract class MetadataReaderFactoryDelegate { + + static MetadataReaderFactory create(@Nullable ResourceLoader resourceLoader) { + return new SimpleMetadataReaderFactory(resourceLoader); + } + + static MetadataReaderFactory create(@Nullable ClassLoader classLoader) { + return new SimpleMetadataReaderFactory(classLoader); + } +} 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 new file mode 100644 index 0000000000..abc6040873 --- /dev/null +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileAnnotationMetadata.java @@ -0,0 +1,144 @@ +/* + * Copyright 2002-2025 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.reflect.Array; +import java.util.Collections; +import java.util.LinkedHashMap; +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 + */ +abstract class ClassFileAnnotationMetadata { + + 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; + } + 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); + } + } + 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; + } + } + + 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) { + return descriptor.substring(1, descriptor.length() - 1) + .replace('/', '.'); + } + + private static Object parseArrayValue(String className, @org.jetbrains.annotations.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 -> { + Object firstResolvedValue = readAnnotationValue(className, arrayValue.values().getFirst(), classLoader); + return stream + .map(rawValue -> readAnnotationValue(className, rawValue, classLoader)) + .toArray(s -> (Object[]) Array.newInstance(firstResolvedValue.getClass(), s)); + } + } + } + + @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()); + } + catch (ClassNotFoundException | LinkageError ex) { + return null; + } + } + + record Source(Annotation entryName) { + + } + +} 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 new file mode 100644 index 0000000000..17d274a82f --- /dev/null +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileClassMetadata.java @@ -0,0 +1,305 @@ +/* + * Copyright 2002-2025 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 + */ +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 = superClassName; + 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(); + // skip parent inner classes + if (!innerClassName.startsWith(currentClassName)) { + continue; + } + // the current class is an inner class + else if (currentClassName.equals(innerClassName)) { + this.innerAccessFlags = classInfo.flags(); + } + // collecting data about actual inner classes + else { + 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 new file mode 100644 index 0000000000..aadc3b0120 --- /dev/null +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMetadataReader.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2025 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.io.IOException; +import java.io.InputStream; +import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassModel; + +import org.jspecify.annotations.Nullable; + +import org.springframework.core.io.Resource; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.ClassMetadata; + +/** + * {@link MetadataReader} implementation based on the {@link ClassFile} API. + * + * @author Brian Clozel + */ +final class ClassFileMetadataReader implements MetadataReader { + + private final Resource resource; + + private final AnnotationMetadata annotationMetadata; + + + ClassFileMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException { + this.resource = resource; + this.annotationMetadata = ClassFileClassMetadata.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); + } + } + + @Override + public Resource getResource() { + return this.resource; + } + + @Override + public ClassMetadata getClassMetadata() { + return this.annotationMetadata; + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + return this.annotationMetadata; + } + +} diff --git a/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMetadataReaderFactory.java b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMetadataReaderFactory.java new file mode 100644 index 0000000000..c267f3b0cf --- /dev/null +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMetadataReaderFactory.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2025 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.io.FileNotFoundException; +import java.io.IOException; + +import org.jspecify.annotations.Nullable; + +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.ClassUtils; + +/** + * Implementation of the {@link MetadataReaderFactory} interface, + * using the {@link java.lang.classfile.ClassFile} API for parsing the bytecode. + * + * @author Brian Clozel + * @since 7.0 + */ +public class ClassFileMetadataReaderFactory implements MetadataReaderFactory { + + + private final ResourceLoader resourceLoader; + + + /** + * Create a new ClassFileMetadataReaderFactory for the default class loader. + */ + public ClassFileMetadataReaderFactory() { + this.resourceLoader = new DefaultResourceLoader(); + } + + /** + * Create a new ClassFileMetadataReaderFactory for the given resource loader. + * @param resourceLoader the Spring ResourceLoader to use + * (also determines the ClassLoader to use) + */ + public ClassFileMetadataReaderFactory(@Nullable ResourceLoader resourceLoader) { + this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader()); + } + + /** + * Create a new ClassFileMetadataReaderFactory for the given class loader. + * @param classLoader the ClassLoader to use + */ + public ClassFileMetadataReaderFactory(@Nullable ClassLoader classLoader) { + this.resourceLoader = + (classLoader != null ? new DefaultResourceLoader(classLoader) : new DefaultResourceLoader()); + } + + /** + * Return the ResourceLoader that this MetadataReaderFactory has been + * constructed with. + */ + public final ResourceLoader getResourceLoader() { + return this.resourceLoader; + } + + @Override + public MetadataReader getMetadataReader(String className) throws IOException { + try { + String resourcePath = ResourceLoader.CLASSPATH_URL_PREFIX + + ClassUtils.convertClassNameToResourcePath(className) + ClassUtils.CLASS_FILE_SUFFIX; + Resource resource = this.resourceLoader.getResource(resourcePath); + return getMetadataReader(resource); + } + catch (FileNotFoundException ex) { + // Maybe an inner class name using the dot name syntax? Need to use the dollar syntax here... + // ClassUtils.forName has an equivalent check for resolution into Class references later on. + int lastDotIndex = className.lastIndexOf('.'); + if (lastDotIndex != -1) { + String innerClassName = + className.substring(0, lastDotIndex) + '$' + className.substring(lastDotIndex + 1); + String innerClassResourcePath = ResourceLoader.CLASSPATH_URL_PREFIX + + ClassUtils.convertClassNameToResourcePath(innerClassName) + ClassUtils.CLASS_FILE_SUFFIX; + Resource innerClassResource = this.resourceLoader.getResource(innerClassResourcePath); + if (innerClassResource.exists()) { + return getMetadataReader(innerClassResource); + } + } + throw ex; + } + } + + @Override + public MetadataReader getMetadataReader(Resource resource) throws IOException { + return new ClassFileMetadataReader(resource, this.resourceLoader.getClassLoader()); + } +} 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 new file mode 100644 index 0000000000..579f9bda91 --- /dev/null +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMethodMetadata.java @@ -0,0 +1,182 @@ +/* + * Copyright 2002-2025 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.MethodModel; +import java.lang.classfile.attribute.RuntimeVisibleAnnotationsAttribute; +import java.lang.constant.ClassDesc; +import java.lang.constant.MethodTypeDesc; +import java.lang.reflect.AccessFlag; +import java.util.Collections; +import java.util.Locale; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.jspecify.annotations.Nullable; + +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.type.MethodMetadata; +import org.springframework.util.ClassUtils; + +/** + * {@link MethodMetadata} extracted from class bytecode using the + * {@link java.lang.classfile.ClassFile} API. + * @author Brian Clozel + */ +class ClassFileMethodMetadata implements MethodMetadata { + + private final String methodName; + + private final AccessFlags accessFlags; + + private final @Nullable String declaringClassName; + + private final String returnTypeName; + + // The source implements equals(), hashCode(), and toString() for the underlying method. + private final Object source; + + private final MergedAnnotations annotations; + + ClassFileMethodMetadata(String methodName, AccessFlags accessFlags, @Nullable String declaringClassName, String returnTypeName, Object source, MergedAnnotations annotations) { + this.methodName = methodName; + this.accessFlags = accessFlags; + this.declaringClassName = declaringClassName; + this.returnTypeName = returnTypeName; + this.source = source; + this.annotations = annotations; + } + + @Override + public String getMethodName() { + return this.methodName; + } + + @Override + public @Nullable String getDeclaringClassName() { + return this.declaringClassName; + } + + @Override + public String getReturnTypeName() { + return this.returnTypeName; + } + + @Override + public boolean isAbstract() { + return this.accessFlags.has(AccessFlag.ABSTRACT); + } + + @Override + public boolean isStatic() { + return this.accessFlags.has(AccessFlag.STATIC); + } + + @Override + public boolean isFinal() { + return this.accessFlags.has(AccessFlag.FINAL); + } + + @Override + public boolean isOverridable() { + return !isStatic() && !isFinal() && !isPrivate(); + } + + private boolean isPrivate() { + return this.accessFlags.has(AccessFlag.PRIVATE); + } + + public boolean isSynthetic() { + return this.accessFlags.has(AccessFlag.SYNTHETIC); + } + + public boolean isDefaultConstructor() { + return this.methodName.equals(""); + } + + @Override + public MergedAnnotations getAnnotations() { + return this.annotations; + } + + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof ClassFileMethodMetadata that && this.source.equals(that.source))); + } + + @Override + public int hashCode() { + return this.source.hashCode(); + } + + @Override + public String toString() { + return this.source.toString(); + } + + static ClassFileMethodMetadata of(MethodModel methodModel, ClassLoader classLoader) { + String methodName = methodModel.methodName().stringValue(); + AccessFlags flags = methodModel.flags(); + String declaringClassName = methodModel.parent().map(parent -> ClassUtils.convertResourcePathToClassName(parent.thisClass().name().stringValue())).orElse(null); + ClassDesc returnType = methodModel.methodTypeSymbol().returnType(); + String returnTypeName = returnType.packageName() + "." + returnType.displayName(); + Source source = new Source(declaringClassName, flags, methodName, methodModel.methodTypeSymbol()); + MergedAnnotations annotations = methodModel.elementStream() + .filter(element -> element instanceof RuntimeVisibleAnnotationsAttribute) + .findFirst() + .map(element -> ClassFileAnnotationMetadata.createMergedAnnotations(methodName, (RuntimeVisibleAnnotationsAttribute) element, classLoader)) + .orElse(MergedAnnotations.of(Collections.emptyList())); + return new ClassFileMethodMetadata(methodName, flags, declaringClassName, returnTypeName, source, annotations); + } + + /** + * {@link MergedAnnotation} source. + * + * @param declaringClassName the name of the declaring class + * @param flags the access flags + * @param methodName the name of the method + * @param descriptor the bytecode descriptor for this method + */ + record Source(@Nullable String declaringClassName, AccessFlags flags, String methodName, MethodTypeDesc descriptor) { + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + this.flags.flags().forEach(flag -> { + builder.append(flag.name().toLowerCase(Locale.ROOT)); + builder.append(' '); + }); + builder.append(this.descriptor.returnType().packageName()); + builder.append("."); + builder.append(this.descriptor.returnType().displayName()); + builder.append(' '); + builder.append(this.declaringClassName); + builder.append('.'); + builder.append(this.methodName); + builder.append('('); + builder.append(Stream.of(this.descriptor.parameterArray()) + .map(desc -> desc.packageName() + "." + desc.displayName()) + .collect(Collectors.joining(","))); + builder.append(')'); + return builder.toString(); + } + } + +} diff --git a/spring-core/src/main/java24/org/springframework/core/type/classreading/MetadataReaderFactoryDelegate.java b/spring-core/src/main/java24/org/springframework/core/type/classreading/MetadataReaderFactoryDelegate.java new file mode 100644 index 0000000000..8887ada063 --- /dev/null +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/MetadataReaderFactoryDelegate.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2025 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 org.jspecify.annotations.Nullable; + +import org.springframework.core.io.ResourceLoader; + +/** + * Internal delegate for instantiating {@link MetadataReaderFactory} implementations. + * For JDK >= 24, the {@link ClassFileMetadataReaderFactory} is being used. + * + * @author Brian Clozel + * @see MetadataReaderFactory + */ +abstract class MetadataReaderFactoryDelegate { + + static MetadataReaderFactory create(@Nullable ResourceLoader resourceLoader) { + return new ClassFileMetadataReaderFactory(resourceLoader); + } + + static MetadataReaderFactory create(@Nullable ClassLoader classLoader) { + return new ClassFileMetadataReaderFactory(classLoader); + } +} diff --git a/spring-core/src/test/java/org/springframework/core/type/AbstractAnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/AbstractAnnotationMetadataTests.java index cc966f1799..c1e2d7a78e 100644 --- a/spring-core/src/test/java/org/springframework/core/type/AbstractAnnotationMetadataTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/AbstractAnnotationMetadataTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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,14 +16,17 @@ package org.springframework.core.type; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.List; +import kotlin.Metadata; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.core.annotation.MergedAnnotation; -import org.springframework.core.type.AbstractAnnotationMetadataTests.TestMemberClass.TestMemberClassInnerClass; -import org.springframework.core.type.AbstractAnnotationMetadataTests.TestMemberClass.TestMemberClassInnerInterface; import org.springframework.util.MultiValueMap; import static org.assertj.core.api.Assertions.assertThat; @@ -34,376 +37,493 @@ import static org.assertj.core.api.Assertions.entry; * * @author Phillip Webb * @author Sam Brannen + * @author Brian Clozel */ public abstract class AbstractAnnotationMetadataTests { - @Test - void verifyEquals() { - AnnotationMetadata testClass1 = get(TestClass.class); - AnnotationMetadata testClass2 = get(TestClass.class); - AnnotationMetadata testMemberClass1 = get(TestMemberClass.class); - AnnotationMetadata testMemberClass2 = get(TestMemberClass.class); - - assertThat(testClass1).isNotEqualTo(null); - - assertThat(testClass1).isEqualTo(testClass1); - assertThat(testClass2).isEqualTo(testClass2); - assertThat(testClass1).isEqualTo(testClass2); - assertThat(testClass2).isEqualTo(testClass1); - - assertThat(testMemberClass1).isEqualTo(testMemberClass1); - assertThat(testMemberClass2).isEqualTo(testMemberClass2); - assertThat(testMemberClass1).isEqualTo(testMemberClass2); - assertThat(testMemberClass2).isEqualTo(testMemberClass1); - - assertThat(testClass1).isNotEqualTo(testMemberClass1); - assertThat(testMemberClass1).isNotEqualTo(testClass1); - } - - @Test - void verifyHashCode() { - AnnotationMetadata testClass1 = get(TestClass.class); - AnnotationMetadata testClass2 = get(TestClass.class); - AnnotationMetadata testMemberClass1 = get(TestMemberClass.class); - AnnotationMetadata testMemberClass2 = get(TestMemberClass.class); - - assertThat(testClass1).hasSameHashCodeAs(testClass2); - assertThat(testMemberClass1).hasSameHashCodeAs(testMemberClass2); - - assertThat(testClass1).doesNotHaveSameHashCodeAs(testMemberClass1); - } - - @Test - void verifyToString() { - assertThat(get(TestClass.class).toString()).isEqualTo(TestClass.class.getName()); - } - - @Test - void getClassNameReturnsClassName() { - assertThat(get(TestClass.class).getClassName()).isEqualTo(TestClass.class.getName()); - } - - @Test - void isInterfaceWhenInterfaceReturnsTrue() { - assertThat(get(TestInterface.class).isInterface()).isTrue(); - assertThat(get(TestAnnotation.class).isInterface()).isTrue(); - } - - @Test - void isInterfaceWhenNotInterfaceReturnsFalse() { - assertThat(get(TestClass.class).isInterface()).isFalse(); - } - - @Test - void isAnnotationWhenAnnotationReturnsTrue() { - assertThat(get(TestAnnotation.class).isAnnotation()).isTrue(); - } - - @Test - void isAnnotationWhenNotAnnotationReturnsFalse() { - assertThat(get(TestClass.class).isAnnotation()).isFalse(); - assertThat(get(TestInterface.class).isAnnotation()).isFalse(); - } - - @Test - void isFinalWhenFinalReturnsTrue() { - assertThat(get(TestFinalClass.class).isFinal()).isTrue(); - } - - @Test - void isFinalWhenNonFinalReturnsFalse() { - assertThat(get(TestClass.class).isFinal()).isFalse(); - } - - @Test - void isIndependentWhenIndependentReturnsTrue() { - assertThat(get(AbstractAnnotationMetadataTests.class).isIndependent()).isTrue(); - assertThat(get(TestClass.class).isIndependent()).isTrue(); - } - - @Test - void isIndependentWhenNotIndependentReturnsFalse() { - assertThat(get(TestNonStaticInnerClass.class).isIndependent()).isFalse(); - } - - @Test - void getEnclosingClassNameWhenHasEnclosingClassReturnsEnclosingClass() { - assertThat(get(TestClass.class).getEnclosingClassName()).isEqualTo( - AbstractAnnotationMetadataTests.class.getName()); - } - - @Test - void getEnclosingClassNameWhenHasNoEnclosingClassReturnsNull() { - assertThat(get(AbstractAnnotationMetadataTests.class).getEnclosingClassName()).isNull(); - } - - @Test - void getSuperClassNameWhenHasSuperClassReturnsName() { - assertThat(get(TestSubclass.class).getSuperClassName()).isEqualTo(TestClass.class.getName()); - assertThat(get(TestClass.class).getSuperClassName()).isEqualTo(Object.class.getName()); - } - - @Test - void getSuperClassNameWhenHasNoSuperClassReturnsNull() { - assertThat(get(Object.class).getSuperClassName()).isNull(); - assertThat(get(TestInterface.class).getSuperClassName()).isNull(); - assertThat(get(TestSubInterface.class).getSuperClassName()).isNull(); - } - - @Test - void getInterfaceNamesWhenHasInterfacesReturnsNames() { - assertThat(get(TestSubclass.class).getInterfaceNames()).containsExactlyInAnyOrder(TestInterface.class.getName()); - assertThat(get(TestSubInterface.class).getInterfaceNames()).containsExactlyInAnyOrder(TestInterface.class.getName()); - } - - @Test - void getInterfaceNamesWhenHasNoInterfacesReturnsEmptyArray() { - assertThat(get(TestClass.class).getInterfaceNames()).isEmpty(); - } - - @Test - void getMemberClassNamesWhenHasMemberClassesReturnsNames() { - assertThat(get(TestMemberClass.class).getMemberClassNames()).containsExactlyInAnyOrder( - TestMemberClassInnerClass.class.getName(), TestMemberClassInnerInterface.class.getName()); - } - - @Test - void getMemberClassNamesWhenHasNoMemberClassesReturnsEmptyArray() { - assertThat(get(TestClass.class).getMemberClassNames()).isEmpty(); - } - - @Test - void getAnnotationsReturnsDirectAnnotations() { - assertThat(get(WithDirectAnnotations.class).getAnnotations().stream()) - .filteredOn(MergedAnnotation::isDirectlyPresent) - .extracting(a -> a.getType().getName()) - .containsExactlyInAnyOrder(DirectAnnotation1.class.getName(), DirectAnnotation2.class.getName()); - } - - @Test - void isAnnotatedWhenMatchesDirectAnnotationReturnsTrue() { - assertThat(get(WithDirectAnnotations.class).isAnnotated(DirectAnnotation1.class.getName())).isTrue(); - } - - @Test - void isAnnotatedWhenMatchesMetaAnnotationReturnsTrue() { - assertThat(get(WithMetaAnnotations.class).isAnnotated(MetaAnnotation2.class.getName())).isTrue(); - } - - @Test - void isAnnotatedWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() { - assertThat(get(TestClass.class).isAnnotated(DirectAnnotation1.class.getName())).isFalse(); - } - - @Test - void getAnnotationAttributesReturnsAttributes() { - assertThat(get(WithAnnotationAttributes.class).getAnnotationAttributes(AnnotationAttributes.class.getName())) - .containsOnly(entry("name", "test"), entry("size", 1)); - } - - @Test - void getAllAnnotationAttributesReturnsAllAttributes() { - MultiValueMap attributes = - get(WithMetaAnnotationAttributes.class).getAllAnnotationAttributes(AnnotationAttributes.class.getName()); - assertThat(attributes).containsOnlyKeys("name", "size"); - assertThat(attributes.get("name")).containsExactlyInAnyOrder("m1", "m2"); - assertThat(attributes.get("size")).containsExactlyInAnyOrder(1, 2); - } - - @Test - void getAnnotationTypesReturnsDirectAnnotations() { - AnnotationMetadata metadata = get(WithDirectAnnotations.class); - assertThat(metadata.getAnnotationTypes()).containsExactlyInAnyOrder( - DirectAnnotation1.class.getName(), DirectAnnotation2.class.getName()); - } - - @Test - void getMetaAnnotationTypesReturnsMetaAnnotations() { - AnnotationMetadata metadata = get(WithMetaAnnotations.class); - assertThat(metadata.getMetaAnnotationTypes(MetaAnnotationRoot.class.getName())) - .containsExactlyInAnyOrder(MetaAnnotation1.class.getName(), MetaAnnotation2.class.getName()); - } - - @Test - void hasAnnotationWhenMatchesDirectAnnotationReturnsTrue() { - assertThat(get(WithDirectAnnotations.class).hasAnnotation(DirectAnnotation1.class.getName())).isTrue(); - } - - @Test - void hasAnnotationWhenMatchesMetaAnnotationReturnsFalse() { - assertThat(get(WithMetaAnnotations.class).hasAnnotation(MetaAnnotation1.class.getName())).isFalse(); - assertThat(get(WithMetaAnnotations.class).hasAnnotation(MetaAnnotation2.class.getName())).isFalse(); - } - - @Test - void hasAnnotationWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() { - assertThat(get(TestClass.class).hasAnnotation(DirectAnnotation1.class.getName())).isFalse(); - } - - @Test - void hasMetaAnnotationWhenMatchesDirectReturnsFalse() { - assertThat(get(WithDirectAnnotations.class).hasMetaAnnotation(DirectAnnotation1.class.getName())).isFalse(); - } - - @Test - void hasMetaAnnotationWhenMatchesMetaAnnotationReturnsTrue() { - assertThat(get(WithMetaAnnotations.class).hasMetaAnnotation(MetaAnnotation1.class.getName())).isTrue(); - assertThat(get(WithMetaAnnotations.class).hasMetaAnnotation(MetaAnnotation2.class.getName())).isTrue(); - } - - @Test - void hasMetaAnnotationWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() { - assertThat(get(TestClass.class).hasMetaAnnotation(MetaAnnotation1.class.getName())).isFalse(); - } - - @Test - void hasAnnotatedMethodsWhenMatchesDirectAnnotationReturnsTrue() { - assertThat(get(WithAnnotatedMethod.class).hasAnnotatedMethods(DirectAnnotation1.class.getName())).isTrue(); - } - - @Test - void hasAnnotatedMethodsWhenMatchesMetaAnnotationReturnsTrue() { - assertThat(get(WithMetaAnnotatedMethod.class).hasAnnotatedMethods(MetaAnnotation2.class.getName())).isTrue(); - } - - @Test - void hasAnnotatedMethodsWhenDoesNotMatchAnyAnnotationReturnsFalse() { - assertThat(get(WithAnnotatedMethod.class).hasAnnotatedMethods(MetaAnnotation2.class.getName())).isFalse(); - assertThat(get(WithNonAnnotatedMethod.class).hasAnnotatedMethods(DirectAnnotation1.class.getName())).isFalse(); - } - - @Test - void getAnnotatedMethodsReturnsMatchingAnnotatedAndMetaAnnotatedMethods() { - assertThat(get(WithDirectAndMetaAnnotatedMethods.class).getAnnotatedMethods(MetaAnnotation2.class.getName())) - .extracting(MethodMetadata::getMethodName) - .containsExactlyInAnyOrder("direct", "meta"); - } - protected abstract AnnotationMetadata get(Class source); + @Nested + class TypeTests { - @Retention(RetentionPolicy.RUNTIME) - public @interface DirectAnnotation1 { - } + @Test + void classEquals() { + AnnotationMetadata testClass1 = get(TestClass.class); + AnnotationMetadata testClass2 = get(TestClass.class); - @Retention(RetentionPolicy.RUNTIME) - public @interface DirectAnnotation2 { - } - - @Retention(RetentionPolicy.RUNTIME) - @MetaAnnotation1 - public @interface MetaAnnotationRoot { - } - - @Retention(RetentionPolicy.RUNTIME) - @MetaAnnotation2 - public @interface MetaAnnotation1 { - } - - @Retention(RetentionPolicy.RUNTIME) - public @interface MetaAnnotation2 { - } - - public static class TestClass { - } - - public interface TestInterface { - } - - public interface TestSubInterface extends TestInterface { - } - - public @interface TestAnnotation { - } - - public static final class TestFinalClass { - } - - public class TestNonStaticInnerClass { - } - - public static class TestSubclass extends TestClass implements TestInterface { - } - - @DirectAnnotation1 - @DirectAnnotation2 - public static class WithDirectAnnotations { - } - - @MetaAnnotationRoot - public static class WithMetaAnnotations { - } - - public static class TestMemberClass { - - public static class TestMemberClassInnerClass { + assertThat(testClass1).isEqualTo(testClass1); + assertThat(testClass2).isEqualTo(testClass2); + assertThat(testClass1).isEqualTo(testClass2); + assertThat(testClass2).isEqualTo(testClass1); } - interface TestMemberClassInnerInterface { + @Test + void memberClassEquals() { + AnnotationMetadata testMemberClass1 = get(TestMemberClass.class); + AnnotationMetadata testMemberClass2 = get(TestMemberClass.class); + + assertThat(testMemberClass1).isEqualTo(testMemberClass1); + assertThat(testMemberClass2).isEqualTo(testMemberClass2); + assertThat(testMemberClass1).isEqualTo(testMemberClass2); + assertThat(testMemberClass2).isEqualTo(testMemberClass1); + } + + @Test + void classHashCode() { + AnnotationMetadata testClass1 = get(TestClass.class); + AnnotationMetadata testClass2 = get(TestClass.class); + + assertThat(testClass1).hasSameHashCodeAs(testClass2); + } + + @Test + void memberClassHashCode() { + AnnotationMetadata testMemberClass1 = get(TestMemberClass.class); + AnnotationMetadata testMemberClass2 = get(TestMemberClass.class); + + assertThat(testMemberClass1).hasSameHashCodeAs(testMemberClass2); + } + + @Test + void classToString() { + assertThat(get(TestClass.class).toString()).isEqualTo(TestClass.class.getName()); + } + + @Test + void getClassNameReturnsClassName() { + assertThat(get(TestClass.class).getClassName()).isEqualTo(TestClass.class.getName()); + } + + @Test + void isInterfaceWhenInterfaceReturnsTrue() { + assertThat(get(TestInterface.class).isInterface()).isTrue(); + assertThat(get(TestAnnotation.class).isInterface()).isTrue(); + } + + @Test + void isInterfaceWhenNotInterfaceReturnsFalse() { + assertThat(get(TestClass.class).isInterface()).isFalse(); + } + + @Test + void isAnnotationWhenAnnotationReturnsTrue() { + assertThat(get(TestAnnotation.class).isAnnotation()).isTrue(); + } + + @Test + void isAnnotationWhenNotAnnotationReturnsFalse() { + assertThat(get(TestClass.class).isAnnotation()).isFalse(); + assertThat(get(TestInterface.class).isAnnotation()).isFalse(); + } + + @Test + void isFinalWhenFinalReturnsTrue() { + assertThat(get(TestFinalClass.class).isFinal()).isTrue(); + } + + @Test + void isFinalWhenNonFinalReturnsFalse() { + assertThat(get(TestClass.class).isFinal()).isFalse(); + } + + @Test + void isIndependentWhenIndependentReturnsTrue() { + assertThat(get(AbstractAnnotationMetadataTests.class).isIndependent()).isTrue(); + assertThat(get(TestClass.class).isIndependent()).isTrue(); + } + + @Test + void isIndependentWhenNotIndependentReturnsFalse() { + assertThat(get(TestNonStaticInnerClass.class).isIndependent()).isFalse(); + } + + @Test + void getEnclosingClassNameWhenHasEnclosingClassReturnsEnclosingClass() { + assertThat(get(TestClass.class).getEnclosingClassName()).isEqualTo( + AbstractAnnotationMetadataTests.TypeTests.class.getName()); + } + + @Test + void getEnclosingClassNameWhenHasNoEnclosingClassReturnsNull() { + assertThat(get(AbstractAnnotationMetadataTests.class).getEnclosingClassName()).isNull(); + } + + @Test + void getSuperClassNameWhenHasSuperClassReturnsName() { + assertThat(get(TestSubclass.class).getSuperClassName()).isEqualTo(TestClass.class.getName()); + assertThat(get(TestClass.class).getSuperClassName()).isEqualTo(Object.class.getName()); + } + + @Test + void getSuperClassNameWhenHasNoSuperClassReturnsNull() { + assertThat(get(Object.class).getSuperClassName()).isNull(); + assertThat(get(TestInterface.class).getSuperClassName()).isIn(null, "java.lang.Object"); + assertThat(get(TestSubInterface.class).getSuperClassName()).isIn(null, "java.lang.Object"); + } + + @Test + void getInterfaceNamesWhenHasInterfacesReturnsNames() { + assertThat(get(TestSubclass.class).getInterfaceNames()).containsExactly(TestInterface.class.getName()); + assertThat(get(TestSubInterface.class).getInterfaceNames()).containsExactly(TestInterface.class.getName()); + } + + @Test + void getInterfaceNamesWhenHasNoInterfacesReturnsEmptyArray() { + assertThat(get(TestClass.class).getInterfaceNames()).isEmpty(); + } + + @Test + void getMemberClassNamesWhenHasMemberClassesReturnsNames() { + assertThat(get(TestMemberClass.class).getMemberClassNames()).containsExactlyInAnyOrder( + TestMemberClass.TestMemberClassInnerClass.class.getName(), TestMemberClass.TestMemberClassInnerInterface.class.getName()); + } + + @Test + void getMemberClassNamesWhenHasNoMemberClassesReturnsEmptyArray() { + assertThat(get(TestClass.class).getMemberClassNames()).isEmpty(); + } + + public static class TestClass { + } + + public interface TestInterface { + } + + public interface TestSubInterface extends TestInterface { + } + + public @interface TestAnnotation { + } + + public static final class TestFinalClass { + } + + public class TestNonStaticInnerClass { + } + + public static class TestSubclass extends TestClass implements TestInterface { + } + + public static class TestMemberClass { + + public static class TestMemberClassInnerClass { + } + + interface TestMemberClassInnerInterface { + } + } } - public static class WithAnnotatedMethod { + @Nested + class AnnotationTests { + + @Test + void getAnnotationsReturnsDirectAnnotations() { + assertThat(get(WithDirectAnnotations.class).getAnnotations().stream()) + .filteredOn(MergedAnnotation::isDirectlyPresent) + .extracting(a -> a.getType().getName()) + .containsExactlyInAnyOrder(DirectAnnotation1.class.getName(), DirectAnnotation2.class.getName()); + } + + @Test + void isAnnotatedWhenMatchesDirectAnnotationReturnsTrue() { + assertThat(get(WithDirectAnnotations.class).isAnnotated(DirectAnnotation1.class.getName())).isTrue(); + } + + @Test + void isAnnotatedWhenMatchesMetaAnnotationReturnsTrue() { + assertThat(get(WithMetaAnnotations.class).isAnnotated(MetaAnnotation2.class.getName())).isTrue(); + } + + @Test + void isAnnotatedWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() { + assertThat(get(NoAnnotationClass.class).isAnnotated(DirectAnnotation1.class.getName())).isFalse(); + } + + @Test + void getAnnotationAttributesReturnsAttributes() { + assertThat(get(WithAnnotationAttributes.class).getAnnotationAttributes(AnnotationAttributes.class.getName())) + .containsOnly(entry("name", "test"), entry("size", 1)); + } + + @Test + void getAllAnnotationAttributesReturnsAllAttributes() { + MultiValueMap attributes = + get(WithMetaAnnotationAttributes.class).getAllAnnotationAttributes(AnnotationAttributes.class.getName()); + assertThat(attributes).containsOnlyKeys("name", "size"); + assertThat(attributes.get("name")).containsExactlyInAnyOrder("m1", "m2"); + assertThat(attributes.get("size")).containsExactlyInAnyOrder(1, 2); + } + + @Test + void getComplexAttributeTypesReturnsAll() { + MultiValueMap attributes = + get(WithComplexAttributeTypes.class).getAllAnnotationAttributes(ComplexAttributes.class.getName()); + assertThat(attributes).containsOnlyKeys("names", "count", "type", "subAnnotation"); + assertThat(attributes.get("names")).hasSize(1); + assertThat(attributes.get("names").get(0)).isEqualTo(new String[]{"first", "second"}); + assertThat(attributes.get("count")).containsExactlyInAnyOrder(TestEnum.ONE); + assertThat(attributes.get("type")).containsExactlyInAnyOrder(TestEnum.class); + assertThat(attributes.get("subAnnotation")).hasSize(1); + } + + @Test + void getComplexAttributeTypesReturnsAllWithKotlinMetadata() { + MultiValueMap attributes = + get(WithComplexAttributeTypes.class).getAllAnnotationAttributes(Metadata.class.getName()); + assertThat(attributes).containsKeys("k", "mv"); + int[] values = {42}; + assertThat(attributes.get("mv")).hasSize(1); + assertThat(attributes.get("mv").get(0)).isEqualTo(values); + } + + @Test + void getRepeatableReturnsAttributes() { + MultiValueMap attributes = + get(WithRepeatableAnnotations.class).getAllAnnotationAttributes(RepeatableAnnotations.class.getName()); + assertThat(attributes).containsKeys("value"); + assertThat(attributes.get("value")).hasSize(1); + } + + @Test + void getAnnotationTypesReturnsDirectAnnotations() { + AnnotationMetadata metadata = get(WithDirectAnnotations.class); + assertThat(metadata.getAnnotationTypes()).containsExactlyInAnyOrder( + DirectAnnotation1.class.getName(), DirectAnnotation2.class.getName()); + } + + @Test + void getMetaAnnotationTypesReturnsMetaAnnotations() { + AnnotationMetadata metadata = get(WithMetaAnnotations.class); + assertThat(metadata.getMetaAnnotationTypes(MetaAnnotationRoot.class.getName())) + .containsExactlyInAnyOrder(MetaAnnotation1.class.getName(), MetaAnnotation2.class.getName()); + } + + @Test + void hasAnnotationWhenMatchesDirectAnnotationReturnsTrue() { + assertThat(get(WithDirectAnnotations.class).hasAnnotation(DirectAnnotation1.class.getName())).isTrue(); + } + + @Test + void hasAnnotationWhenMatchesMetaAnnotationReturnsFalse() { + assertThat(get(WithMetaAnnotations.class).hasAnnotation(MetaAnnotation1.class.getName())).isFalse(); + assertThat(get(WithMetaAnnotations.class).hasAnnotation(MetaAnnotation2.class.getName())).isFalse(); + } + + @Test + void hasAnnotationWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() { + assertThat(get(NoAnnotationClass.class).hasAnnotation(DirectAnnotation1.class.getName())).isFalse(); + } + + @Test + void hasMetaAnnotationWhenMatchesDirectReturnsFalse() { + assertThat(get(WithDirectAnnotations.class).hasMetaAnnotation(DirectAnnotation1.class.getName())).isFalse(); + } + + @Test + void hasMetaAnnotationWhenMatchesMetaAnnotationReturnsTrue() { + assertThat(get(WithMetaAnnotations.class).hasMetaAnnotation(MetaAnnotation1.class.getName())).isTrue(); + assertThat(get(WithMetaAnnotations.class).hasMetaAnnotation(MetaAnnotation2.class.getName())).isTrue(); + } + + @Test + void hasMetaAnnotationWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() { + assertThat(get(NoAnnotationClass.class).hasMetaAnnotation(MetaAnnotation1.class.getName())).isFalse(); + } + + @Test + void hasAnnotatedMethodsWhenMatchesDirectAnnotationReturnsTrue() { + assertThat(get(WithAnnotatedMethod.class).hasAnnotatedMethods(DirectAnnotation1.class.getName())).isTrue(); + } + + @Test + void hasAnnotatedMethodsWhenMatchesMetaAnnotationReturnsTrue() { + assertThat(get(WithMetaAnnotatedMethod.class).hasAnnotatedMethods(MetaAnnotation2.class.getName())).isTrue(); + } + + @Test + void hasAnnotatedMethodsWhenDoesNotMatchAnyAnnotationReturnsFalse() { + assertThat(get(WithAnnotatedMethod.class).hasAnnotatedMethods(MetaAnnotation2.class.getName())).isFalse(); + assertThat(get(WithNonAnnotatedMethod.class).hasAnnotatedMethods(DirectAnnotation1.class.getName())).isFalse(); + } + + @Test + void getAnnotatedMethodsReturnsMatchingAnnotatedAndMetaAnnotatedMethods() { + assertThat(get(WithDirectAndMetaAnnotatedMethods.class).getAnnotatedMethods(MetaAnnotation2.class.getName())) + .extracting(MethodMetadata::getMethodName) + .containsExactlyInAnyOrder("direct", "meta"); + } + + public static class WithAnnotatedMethod { + + @DirectAnnotation1 + public void test() { + } + + } + + public static class WithMetaAnnotatedMethod { + + @MetaAnnotationRoot + public void test() { + } + + } + + public static class WithNonAnnotatedMethod { + + } + + public static class WithDirectAndMetaAnnotatedMethods { + + @MetaAnnotation2 + public void direct() { + } + + @MetaAnnotationRoot + public void meta() { + } + + } + + @AnnotationAttributes(name = "test", size = 1) + public static class WithAnnotationAttributes { + } + + @MetaAnnotationAttributes1 + @MetaAnnotationAttributes2 + public static class WithMetaAnnotationAttributes { + } + + @Retention(RetentionPolicy.RUNTIME) + @AnnotationAttributes(name = "m1", size = 1) + public @interface MetaAnnotationAttributes1 { + } + + @Retention(RetentionPolicy.RUNTIME) + @AnnotationAttributes(name = "m2", size = 2) + public @interface MetaAnnotationAttributes2 { + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface AnnotationAttributes { + + String name(); + + int size(); + + } + + @ComplexAttributes(names = {"first", "second"}, count = TestEnum.ONE, + type = TestEnum.class, subAnnotation = @SubAnnotation(name="spring")) + @Metadata(mv = {42}) + public static class WithComplexAttributeTypes { + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface ComplexAttributes { + + String[] names(); + + TestEnum count(); + + Class type(); + + SubAnnotation subAnnotation(); + } + + public @interface SubAnnotation { + + String name(); + } + + public enum TestEnum { + ONE, TWO, THREE + } + + @RepeatableAnnotation(name = "first") + @RepeatableAnnotation(name = "second") + public static class WithRepeatableAnnotations { + + } + + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(RepeatableAnnotations.class) + public @interface RepeatableAnnotation { + + String name(); + + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface RepeatableAnnotations { + + RepeatableAnnotation[] value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface DirectAnnotation1 { + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface DirectAnnotation2 { + } + + @Retention(RetentionPolicy.RUNTIME) + @MetaAnnotation1 + public @interface MetaAnnotationRoot { + } + + @Retention(RetentionPolicy.RUNTIME) + @MetaAnnotation2 + public @interface MetaAnnotation1 { + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface MetaAnnotation2 { + } @DirectAnnotation1 - public void test() { - } - - } - - public static class WithMetaAnnotatedMethod { - - @MetaAnnotationRoot - public void test() { - } - - } - - public static class WithNonAnnotatedMethod { - - public void test() { - } - - } - - public static class WithDirectAndMetaAnnotatedMethods { - - @MetaAnnotation2 - public void direct() { + @DirectAnnotation2 + public static class WithDirectAnnotations { } @MetaAnnotationRoot - public void meta() { + public static class WithMetaAnnotations { + } + + static class NoAnnotationClass { + } } - @AnnotationAttributes(name = "test", size = 1) - public static class WithAnnotationAttributes { - } + @Nested + class MethodTests { - @MetaAnnotationAttributes1 - @MetaAnnotationAttributes2 - public static class WithMetaAnnotationAttributes { - } + @Test + void declaredMethodsToString() { + List methods = get(TestMethods.class).getDeclaredMethods().stream().map(Object::toString).toList(); + List expected = Arrays.stream(TestMethods.class.getDeclaredMethods()).map(Object::toString).toList(); + assertThat(methods).containsExactlyInAnyOrderElementsOf(expected); + } - @Retention(RetentionPolicy.RUNTIME) - @AnnotationAttributes(name = "m1", size = 1) - public @interface MetaAnnotationAttributes1 { - } + static class TestMethods { + public String test1(String argument) { + return "test"; + } - @Retention(RetentionPolicy.RUNTIME) - @AnnotationAttributes(name = "m2", size = 2) - public @interface MetaAnnotationAttributes2 { - } + public String test2(String argument) { + return "test"; + } - @Retention(RetentionPolicy.RUNTIME) - public @interface AnnotationAttributes { - - String name(); - - int size(); + public String test3(String argument) { + return "test"; + } + } } 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 f66c81bc35..27b53e3de4 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -21,7 +21,8 @@ import org.springframework.core.type.AnnotationMetadata; /** * Tests for {@link SimpleAnnotationMetadata} and - * {@link SimpleAnnotationMetadataReadingVisitor}. + * {@link SimpleAnnotationMetadataReadingVisitor} on Java < 24, + * and for the ClassFile API variant on Java >= 24. * * @author Phillip Webb */ @@ -30,9 +31,8 @@ class SimpleAnnotationMetadataTests extends AbstractAnnotationMetadataTests { @Override protected AnnotationMetadata get(Class source) { try { - return new SimpleMetadataReaderFactory( - source.getClassLoader()).getMetadataReader( - source.getName()).getAnnotationMetadata(); + return MetadataReaderFactory.create(source.getClassLoader()) + .getMetadataReader(source.getName()).getAnnotationMetadata(); } catch (Exception ex) { throw new IllegalStateException(ex);