Browse Source
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-33616pull/33997/head
11 changed files with 916 additions and 7 deletions
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
@ -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<MergedAnnotation<?>> annotations = new LinkedHashSet<>(4); |
||||||
|
annotationAttribute.annotations().forEach(ann -> { |
||||||
|
MergedAnnotation<java.lang.annotation.Annotation> mergedAnnotation = createMergedAnnotation(entryName, ann, classLoader); |
||||||
|
if (mergedAnnotation != null) { |
||||||
|
annotations.add(mergedAnnotation); |
||||||
|
} |
||||||
|
}); |
||||||
|
return MergedAnnotations.of(annotations); |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
@Nullable |
||||||
|
private static <A extends java.lang.annotation.Annotation> MergedAnnotation<A> createMergedAnnotation(String entryName, Annotation annotation, @Nullable ClassLoader classLoader) { |
||||||
|
String typeName = fromTypeDescriptor(annotation.className().stringValue()); |
||||||
|
if (AnnotationFilter.PLAIN.matches(typeName)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
Map<String, Object> attributes = new LinkedHashMap<>(4); |
||||||
|
try { |
||||||
|
Class<A> annotationType = (Class<A>) ClassUtils.forName(typeName, classLoader); |
||||||
|
for (AnnotationElement element : annotation.elements()) { |
||||||
|
attributes.put(element.name().stringValue(), readAnnotationValue(element.value(), classLoader)); |
||||||
|
} |
||||||
|
Map<String, Object> 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<AnnotationValue> rawValues = arrayValue.values(); |
||||||
|
List<Object> 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<Object> 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 <E extends Enum<E>> Enum<E> parseEnum(AnnotationValue.OfEnum enumValue, @Nullable ClassLoader classLoader) throws ClassNotFoundException { |
||||||
|
String enumClassName = fromTypeDescriptor(enumValue.className().stringValue()); |
||||||
|
Class<E> enumClass = (Class<E>) ClassUtils.forName(enumClassName, classLoader); |
||||||
|
return Enum.valueOf(enumClass, enumValue.constantName().stringValue()); |
||||||
|
} |
||||||
|
|
||||||
|
record Source(String entryName) { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -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<String> interfaceNames; |
||||||
|
|
||||||
|
private final Set<String> memberClassNames; |
||||||
|
|
||||||
|
private final Set<MethodMetadata> declaredMethods; |
||||||
|
|
||||||
|
private final MergedAnnotations mergedAnnotations; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private Set<String> annotationTypes; |
||||||
|
|
||||||
|
ClassFileClassMetadata(String className, AccessFlags accessFlags, @Nullable String enclosingClassName, |
||||||
|
@Nullable String superClassName, boolean independentInnerClass, Set<String> interfaceNames, |
||||||
|
Set<String> memberClassNames, Set<MethodMetadata> 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<String> getAnnotationTypes() { |
||||||
|
Set<String> annotationTypes = this.annotationTypes; |
||||||
|
if (annotationTypes == null) { |
||||||
|
annotationTypes = Collections.unmodifiableSet( |
||||||
|
AnnotationMetadata.super.getAnnotationTypes()); |
||||||
|
this.annotationTypes = annotationTypes; |
||||||
|
} |
||||||
|
return annotationTypes; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Set<MethodMetadata> getAnnotatedMethods(String annotationName) { |
||||||
|
Set<MethodMetadata> result = new LinkedHashSet<>(4); |
||||||
|
for (MethodMetadata annotatedMethod : this.declaredMethods) { |
||||||
|
if (annotatedMethod.isAnnotated(annotationName)) { |
||||||
|
result.add(annotatedMethod); |
||||||
|
} |
||||||
|
} |
||||||
|
return Collections.unmodifiableSet(result); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Set<MethodMetadata> 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<AccessFlag> innerAccessFlags; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private String enclosingClassName; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private String superClassName; |
||||||
|
|
||||||
|
private Set<String> interfaceNames = new HashSet<>(); |
||||||
|
|
||||||
|
private Set<String> memberClassNames = new HashSet<>(); |
||||||
|
|
||||||
|
private Set<MethodMetadata> 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<AccessFlag> 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); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -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; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -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()); |
||||||
|
} |
||||||
|
} |
||||||
@ -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(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue