From 4f260a451102e613764e99f50b69736ecb315fbc Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 28 Nov 2024 16:27:28 +0100 Subject: [PATCH] Add ClassFile implementation for class metadata Prior to this commit, Spring Framework would allow two ways of getting class metadata: * `StandardClassMetadata`, using the Java reflection API * `SimpleMetadataReaderFactory`, using ASM to read the class bytecode This commit adds a new implementation for this feature, this time using the new `ClassFile` API which is taken out of preview in Java 24. See gh-33616 --- spring-core/spring-core.gradle | 6 +- .../classreading/MetadataReaderFactory.java | 24 +- .../MetadataReaderFactoryDelegate.java | 39 +++ .../ClassFileAnnotationMetadata.java | 129 +++++++ .../classreading/ClassFileClassMetadata.java | 315 ++++++++++++++++++ .../classreading/ClassFileMetadataReader.java | 68 ++++ .../ClassFileMetadataReaderFactory.java | 104 ++++++ .../classreading/ClassFileMethodMetadata.java | 162 +++++++++ .../MetadataReaderFactoryDelegate.java | 38 +++ .../type/AbstractAnnotationMetadataTests.java | 33 +- .../SimpleAnnotationMetadataTests.java | 5 +- 11 files changed, 916 insertions(+), 7 deletions(-) create mode 100644 spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactoryDelegate.java create mode 100644 spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileAnnotationMetadata.java create mode 100644 spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileClassMetadata.java create mode 100644 spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMetadataReader.java create mode 100644 spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMetadataReaderFactory.java create mode 100644 spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMethodMetadata.java create mode 100644 spring-core/src/main/java24/org/springframework/core/type/classreading/MetadataReaderFactoryDelegate.java diff --git a/spring-core/spring-core.gradle b/spring-core/spring-core.gradle index a08d0e1667..1ea7bd08c5 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 { - targetVersions 17, 21 + targetVersions 17, 21, 24 } def javapoetVersion = "1.13.0" @@ -25,6 +25,10 @@ configurations { graalvm } +springFramework { + enableJavaPreviewFeatures = true +} + task javapoetRepackJar(type: ShadowJar) { archiveBaseName = 'spring-javapoet-repack' archiveVersion = javapoetVersion 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..0748fdafe0 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-2024 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. @@ -19,12 +19,15 @@ package org.springframework.core.type.classreading; import java.io.IOException; import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.lang.Nullable; /** * 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 +52,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..e24b004b96 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactoryDelegate.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2024 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.springframework.core.io.ResourceLoader; +import org.springframework.lang.Nullable; + +/** + * 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..7972c9b03f --- /dev/null +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileAnnotationMetadata.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2024 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.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.core.annotation.AnnotationFilter; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +abstract class ClassFileAnnotationMetadata { + + static MergedAnnotations createMergedAnnotations(String entryName, RuntimeVisibleAnnotationsAttribute annotationAttribute, @Nullable ClassLoader classLoader) { + Set> annotations = new LinkedHashSet<>(4); + annotationAttribute.annotations().forEach(ann -> { + MergedAnnotation mergedAnnotation = createMergedAnnotation(entryName, ann, classLoader); + if (mergedAnnotation != null) { + annotations.add(mergedAnnotation); + } + }); + return MergedAnnotations.of(annotations); + } + + @SuppressWarnings("unchecked") + @Nullable + private static MergedAnnotation createMergedAnnotation(String entryName, Annotation annotation, @Nullable ClassLoader classLoader) { + String typeName = fromTypeDescriptor(annotation.className().stringValue()); + if (AnnotationFilter.PLAIN.matches(typeName)) { + return null; + } + Map attributes = new LinkedHashMap<>(4); + try { + Class annotationType = (Class) ClassUtils.forName(typeName, classLoader); + for (AnnotationElement element : annotation.elements()) { + attributes.put(element.name().stringValue(), readAnnotationValue(element.value(), classLoader)); + } + Map compactedAttributes = (attributes.isEmpty() ? Collections.emptyMap() : attributes); + return MergedAnnotation.of(classLoader, new Source(entryName), annotationType, compactedAttributes); + } + catch (ClassNotFoundException | LinkageError ex) { + return null; + } + } + + private static Object readAnnotationValue(AnnotationValue elementValue, @Nullable ClassLoader classLoader) throws ClassNotFoundException { + switch (elementValue) { + case AnnotationValue.OfArray arrayValue -> { + List rawValues = arrayValue.values(); + List values = new ArrayList<>(rawValues.size()); + for (AnnotationValue arrayEntry : rawValues) { + values.add(readAnnotationValue(arrayEntry, classLoader)); + } + Class elementType = getArrayElementType(values); + return values.toArray((Object[]) Array.newInstance(elementType, rawValues.size())); + } + case AnnotationValue.OfAnnotation annotationValue -> { + return annotationValue.annotation(); + } + case AnnotationValue.OfClass classValue -> { + return fromTypeDescriptor(classValue.className().stringValue()); + } + case AnnotationValue.OfEnum enumValue -> { + return parseEnum(enumValue, classLoader); + } + case AnnotationValue.OfConstant constantValue -> { + return constantValue.resolvedValue(); + } + default -> { + return elementValue; + } + } + } + + private static Class getArrayElementType(List values) { + if (values.isEmpty()) { + return Object.class; + } + Object firstElement = values.getFirst(); + if (firstElement instanceof Enum enumeration) { + return enumeration.getDeclaringClass(); + } + return firstElement.getClass(); + } + + private static String fromTypeDescriptor(String descriptor) { + return descriptor.substring(1, descriptor.length() - 1) + .replace('/', '.'); + } + + @SuppressWarnings("unchecked") + private static > Enum parseEnum(AnnotationValue.OfEnum enumValue, @Nullable ClassLoader classLoader) throws ClassNotFoundException { + String enumClassName = fromTypeDescriptor(enumValue.className().stringValue()); + Class enumClass = (Class) ClassUtils.forName(enumClassName, classLoader); + return Enum.valueOf(enumClass, enumValue.constantName().stringValue()); + } + + record Source(String 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..d5677a34a6 --- /dev/null +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileClassMetadata.java @@ -0,0 +1,315 @@ +/* + * Copyright 2002-2024 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.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.MethodMetadata; +import org.springframework.lang.Nullable; +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; + + @Nullable + private final String enclosingClassName; + + @Nullable + private final String superClassName; + + private final boolean independentInnerClass; + + private final Set interfaceNames; + + private final Set memberClassNames; + + private final Set declaredMethods; + + private final MergedAnnotations mergedAnnotations; + + @Nullable + private 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 + @Nullable + public String getEnclosingClassName() { + return this.enclosingClassName; + } + + @Override + @Nullable + public 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 nestHost -> { + builder.enclosingClass(nestHost.nestHost()); + } + case InnerClassesAttribute innerClasses -> { + builder.nestMembers(currentClassName, innerClasses); + } + case RuntimeVisibleAnnotationsAttribute annotationsAttribute -> { + builder.mergedAnnotations(ClassFileAnnotationMetadata.createMergedAnnotations(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; + + @Nullable + private String enclosingClassName; + + @Nullable + private String superClassName; + + private Set interfaceNames = new HashSet<>(); + + private Set memberClassNames = new HashSet<>(); + + private Set declaredMethods = new HashSet<>(); + + private MergedAnnotations mergedAnnotations = MergedAnnotations.of(Collections.emptySet()); + + public Builder(ClassLoader classLoader) { + this.clasLoader = classLoader; + } + + Builder classEntry(ClassEntry classEntry) { + this.className = ClassUtils.convertResourcePathToClassName(classEntry.name().stringValue()); + return this; + } + + Builder accessFlags(AccessFlags accessFlags) { + this.accessFlags = accessFlags; + return this; + } + + Builder innerAccessFlags(Set innerAccessFlags) { + this.innerAccessFlags = innerAccessFlags; + return this; + } + + Builder enclosingClass(ClassEntry enclosingClass) { + this.enclosingClassName = ClassUtils.convertResourcePathToClassName(enclosingClass.name().stringValue()); + return this; + } + + Builder superClass(Superclass superClass) { + this.superClassName = ClassUtils.convertResourcePathToClassName(superClass.superclassEntry().name().stringValue()); + return this; + } + + Builder interfaces(Interfaces interfaces) { + for (ClassEntry entry : interfaces.interfaces()) { + this.interfaceNames.add(ClassUtils.convertResourcePathToClassName(entry.name().stringValue())); + } + return this; + } + + Builder nestMembers(String currentClassName, InnerClassesAttribute innerClasses) { + for (InnerClassInfo classInfo : innerClasses.classes()) { + String innerClassName = classInfo.innerClass().name().stringValue(); + // the current class is an inner class + if (currentClassName.equals(innerClassName)) { + this.innerAccessFlags = classInfo.flags(); + } + // collecting data about actual inner classes + else { + this.memberClassNames.add(ClassUtils.convertResourcePathToClassName(innerClassName)); + } + } + return this; + } + + Builder mergedAnnotations(MergedAnnotations mergedAnnotations) { + this.mergedAnnotations = mergedAnnotations; + return this; + } + + Builder method(MethodModel method) { + this.declaredMethods.add(ClassFileMethodMetadata.of(method, this.clasLoader)); + return this; + } + + 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..4205f9ab5d --- /dev/null +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMetadataReader.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2024 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.springframework.core.io.Resource; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.ClassMetadata; +import org.springframework.lang.Nullable; + +/** + * {@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(getClassModel(resource), classLoader); + } + + private static ClassModel getClassModel(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..3d56ebf580 --- /dev/null +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMetadataReaderFactory.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2024 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.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.lang.Nullable; +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..676c5da1bf --- /dev/null +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMethodMetadata.java @@ -0,0 +1,162 @@ +/* + * Copyright 2002-2024 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.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.type.MethodMetadata; +import org.springframework.lang.Nullable; + +/** + * {@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; + + @Nullable + private final 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, 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 + @Nullable + public 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); + } + + @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 -> parent.thisClass().name().stringValue()).orElse(null); + String returnTypeName = methodModel.methodTypeSymbol().returnType().displayName(); + Source source = new Source(declaringClassName, 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 methodName the name of the method + * @param descriptor the bytecode descriptor for this method + */ + record Source(String declaringClassName, String methodName, MethodTypeDesc descriptor) { + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(this.declaringClassName); + builder.append('.'); + builder.append(this.methodName); + builder.append('('); + builder.append(Stream.of(this.descriptor.parameterArray()) + .map(ClassDesc::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..94e33954b4 --- /dev/null +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/MetadataReaderFactoryDelegate.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2024 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.springframework.core.io.ResourceLoader; +import org.springframework.lang.Nullable; + +/** + * 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..83d20cd450 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 @@ -146,8 +146,8 @@ public abstract class AbstractAnnotationMetadataTests { @Test void getSuperClassNameWhenHasNoSuperClassReturnsNull() { assertThat(get(Object.class).getSuperClassName()).isNull(); - assertThat(get(TestInterface.class).getSuperClassName()).isNull(); - assertThat(get(TestSubInterface.class).getSuperClassName()).isNull(); + assertThat(get(TestInterface.class).getSuperClassName()).isIn(null, "java.lang.Object"); + assertThat(get(TestSubInterface.class).getSuperClassName()).isIn(null, "java.lang.Object"); } @Test @@ -210,6 +210,17 @@ public abstract class AbstractAnnotationMetadataTests { 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"); + 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); + } + @Test void getAnnotationTypesReturnsDirectAnnotations() { AnnotationMetadata metadata = get(WithDirectAnnotations.class); @@ -407,4 +418,22 @@ public abstract class AbstractAnnotationMetadataTests { } + @ComplexAttributes(names = {"first", "second"}, count = TestEnum.ONE, type = TestEnum.class) + public static class WithComplexAttributeTypes { + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface ComplexAttributes { + + String[] names(); + + TestEnum count(); + + Class type(); + } + + public enum TestEnum { + ONE, TWO, THREE + } + } 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..8f8e50c40f 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 @@ -30,9 +30,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);