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
This commit is contained in:
parent
28273b9309
commit
4f260a4511
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<String, Object> 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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue