Add ClassFile variant for class metadata reading
Prior to this commit, Spring Framework would use its own ASM fork to read class/method/annotation metadata from bytecode. This is typically used in configuration class parsing to build bean definitions without actually loading classes at runtime at that step. This commit adds support for a new metadata reading implementation that uses the ClassFile API available as of Java 24. For now, this is turned on by default for Java 24+. Closes gh-33616
This commit is contained in:
parent
20b35f068a
commit
fb423d66e3
|
@ -84,7 +84,6 @@ import org.springframework.core.annotation.MergedAnnotations;
|
||||||
import org.springframework.core.type.AnnotationMetadata;
|
import org.springframework.core.type.AnnotationMetadata;
|
||||||
import org.springframework.core.type.MethodMetadata;
|
import org.springframework.core.type.MethodMetadata;
|
||||||
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
||||||
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
|
|
||||||
import org.springframework.javapoet.ClassName;
|
import org.springframework.javapoet.ClassName;
|
||||||
import org.springframework.javapoet.CodeBlock;
|
import org.springframework.javapoet.CodeBlock;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
@ -271,7 +270,7 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
|
||||||
"AutowiredAnnotationBeanPostProcessor requires a ConfigurableListableBeanFactory: " + beanFactory);
|
"AutowiredAnnotationBeanPostProcessor requires a ConfigurableListableBeanFactory: " + beanFactory);
|
||||||
}
|
}
|
||||||
this.beanFactory = clbf;
|
this.beanFactory = clbf;
|
||||||
this.metadataReaderFactory = new SimpleMetadataReaderFactory(clbf.getBeanClassLoader());
|
this.metadataReaderFactory = MetadataReaderFactory.create(clbf.getBeanClassLoader());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ apply plugin: "kotlin"
|
||||||
apply plugin: "kotlinx-serialization"
|
apply plugin: "kotlinx-serialization"
|
||||||
|
|
||||||
multiRelease {
|
multiRelease {
|
||||||
releaseVersions 21
|
releaseVersions 21, 24
|
||||||
}
|
}
|
||||||
|
|
||||||
def javapoetVersion = "1.13.0"
|
def javapoetVersion = "1.13.0"
|
||||||
|
@ -20,6 +20,7 @@ def objenesisVersion = "3.4"
|
||||||
configurations {
|
configurations {
|
||||||
java21Api.extendsFrom(api)
|
java21Api.extendsFrom(api)
|
||||||
java21Implementation.extendsFrom(implementation)
|
java21Implementation.extendsFrom(implementation)
|
||||||
|
java24Api.extendsFrom(api)
|
||||||
javapoet
|
javapoet
|
||||||
objenesis
|
objenesis
|
||||||
graalvm
|
graalvm
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2022 the original author or authors.
|
* Copyright 2002-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -36,11 +36,13 @@ import org.springframework.core.io.ResourceLoader;
|
||||||
* @author Costin Leau
|
* @author Costin Leau
|
||||||
* @since 2.5
|
* @since 2.5
|
||||||
*/
|
*/
|
||||||
public class CachingMetadataReaderFactory extends SimpleMetadataReaderFactory {
|
public class CachingMetadataReaderFactory implements MetadataReaderFactory {
|
||||||
|
|
||||||
/** Default maximum number of entries for a local MetadataReader cache: 256. */
|
/** Default maximum number of entries for a local MetadataReader cache: 256. */
|
||||||
public static final int DEFAULT_CACHE_LIMIT = 256;
|
public static final int DEFAULT_CACHE_LIMIT = 256;
|
||||||
|
|
||||||
|
private final MetadataReaderFactory delegate;
|
||||||
|
|
||||||
/** MetadataReader cache: either local or shared at the ResourceLoader level. */
|
/** MetadataReader cache: either local or shared at the ResourceLoader level. */
|
||||||
private @Nullable Map<Resource, MetadataReader> metadataReaderCache;
|
private @Nullable Map<Resource, MetadataReader> metadataReaderCache;
|
||||||
|
|
||||||
|
@ -50,7 +52,7 @@ public class CachingMetadataReaderFactory extends SimpleMetadataReaderFactory {
|
||||||
* using a local resource cache.
|
* using a local resource cache.
|
||||||
*/
|
*/
|
||||||
public CachingMetadataReaderFactory() {
|
public CachingMetadataReaderFactory() {
|
||||||
super();
|
this.delegate = MetadataReaderFactory.create((ClassLoader) null);
|
||||||
setCacheLimit(DEFAULT_CACHE_LIMIT);
|
setCacheLimit(DEFAULT_CACHE_LIMIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +62,7 @@ public class CachingMetadataReaderFactory extends SimpleMetadataReaderFactory {
|
||||||
* @param classLoader the ClassLoader to use
|
* @param classLoader the ClassLoader to use
|
||||||
*/
|
*/
|
||||||
public CachingMetadataReaderFactory(@Nullable ClassLoader classLoader) {
|
public CachingMetadataReaderFactory(@Nullable ClassLoader classLoader) {
|
||||||
super(classLoader);
|
this.delegate = MetadataReaderFactory.create(classLoader);
|
||||||
setCacheLimit(DEFAULT_CACHE_LIMIT);
|
setCacheLimit(DEFAULT_CACHE_LIMIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +74,7 @@ public class CachingMetadataReaderFactory extends SimpleMetadataReaderFactory {
|
||||||
* @see DefaultResourceLoader#getResourceCache
|
* @see DefaultResourceLoader#getResourceCache
|
||||||
*/
|
*/
|
||||||
public CachingMetadataReaderFactory(@Nullable ResourceLoader resourceLoader) {
|
public CachingMetadataReaderFactory(@Nullable ResourceLoader resourceLoader) {
|
||||||
super(resourceLoader);
|
this.delegate = MetadataReaderFactory.create(resourceLoader);
|
||||||
if (resourceLoader instanceof DefaultResourceLoader defaultResourceLoader) {
|
if (resourceLoader instanceof DefaultResourceLoader defaultResourceLoader) {
|
||||||
this.metadataReaderCache = defaultResourceLoader.getResourceCache(MetadataReader.class);
|
this.metadataReaderCache = defaultResourceLoader.getResourceCache(MetadataReader.class);
|
||||||
}
|
}
|
||||||
|
@ -81,7 +83,6 @@ public class CachingMetadataReaderFactory extends SimpleMetadataReaderFactory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specify the maximum number of entries for the MetadataReader cache.
|
* Specify the maximum number of entries for the MetadataReader cache.
|
||||||
* <p>Default is 256 for a local cache, whereas a shared cache is
|
* <p>Default is 256 for a local cache, whereas a shared cache is
|
||||||
|
@ -112,6 +113,10 @@ public class CachingMetadataReaderFactory extends SimpleMetadataReaderFactory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MetadataReader getMetadataReader(String className) throws IOException {
|
||||||
|
return this.delegate.getMetadataReader(className);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MetadataReader getMetadataReader(Resource resource) throws IOException {
|
public MetadataReader getMetadataReader(Resource resource) throws IOException {
|
||||||
|
@ -119,7 +124,7 @@ public class CachingMetadataReaderFactory extends SimpleMetadataReaderFactory {
|
||||||
// No synchronization necessary...
|
// No synchronization necessary...
|
||||||
MetadataReader metadataReader = this.metadataReaderCache.get(resource);
|
MetadataReader metadataReader = this.metadataReaderCache.get(resource);
|
||||||
if (metadataReader == null) {
|
if (metadataReader == null) {
|
||||||
metadataReader = super.getMetadataReader(resource);
|
metadataReader = this.delegate.getMetadataReader(resource);
|
||||||
this.metadataReaderCache.put(resource, metadataReader);
|
this.metadataReaderCache.put(resource, metadataReader);
|
||||||
}
|
}
|
||||||
return metadataReader;
|
return metadataReader;
|
||||||
|
@ -128,14 +133,14 @@ public class CachingMetadataReaderFactory extends SimpleMetadataReaderFactory {
|
||||||
synchronized (this.metadataReaderCache) {
|
synchronized (this.metadataReaderCache) {
|
||||||
MetadataReader metadataReader = this.metadataReaderCache.get(resource);
|
MetadataReader metadataReader = this.metadataReaderCache.get(resource);
|
||||||
if (metadataReader == null) {
|
if (metadataReader == null) {
|
||||||
metadataReader = super.getMetadataReader(resource);
|
metadataReader = this.delegate.getMetadataReader(resource);
|
||||||
this.metadataReaderCache.put(resource, metadataReader);
|
this.metadataReaderCache.put(resource, metadataReader);
|
||||||
}
|
}
|
||||||
return metadataReader;
|
return metadataReader;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return super.getMetadataReader(resource);
|
return this.delegate.getMetadataReader(resource);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2023 the original author or authors.
|
* Copyright 2002-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -18,13 +18,17 @@ package org.springframework.core.type.classreading;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.ResourceLoader;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory interface for {@link MetadataReader} instances.
|
* Factory interface for {@link MetadataReader} instances.
|
||||||
* Allows for caching a MetadataReader per original resource.
|
* Allows for caching a MetadataReader per original resource.
|
||||||
*
|
*
|
||||||
* @author Juergen Hoeller
|
* @author Juergen Hoeller
|
||||||
|
* @author Brian Clozel
|
||||||
* @since 2.5
|
* @since 2.5
|
||||||
* @see SimpleMetadataReaderFactory
|
* @see SimpleMetadataReaderFactory
|
||||||
* @see CachingMetadataReaderFactory
|
* @see CachingMetadataReaderFactory
|
||||||
|
@ -49,4 +53,23 @@ public interface MetadataReaderFactory {
|
||||||
*/
|
*/
|
||||||
MetadataReader getMetadataReader(Resource resource) throws IOException;
|
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,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.core.type.classreading;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
|
import org.springframework.core.io.ResourceLoader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal delegate for instantiating {@link MetadataReaderFactory} implementations.
|
||||||
|
* For JDK < 24, the {@link SimpleMetadataReaderFactory} is being used.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 7.0
|
||||||
|
* @see MetadataReaderFactory
|
||||||
|
*/
|
||||||
|
abstract class MetadataReaderFactoryDelegate {
|
||||||
|
|
||||||
|
static MetadataReaderFactory create(@Nullable ResourceLoader resourceLoader) {
|
||||||
|
return new SimpleMetadataReaderFactory(resourceLoader);
|
||||||
|
}
|
||||||
|
|
||||||
|
static MetadataReaderFactory create(@Nullable ClassLoader classLoader) {
|
||||||
|
return new SimpleMetadataReaderFactory(classLoader);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.core.type.classreading;
|
||||||
|
|
||||||
|
|
||||||
|
import java.lang.classfile.Annotation;
|
||||||
|
import java.lang.classfile.AnnotationElement;
|
||||||
|
import java.lang.classfile.AnnotationValue;
|
||||||
|
import java.lang.classfile.attribute.RuntimeVisibleAnnotationsAttribute;
|
||||||
|
import java.lang.reflect.Array;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
|
import org.springframework.core.annotation.AnnotationFilter;
|
||||||
|
import org.springframework.core.annotation.MergedAnnotation;
|
||||||
|
import org.springframework.core.annotation.MergedAnnotations;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse {@link RuntimeVisibleAnnotationsAttribute} into {@link MergedAnnotations}
|
||||||
|
* instances.
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
abstract class ClassFileAnnotationMetadata {
|
||||||
|
|
||||||
|
static MergedAnnotations createMergedAnnotations(String className, RuntimeVisibleAnnotationsAttribute annotationAttribute, @Nullable ClassLoader classLoader) {
|
||||||
|
Set<MergedAnnotation<?>> annotations = annotationAttribute.annotations()
|
||||||
|
.stream()
|
||||||
|
.map(ann -> createMergedAnnotation(className, ann, classLoader))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
return MergedAnnotations.of(annotations);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static <A extends java.lang.annotation.Annotation> @Nullable MergedAnnotation<A> createMergedAnnotation(String className, 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 {
|
||||||
|
for (AnnotationElement element : annotation.elements()) {
|
||||||
|
Object annotationValue = readAnnotationValue(className, element.value(), classLoader);
|
||||||
|
if (annotationValue != null) {
|
||||||
|
attributes.put(element.name().stringValue(), annotationValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<String, Object> compactedAttributes = (attributes.isEmpty() ? Collections.emptyMap() : attributes);
|
||||||
|
Class<A> annotationType = (Class<A>) ClassUtils.forName(typeName, classLoader);
|
||||||
|
return MergedAnnotation.of(classLoader, new Source(annotation), annotationType, compactedAttributes);
|
||||||
|
}
|
||||||
|
catch (ClassNotFoundException | LinkageError ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @Nullable Object readAnnotationValue(String className, AnnotationValue elementValue, @Nullable ClassLoader classLoader) {
|
||||||
|
switch (elementValue) {
|
||||||
|
case AnnotationValue.OfConstant constantValue -> {
|
||||||
|
return constantValue.resolvedValue();
|
||||||
|
}
|
||||||
|
case AnnotationValue.OfAnnotation annotationValue -> {
|
||||||
|
return createMergedAnnotation(className, annotationValue.annotation(), classLoader);
|
||||||
|
}
|
||||||
|
case AnnotationValue.OfClass classValue -> {
|
||||||
|
return fromTypeDescriptor(classValue.className().stringValue());
|
||||||
|
}
|
||||||
|
case AnnotationValue.OfEnum enumValue -> {
|
||||||
|
return parseEnum(enumValue, classLoader);
|
||||||
|
}
|
||||||
|
case AnnotationValue.OfArray arrayValue -> {
|
||||||
|
return parseArrayValue(className, classLoader, arrayValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String fromTypeDescriptor(String descriptor) {
|
||||||
|
return descriptor.substring(1, descriptor.length() - 1)
|
||||||
|
.replace('/', '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object parseArrayValue(String className, @org.jetbrains.annotations.Nullable ClassLoader classLoader, AnnotationValue.OfArray arrayValue) {
|
||||||
|
if (arrayValue.values().isEmpty()) {
|
||||||
|
return new Object[0];
|
||||||
|
}
|
||||||
|
Stream<AnnotationValue> stream = arrayValue.values().stream();
|
||||||
|
switch (arrayValue.values().getFirst()) {
|
||||||
|
case AnnotationValue.OfInt _ -> {
|
||||||
|
return stream.map(AnnotationValue.OfInt.class::cast).mapToInt(AnnotationValue.OfInt::intValue).toArray();
|
||||||
|
}
|
||||||
|
case AnnotationValue.OfDouble _ -> {
|
||||||
|
return stream.map(AnnotationValue.OfDouble.class::cast).mapToDouble(AnnotationValue.OfDouble::doubleValue).toArray();
|
||||||
|
}
|
||||||
|
case AnnotationValue.OfLong _ -> {
|
||||||
|
return stream.map(AnnotationValue.OfLong.class::cast).mapToLong(AnnotationValue.OfLong::longValue).toArray();
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
Object firstResolvedValue = readAnnotationValue(className, arrayValue.values().getFirst(), classLoader);
|
||||||
|
return stream
|
||||||
|
.map(rawValue -> readAnnotationValue(className, rawValue, classLoader))
|
||||||
|
.toArray(s -> (Object[]) Array.newInstance(firstResolvedValue.getClass(), s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static @Nullable <E extends Enum<E>> Enum<E> parseEnum(AnnotationValue.OfEnum enumValue, @Nullable ClassLoader classLoader) {
|
||||||
|
String enumClassName = fromTypeDescriptor(enumValue.className().stringValue());
|
||||||
|
try {
|
||||||
|
Class<E> enumClass = (Class<E>) ClassUtils.forName(enumClassName, classLoader);
|
||||||
|
return Enum.valueOf(enumClass, enumValue.constantName().stringValue());
|
||||||
|
}
|
||||||
|
catch (ClassNotFoundException | LinkageError ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record Source(Annotation entryName) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,305 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.core.type.classreading;
|
||||||
|
|
||||||
|
import java.lang.classfile.AccessFlags;
|
||||||
|
import java.lang.classfile.ClassModel;
|
||||||
|
import java.lang.classfile.Interfaces;
|
||||||
|
import java.lang.classfile.MethodModel;
|
||||||
|
import java.lang.classfile.Superclass;
|
||||||
|
import java.lang.classfile.attribute.InnerClassInfo;
|
||||||
|
import java.lang.classfile.attribute.InnerClassesAttribute;
|
||||||
|
import java.lang.classfile.attribute.NestHostAttribute;
|
||||||
|
import java.lang.classfile.attribute.RuntimeVisibleAnnotationsAttribute;
|
||||||
|
import java.lang.classfile.constantpool.ClassEntry;
|
||||||
|
import java.lang.reflect.AccessFlag;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
|
import org.springframework.core.annotation.MergedAnnotations;
|
||||||
|
import org.springframework.core.type.AnnotationMetadata;
|
||||||
|
import org.springframework.core.type.MethodMetadata;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link AnnotationMetadata} implementation that leverages
|
||||||
|
* the {@link java.lang.classfile.ClassFile} API.
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
class ClassFileClassMetadata implements AnnotationMetadata {
|
||||||
|
|
||||||
|
private final String className;
|
||||||
|
|
||||||
|
private final AccessFlags accessFlags;
|
||||||
|
|
||||||
|
private final @Nullable String enclosingClassName;
|
||||||
|
|
||||||
|
private final @Nullable String superClassName;
|
||||||
|
|
||||||
|
private final boolean independentInnerClass;
|
||||||
|
|
||||||
|
private final Set<String> interfaceNames;
|
||||||
|
|
||||||
|
private final Set<String> memberClassNames;
|
||||||
|
|
||||||
|
private final Set<MethodMetadata> declaredMethods;
|
||||||
|
|
||||||
|
private final MergedAnnotations mergedAnnotations;
|
||||||
|
|
||||||
|
private @Nullable 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
|
||||||
|
public @Nullable String getEnclosingClassName() {
|
||||||
|
return this.enclosingClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable String getSuperClassName() {
|
||||||
|
return this.superClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getInterfaceNames() {
|
||||||
|
return StringUtils.toStringArray(this.interfaceNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getMemberClassNames() {
|
||||||
|
return StringUtils.toStringArray(this.memberClassNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MergedAnnotations getAnnotations() {
|
||||||
|
return this.mergedAnnotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<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 _ -> {
|
||||||
|
builder.enclosingClass(classModel.thisClass());
|
||||||
|
}
|
||||||
|
case InnerClassesAttribute innerClasses -> {
|
||||||
|
builder.nestMembers(currentClassName, innerClasses);
|
||||||
|
}
|
||||||
|
case RuntimeVisibleAnnotationsAttribute annotationsAttribute -> {
|
||||||
|
builder.mergedAnnotations(ClassFileAnnotationMetadata.createMergedAnnotations(
|
||||||
|
ClassUtils.convertResourcePathToClassName(currentClassName), annotationsAttribute, classLoader));
|
||||||
|
}
|
||||||
|
case Superclass superclass -> {
|
||||||
|
builder.superClass(superclass);
|
||||||
|
}
|
||||||
|
case Interfaces interfaces -> {
|
||||||
|
builder.interfaces(interfaces);
|
||||||
|
}
|
||||||
|
case MethodModel method -> {
|
||||||
|
builder.method(method);
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
// ignore class element
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Builder {
|
||||||
|
|
||||||
|
private final ClassLoader clasLoader;
|
||||||
|
|
||||||
|
private String className;
|
||||||
|
|
||||||
|
private AccessFlags accessFlags;
|
||||||
|
|
||||||
|
private Set<AccessFlag> innerAccessFlags;
|
||||||
|
|
||||||
|
private @Nullable String enclosingClassName;
|
||||||
|
|
||||||
|
private @Nullable String superClassName;
|
||||||
|
|
||||||
|
private Set<String> interfaceNames = new LinkedHashSet<>(4);
|
||||||
|
|
||||||
|
private Set<String> memberClassNames = new LinkedHashSet<>(4);
|
||||||
|
|
||||||
|
private Set<MethodMetadata> declaredMethods = new LinkedHashSet<>(4);
|
||||||
|
|
||||||
|
private MergedAnnotations mergedAnnotations = MergedAnnotations.of(Collections.emptySet());
|
||||||
|
|
||||||
|
public Builder(ClassLoader classLoader) {
|
||||||
|
this.clasLoader = classLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
void classEntry(ClassEntry classEntry) {
|
||||||
|
this.className = ClassUtils.convertResourcePathToClassName(classEntry.name().stringValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
void accessFlags(AccessFlags accessFlags) {
|
||||||
|
this.accessFlags = accessFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
void enclosingClass(ClassEntry thisClass) {
|
||||||
|
String thisClassName = thisClass.name().stringValue();
|
||||||
|
int currentClassIndex = thisClassName.lastIndexOf('$');
|
||||||
|
this.enclosingClassName = ClassUtils.convertResourcePathToClassName(thisClassName.substring(0, currentClassIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
void superClass(Superclass superClass) {
|
||||||
|
this.superClassName = ClassUtils.convertResourcePathToClassName(superClass.superclassEntry().name().stringValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
void interfaces(Interfaces interfaces) {
|
||||||
|
for (ClassEntry entry : interfaces.interfaces()) {
|
||||||
|
this.interfaceNames.add(ClassUtils.convertResourcePathToClassName(entry.name().stringValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void nestMembers(String currentClassName, InnerClassesAttribute innerClasses) {
|
||||||
|
for (InnerClassInfo classInfo : innerClasses.classes()) {
|
||||||
|
String innerClassName = classInfo.innerClass().name().stringValue();
|
||||||
|
// skip parent inner classes
|
||||||
|
if (!innerClassName.startsWith(currentClassName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// the current class is an inner class
|
||||||
|
else if (currentClassName.equals(innerClassName)) {
|
||||||
|
this.innerAccessFlags = classInfo.flags();
|
||||||
|
}
|
||||||
|
// collecting data about actual inner classes
|
||||||
|
else {
|
||||||
|
this.memberClassNames.add(ClassUtils.convertResourcePathToClassName(innerClassName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void mergedAnnotations(MergedAnnotations mergedAnnotations) {
|
||||||
|
this.mergedAnnotations = mergedAnnotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
void method(MethodModel method) {
|
||||||
|
ClassFileMethodMetadata classFileMethodMetadata = ClassFileMethodMetadata.of(method, this.clasLoader);
|
||||||
|
if (!classFileMethodMetadata.isSynthetic() && !classFileMethodMetadata.isDefaultConstructor()) {
|
||||||
|
this.declaredMethods.add(classFileMethodMetadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ClassFileClassMetadata build() {
|
||||||
|
boolean independentInnerClass = (this.enclosingClassName != null) && this.innerAccessFlags.contains(AccessFlag.STATIC);
|
||||||
|
return new ClassFileClassMetadata(this.className, this.accessFlags, this.enclosingClassName, this.superClassName,
|
||||||
|
independentInnerClass, this.interfaceNames, this.memberClassNames, this.declaredMethods, this.mergedAnnotations);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.core.type.classreading;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.lang.classfile.ClassFile;
|
||||||
|
import java.lang.classfile.ClassModel;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.type.AnnotationMetadata;
|
||||||
|
import org.springframework.core.type.ClassMetadata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link MetadataReader} implementation based on the {@link ClassFile} API.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
final class ClassFileMetadataReader implements MetadataReader {
|
||||||
|
|
||||||
|
private final Resource resource;
|
||||||
|
|
||||||
|
private final AnnotationMetadata annotationMetadata;
|
||||||
|
|
||||||
|
|
||||||
|
ClassFileMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException {
|
||||||
|
this.resource = resource;
|
||||||
|
this.annotationMetadata = ClassFileClassMetadata.of(parseClassModel(resource), classLoader);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ClassModel parseClassModel(Resource resource) throws IOException {
|
||||||
|
try (InputStream is = resource.getInputStream()) {
|
||||||
|
byte[] bytes = is.readAllBytes();
|
||||||
|
return ClassFile.of().parse(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Resource getResource() {
|
||||||
|
return this.resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClassMetadata getClassMetadata() {
|
||||||
|
return this.annotationMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AnnotationMetadata getAnnotationMetadata() {
|
||||||
|
return this.annotationMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.core.type.classreading;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
|
import org.springframework.core.io.DefaultResourceLoader;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.ResourceLoader;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the {@link MetadataReaderFactory} interface,
|
||||||
|
* using the {@link java.lang.classfile.ClassFile} API for parsing the bytecode.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
public class ClassFileMetadataReaderFactory implements MetadataReaderFactory {
|
||||||
|
|
||||||
|
|
||||||
|
private final ResourceLoader resourceLoader;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new ClassFileMetadataReaderFactory for the default class loader.
|
||||||
|
*/
|
||||||
|
public ClassFileMetadataReaderFactory() {
|
||||||
|
this.resourceLoader = new DefaultResourceLoader();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new ClassFileMetadataReaderFactory for the given resource loader.
|
||||||
|
* @param resourceLoader the Spring ResourceLoader to use
|
||||||
|
* (also determines the ClassLoader to use)
|
||||||
|
*/
|
||||||
|
public ClassFileMetadataReaderFactory(@Nullable ResourceLoader resourceLoader) {
|
||||||
|
this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new ClassFileMetadataReaderFactory for the given class loader.
|
||||||
|
* @param classLoader the ClassLoader to use
|
||||||
|
*/
|
||||||
|
public ClassFileMetadataReaderFactory(@Nullable ClassLoader classLoader) {
|
||||||
|
this.resourceLoader =
|
||||||
|
(classLoader != null ? new DefaultResourceLoader(classLoader) : new DefaultResourceLoader());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the ResourceLoader that this MetadataReaderFactory has been
|
||||||
|
* constructed with.
|
||||||
|
*/
|
||||||
|
public final ResourceLoader getResourceLoader() {
|
||||||
|
return this.resourceLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MetadataReader getMetadataReader(String className) throws IOException {
|
||||||
|
try {
|
||||||
|
String resourcePath = ResourceLoader.CLASSPATH_URL_PREFIX +
|
||||||
|
ClassUtils.convertClassNameToResourcePath(className) + ClassUtils.CLASS_FILE_SUFFIX;
|
||||||
|
Resource resource = this.resourceLoader.getResource(resourcePath);
|
||||||
|
return getMetadataReader(resource);
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException ex) {
|
||||||
|
// Maybe an inner class name using the dot name syntax? Need to use the dollar syntax here...
|
||||||
|
// ClassUtils.forName has an equivalent check for resolution into Class references later on.
|
||||||
|
int lastDotIndex = className.lastIndexOf('.');
|
||||||
|
if (lastDotIndex != -1) {
|
||||||
|
String innerClassName =
|
||||||
|
className.substring(0, lastDotIndex) + '$' + className.substring(lastDotIndex + 1);
|
||||||
|
String innerClassResourcePath = ResourceLoader.CLASSPATH_URL_PREFIX +
|
||||||
|
ClassUtils.convertClassNameToResourcePath(innerClassName) + ClassUtils.CLASS_FILE_SUFFIX;
|
||||||
|
Resource innerClassResource = this.resourceLoader.getResource(innerClassResourcePath);
|
||||||
|
if (innerClassResource.exists()) {
|
||||||
|
return getMetadataReader(innerClassResource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MetadataReader getMetadataReader(Resource resource) throws IOException {
|
||||||
|
return new ClassFileMetadataReader(resource, this.resourceLoader.getClassLoader());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,182 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.core.type.classreading;
|
||||||
|
|
||||||
|
import java.lang.classfile.AccessFlags;
|
||||||
|
import java.lang.classfile.MethodModel;
|
||||||
|
import java.lang.classfile.attribute.RuntimeVisibleAnnotationsAttribute;
|
||||||
|
import java.lang.constant.ClassDesc;
|
||||||
|
import java.lang.constant.MethodTypeDesc;
|
||||||
|
import java.lang.reflect.AccessFlag;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
|
import org.springframework.core.annotation.MergedAnnotation;
|
||||||
|
import org.springframework.core.annotation.MergedAnnotations;
|
||||||
|
import org.springframework.core.type.MethodMetadata;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link MethodMetadata} extracted from class bytecode using the
|
||||||
|
* {@link java.lang.classfile.ClassFile} API.
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
class ClassFileMethodMetadata implements MethodMetadata {
|
||||||
|
|
||||||
|
private final String methodName;
|
||||||
|
|
||||||
|
private final AccessFlags accessFlags;
|
||||||
|
|
||||||
|
private final @Nullable String declaringClassName;
|
||||||
|
|
||||||
|
private final String returnTypeName;
|
||||||
|
|
||||||
|
// The source implements equals(), hashCode(), and toString() for the underlying method.
|
||||||
|
private final Object source;
|
||||||
|
|
||||||
|
private final MergedAnnotations annotations;
|
||||||
|
|
||||||
|
ClassFileMethodMetadata(String methodName, AccessFlags accessFlags, @Nullable String declaringClassName, String returnTypeName, Object source, MergedAnnotations annotations) {
|
||||||
|
this.methodName = methodName;
|
||||||
|
this.accessFlags = accessFlags;
|
||||||
|
this.declaringClassName = declaringClassName;
|
||||||
|
this.returnTypeName = returnTypeName;
|
||||||
|
this.source = source;
|
||||||
|
this.annotations = annotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMethodName() {
|
||||||
|
return this.methodName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable String getDeclaringClassName() {
|
||||||
|
return this.declaringClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getReturnTypeName() {
|
||||||
|
return this.returnTypeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAbstract() {
|
||||||
|
return this.accessFlags.has(AccessFlag.ABSTRACT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isStatic() {
|
||||||
|
return this.accessFlags.has(AccessFlag.STATIC);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isFinal() {
|
||||||
|
return this.accessFlags.has(AccessFlag.FINAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isOverridable() {
|
||||||
|
return !isStatic() && !isFinal() && !isPrivate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPrivate() {
|
||||||
|
return this.accessFlags.has(AccessFlag.PRIVATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSynthetic() {
|
||||||
|
return this.accessFlags.has(AccessFlag.SYNTHETIC);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDefaultConstructor() {
|
||||||
|
return this.methodName.equals("<init>");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MergedAnnotations getAnnotations() {
|
||||||
|
return this.annotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable Object other) {
|
||||||
|
return (this == other || (other instanceof ClassFileMethodMetadata that && this.source.equals(that.source)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return this.source.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.source.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static ClassFileMethodMetadata of(MethodModel methodModel, ClassLoader classLoader) {
|
||||||
|
String methodName = methodModel.methodName().stringValue();
|
||||||
|
AccessFlags flags = methodModel.flags();
|
||||||
|
String declaringClassName = methodModel.parent().map(parent -> ClassUtils.convertResourcePathToClassName(parent.thisClass().name().stringValue())).orElse(null);
|
||||||
|
ClassDesc returnType = methodModel.methodTypeSymbol().returnType();
|
||||||
|
String returnTypeName = returnType.packageName() + "." + returnType.displayName();
|
||||||
|
Source source = new Source(declaringClassName, flags, methodName, methodModel.methodTypeSymbol());
|
||||||
|
MergedAnnotations annotations = methodModel.elementStream()
|
||||||
|
.filter(element -> element instanceof RuntimeVisibleAnnotationsAttribute)
|
||||||
|
.findFirst()
|
||||||
|
.map(element -> ClassFileAnnotationMetadata.createMergedAnnotations(methodName, (RuntimeVisibleAnnotationsAttribute) element, classLoader))
|
||||||
|
.orElse(MergedAnnotations.of(Collections.emptyList()));
|
||||||
|
return new ClassFileMethodMetadata(methodName, flags, declaringClassName, returnTypeName, source, annotations);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link MergedAnnotation} source.
|
||||||
|
*
|
||||||
|
* @param declaringClassName the name of the declaring class
|
||||||
|
* @param flags the access flags
|
||||||
|
* @param methodName the name of the method
|
||||||
|
* @param descriptor the bytecode descriptor for this method
|
||||||
|
*/
|
||||||
|
record Source(@Nullable String declaringClassName, AccessFlags flags, String methodName, MethodTypeDesc descriptor) {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
this.flags.flags().forEach(flag -> {
|
||||||
|
builder.append(flag.name().toLowerCase(Locale.ROOT));
|
||||||
|
builder.append(' ');
|
||||||
|
});
|
||||||
|
builder.append(this.descriptor.returnType().packageName());
|
||||||
|
builder.append(".");
|
||||||
|
builder.append(this.descriptor.returnType().displayName());
|
||||||
|
builder.append(' ');
|
||||||
|
builder.append(this.declaringClassName);
|
||||||
|
builder.append('.');
|
||||||
|
builder.append(this.methodName);
|
||||||
|
builder.append('(');
|
||||||
|
builder.append(Stream.of(this.descriptor.parameterArray())
|
||||||
|
.map(desc -> desc.packageName() + "." + desc.displayName())
|
||||||
|
.collect(Collectors.joining(",")));
|
||||||
|
builder.append(')');
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.core.type.classreading;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
|
import org.springframework.core.io.ResourceLoader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal delegate for instantiating {@link MetadataReaderFactory} implementations.
|
||||||
|
* For JDK >= 24, the {@link ClassFileMetadataReaderFactory} is being used.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @see MetadataReaderFactory
|
||||||
|
*/
|
||||||
|
abstract class MetadataReaderFactoryDelegate {
|
||||||
|
|
||||||
|
static MetadataReaderFactory create(@Nullable ResourceLoader resourceLoader) {
|
||||||
|
return new ClassFileMetadataReaderFactory(resourceLoader);
|
||||||
|
}
|
||||||
|
|
||||||
|
static MetadataReaderFactory create(@Nullable ClassLoader classLoader) {
|
||||||
|
return new ClassFileMetadataReaderFactory(classLoader);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2024 the original author or authors.
|
* Copyright 2002-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -16,14 +16,17 @@
|
||||||
|
|
||||||
package org.springframework.core.type;
|
package org.springframework.core.type;
|
||||||
|
|
||||||
|
import java.lang.annotation.Repeatable;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import kotlin.Metadata;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import org.springframework.core.annotation.MergedAnnotation;
|
import org.springframework.core.annotation.MergedAnnotation;
|
||||||
import org.springframework.core.type.AbstractAnnotationMetadataTests.TestMemberClass.TestMemberClassInnerClass;
|
|
||||||
import org.springframework.core.type.AbstractAnnotationMetadataTests.TestMemberClass.TestMemberClassInnerInterface;
|
|
||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
@ -34,376 +37,493 @@ import static org.assertj.core.api.Assertions.entry;
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
* @author Sam Brannen
|
* @author Sam Brannen
|
||||||
|
* @author Brian Clozel
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractAnnotationMetadataTests {
|
public abstract class AbstractAnnotationMetadataTests {
|
||||||
|
|
||||||
@Test
|
|
||||||
void verifyEquals() {
|
|
||||||
AnnotationMetadata testClass1 = get(TestClass.class);
|
|
||||||
AnnotationMetadata testClass2 = get(TestClass.class);
|
|
||||||
AnnotationMetadata testMemberClass1 = get(TestMemberClass.class);
|
|
||||||
AnnotationMetadata testMemberClass2 = get(TestMemberClass.class);
|
|
||||||
|
|
||||||
assertThat(testClass1).isNotEqualTo(null);
|
|
||||||
|
|
||||||
assertThat(testClass1).isEqualTo(testClass1);
|
|
||||||
assertThat(testClass2).isEqualTo(testClass2);
|
|
||||||
assertThat(testClass1).isEqualTo(testClass2);
|
|
||||||
assertThat(testClass2).isEqualTo(testClass1);
|
|
||||||
|
|
||||||
assertThat(testMemberClass1).isEqualTo(testMemberClass1);
|
|
||||||
assertThat(testMemberClass2).isEqualTo(testMemberClass2);
|
|
||||||
assertThat(testMemberClass1).isEqualTo(testMemberClass2);
|
|
||||||
assertThat(testMemberClass2).isEqualTo(testMemberClass1);
|
|
||||||
|
|
||||||
assertThat(testClass1).isNotEqualTo(testMemberClass1);
|
|
||||||
assertThat(testMemberClass1).isNotEqualTo(testClass1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void verifyHashCode() {
|
|
||||||
AnnotationMetadata testClass1 = get(TestClass.class);
|
|
||||||
AnnotationMetadata testClass2 = get(TestClass.class);
|
|
||||||
AnnotationMetadata testMemberClass1 = get(TestMemberClass.class);
|
|
||||||
AnnotationMetadata testMemberClass2 = get(TestMemberClass.class);
|
|
||||||
|
|
||||||
assertThat(testClass1).hasSameHashCodeAs(testClass2);
|
|
||||||
assertThat(testMemberClass1).hasSameHashCodeAs(testMemberClass2);
|
|
||||||
|
|
||||||
assertThat(testClass1).doesNotHaveSameHashCodeAs(testMemberClass1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void verifyToString() {
|
|
||||||
assertThat(get(TestClass.class).toString()).isEqualTo(TestClass.class.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getClassNameReturnsClassName() {
|
|
||||||
assertThat(get(TestClass.class).getClassName()).isEqualTo(TestClass.class.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void isInterfaceWhenInterfaceReturnsTrue() {
|
|
||||||
assertThat(get(TestInterface.class).isInterface()).isTrue();
|
|
||||||
assertThat(get(TestAnnotation.class).isInterface()).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void isInterfaceWhenNotInterfaceReturnsFalse() {
|
|
||||||
assertThat(get(TestClass.class).isInterface()).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void isAnnotationWhenAnnotationReturnsTrue() {
|
|
||||||
assertThat(get(TestAnnotation.class).isAnnotation()).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void isAnnotationWhenNotAnnotationReturnsFalse() {
|
|
||||||
assertThat(get(TestClass.class).isAnnotation()).isFalse();
|
|
||||||
assertThat(get(TestInterface.class).isAnnotation()).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void isFinalWhenFinalReturnsTrue() {
|
|
||||||
assertThat(get(TestFinalClass.class).isFinal()).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void isFinalWhenNonFinalReturnsFalse() {
|
|
||||||
assertThat(get(TestClass.class).isFinal()).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void isIndependentWhenIndependentReturnsTrue() {
|
|
||||||
assertThat(get(AbstractAnnotationMetadataTests.class).isIndependent()).isTrue();
|
|
||||||
assertThat(get(TestClass.class).isIndependent()).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void isIndependentWhenNotIndependentReturnsFalse() {
|
|
||||||
assertThat(get(TestNonStaticInnerClass.class).isIndependent()).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getEnclosingClassNameWhenHasEnclosingClassReturnsEnclosingClass() {
|
|
||||||
assertThat(get(TestClass.class).getEnclosingClassName()).isEqualTo(
|
|
||||||
AbstractAnnotationMetadataTests.class.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getEnclosingClassNameWhenHasNoEnclosingClassReturnsNull() {
|
|
||||||
assertThat(get(AbstractAnnotationMetadataTests.class).getEnclosingClassName()).isNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getSuperClassNameWhenHasSuperClassReturnsName() {
|
|
||||||
assertThat(get(TestSubclass.class).getSuperClassName()).isEqualTo(TestClass.class.getName());
|
|
||||||
assertThat(get(TestClass.class).getSuperClassName()).isEqualTo(Object.class.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getSuperClassNameWhenHasNoSuperClassReturnsNull() {
|
|
||||||
assertThat(get(Object.class).getSuperClassName()).isNull();
|
|
||||||
assertThat(get(TestInterface.class).getSuperClassName()).isNull();
|
|
||||||
assertThat(get(TestSubInterface.class).getSuperClassName()).isNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getInterfaceNamesWhenHasInterfacesReturnsNames() {
|
|
||||||
assertThat(get(TestSubclass.class).getInterfaceNames()).containsExactlyInAnyOrder(TestInterface.class.getName());
|
|
||||||
assertThat(get(TestSubInterface.class).getInterfaceNames()).containsExactlyInAnyOrder(TestInterface.class.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getInterfaceNamesWhenHasNoInterfacesReturnsEmptyArray() {
|
|
||||||
assertThat(get(TestClass.class).getInterfaceNames()).isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getMemberClassNamesWhenHasMemberClassesReturnsNames() {
|
|
||||||
assertThat(get(TestMemberClass.class).getMemberClassNames()).containsExactlyInAnyOrder(
|
|
||||||
TestMemberClassInnerClass.class.getName(), TestMemberClassInnerInterface.class.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getMemberClassNamesWhenHasNoMemberClassesReturnsEmptyArray() {
|
|
||||||
assertThat(get(TestClass.class).getMemberClassNames()).isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getAnnotationsReturnsDirectAnnotations() {
|
|
||||||
assertThat(get(WithDirectAnnotations.class).getAnnotations().stream())
|
|
||||||
.filteredOn(MergedAnnotation::isDirectlyPresent)
|
|
||||||
.extracting(a -> a.getType().getName())
|
|
||||||
.containsExactlyInAnyOrder(DirectAnnotation1.class.getName(), DirectAnnotation2.class.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void isAnnotatedWhenMatchesDirectAnnotationReturnsTrue() {
|
|
||||||
assertThat(get(WithDirectAnnotations.class).isAnnotated(DirectAnnotation1.class.getName())).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void isAnnotatedWhenMatchesMetaAnnotationReturnsTrue() {
|
|
||||||
assertThat(get(WithMetaAnnotations.class).isAnnotated(MetaAnnotation2.class.getName())).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void isAnnotatedWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() {
|
|
||||||
assertThat(get(TestClass.class).isAnnotated(DirectAnnotation1.class.getName())).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getAnnotationAttributesReturnsAttributes() {
|
|
||||||
assertThat(get(WithAnnotationAttributes.class).getAnnotationAttributes(AnnotationAttributes.class.getName()))
|
|
||||||
.containsOnly(entry("name", "test"), entry("size", 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getAllAnnotationAttributesReturnsAllAttributes() {
|
|
||||||
MultiValueMap<String, Object> attributes =
|
|
||||||
get(WithMetaAnnotationAttributes.class).getAllAnnotationAttributes(AnnotationAttributes.class.getName());
|
|
||||||
assertThat(attributes).containsOnlyKeys("name", "size");
|
|
||||||
assertThat(attributes.get("name")).containsExactlyInAnyOrder("m1", "m2");
|
|
||||||
assertThat(attributes.get("size")).containsExactlyInAnyOrder(1, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getAnnotationTypesReturnsDirectAnnotations() {
|
|
||||||
AnnotationMetadata metadata = get(WithDirectAnnotations.class);
|
|
||||||
assertThat(metadata.getAnnotationTypes()).containsExactlyInAnyOrder(
|
|
||||||
DirectAnnotation1.class.getName(), DirectAnnotation2.class.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getMetaAnnotationTypesReturnsMetaAnnotations() {
|
|
||||||
AnnotationMetadata metadata = get(WithMetaAnnotations.class);
|
|
||||||
assertThat(metadata.getMetaAnnotationTypes(MetaAnnotationRoot.class.getName()))
|
|
||||||
.containsExactlyInAnyOrder(MetaAnnotation1.class.getName(), MetaAnnotation2.class.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void hasAnnotationWhenMatchesDirectAnnotationReturnsTrue() {
|
|
||||||
assertThat(get(WithDirectAnnotations.class).hasAnnotation(DirectAnnotation1.class.getName())).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void hasAnnotationWhenMatchesMetaAnnotationReturnsFalse() {
|
|
||||||
assertThat(get(WithMetaAnnotations.class).hasAnnotation(MetaAnnotation1.class.getName())).isFalse();
|
|
||||||
assertThat(get(WithMetaAnnotations.class).hasAnnotation(MetaAnnotation2.class.getName())).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void hasAnnotationWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() {
|
|
||||||
assertThat(get(TestClass.class).hasAnnotation(DirectAnnotation1.class.getName())).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void hasMetaAnnotationWhenMatchesDirectReturnsFalse() {
|
|
||||||
assertThat(get(WithDirectAnnotations.class).hasMetaAnnotation(DirectAnnotation1.class.getName())).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void hasMetaAnnotationWhenMatchesMetaAnnotationReturnsTrue() {
|
|
||||||
assertThat(get(WithMetaAnnotations.class).hasMetaAnnotation(MetaAnnotation1.class.getName())).isTrue();
|
|
||||||
assertThat(get(WithMetaAnnotations.class).hasMetaAnnotation(MetaAnnotation2.class.getName())).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void hasMetaAnnotationWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() {
|
|
||||||
assertThat(get(TestClass.class).hasMetaAnnotation(MetaAnnotation1.class.getName())).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void hasAnnotatedMethodsWhenMatchesDirectAnnotationReturnsTrue() {
|
|
||||||
assertThat(get(WithAnnotatedMethod.class).hasAnnotatedMethods(DirectAnnotation1.class.getName())).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void hasAnnotatedMethodsWhenMatchesMetaAnnotationReturnsTrue() {
|
|
||||||
assertThat(get(WithMetaAnnotatedMethod.class).hasAnnotatedMethods(MetaAnnotation2.class.getName())).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void hasAnnotatedMethodsWhenDoesNotMatchAnyAnnotationReturnsFalse() {
|
|
||||||
assertThat(get(WithAnnotatedMethod.class).hasAnnotatedMethods(MetaAnnotation2.class.getName())).isFalse();
|
|
||||||
assertThat(get(WithNonAnnotatedMethod.class).hasAnnotatedMethods(DirectAnnotation1.class.getName())).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getAnnotatedMethodsReturnsMatchingAnnotatedAndMetaAnnotatedMethods() {
|
|
||||||
assertThat(get(WithDirectAndMetaAnnotatedMethods.class).getAnnotatedMethods(MetaAnnotation2.class.getName()))
|
|
||||||
.extracting(MethodMetadata::getMethodName)
|
|
||||||
.containsExactlyInAnyOrder("direct", "meta");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract AnnotationMetadata get(Class<?> source);
|
protected abstract AnnotationMetadata get(Class<?> source);
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class TypeTests {
|
||||||
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Test
|
||||||
public @interface DirectAnnotation1 {
|
void classEquals() {
|
||||||
}
|
AnnotationMetadata testClass1 = get(TestClass.class);
|
||||||
|
AnnotationMetadata testClass2 = get(TestClass.class);
|
||||||
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
assertThat(testClass1).isEqualTo(testClass1);
|
||||||
public @interface DirectAnnotation2 {
|
assertThat(testClass2).isEqualTo(testClass2);
|
||||||
}
|
assertThat(testClass1).isEqualTo(testClass2);
|
||||||
|
assertThat(testClass2).isEqualTo(testClass1);
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
@MetaAnnotation1
|
|
||||||
public @interface MetaAnnotationRoot {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
@MetaAnnotation2
|
|
||||||
public @interface MetaAnnotation1 {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
public @interface MetaAnnotation2 {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class TestClass {
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface TestInterface {
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface TestSubInterface extends TestInterface {
|
|
||||||
}
|
|
||||||
|
|
||||||
public @interface TestAnnotation {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class TestFinalClass {
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TestNonStaticInnerClass {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class TestSubclass extends TestClass implements TestInterface {
|
|
||||||
}
|
|
||||||
|
|
||||||
@DirectAnnotation1
|
|
||||||
@DirectAnnotation2
|
|
||||||
public static class WithDirectAnnotations {
|
|
||||||
}
|
|
||||||
|
|
||||||
@MetaAnnotationRoot
|
|
||||||
public static class WithMetaAnnotations {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class TestMemberClass {
|
|
||||||
|
|
||||||
public static class TestMemberClassInnerClass {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TestMemberClassInnerInterface {
|
@Test
|
||||||
|
void memberClassEquals() {
|
||||||
|
AnnotationMetadata testMemberClass1 = get(TestMemberClass.class);
|
||||||
|
AnnotationMetadata testMemberClass2 = get(TestMemberClass.class);
|
||||||
|
|
||||||
|
assertThat(testMemberClass1).isEqualTo(testMemberClass1);
|
||||||
|
assertThat(testMemberClass2).isEqualTo(testMemberClass2);
|
||||||
|
assertThat(testMemberClass1).isEqualTo(testMemberClass2);
|
||||||
|
assertThat(testMemberClass2).isEqualTo(testMemberClass1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void classHashCode() {
|
||||||
|
AnnotationMetadata testClass1 = get(TestClass.class);
|
||||||
|
AnnotationMetadata testClass2 = get(TestClass.class);
|
||||||
|
|
||||||
|
assertThat(testClass1).hasSameHashCodeAs(testClass2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void memberClassHashCode() {
|
||||||
|
AnnotationMetadata testMemberClass1 = get(TestMemberClass.class);
|
||||||
|
AnnotationMetadata testMemberClass2 = get(TestMemberClass.class);
|
||||||
|
|
||||||
|
assertThat(testMemberClass1).hasSameHashCodeAs(testMemberClass2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void classToString() {
|
||||||
|
assertThat(get(TestClass.class).toString()).isEqualTo(TestClass.class.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getClassNameReturnsClassName() {
|
||||||
|
assertThat(get(TestClass.class).getClassName()).isEqualTo(TestClass.class.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isInterfaceWhenInterfaceReturnsTrue() {
|
||||||
|
assertThat(get(TestInterface.class).isInterface()).isTrue();
|
||||||
|
assertThat(get(TestAnnotation.class).isInterface()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isInterfaceWhenNotInterfaceReturnsFalse() {
|
||||||
|
assertThat(get(TestClass.class).isInterface()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isAnnotationWhenAnnotationReturnsTrue() {
|
||||||
|
assertThat(get(TestAnnotation.class).isAnnotation()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isAnnotationWhenNotAnnotationReturnsFalse() {
|
||||||
|
assertThat(get(TestClass.class).isAnnotation()).isFalse();
|
||||||
|
assertThat(get(TestInterface.class).isAnnotation()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isFinalWhenFinalReturnsTrue() {
|
||||||
|
assertThat(get(TestFinalClass.class).isFinal()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isFinalWhenNonFinalReturnsFalse() {
|
||||||
|
assertThat(get(TestClass.class).isFinal()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isIndependentWhenIndependentReturnsTrue() {
|
||||||
|
assertThat(get(AbstractAnnotationMetadataTests.class).isIndependent()).isTrue();
|
||||||
|
assertThat(get(TestClass.class).isIndependent()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isIndependentWhenNotIndependentReturnsFalse() {
|
||||||
|
assertThat(get(TestNonStaticInnerClass.class).isIndependent()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getEnclosingClassNameWhenHasEnclosingClassReturnsEnclosingClass() {
|
||||||
|
assertThat(get(TestClass.class).getEnclosingClassName()).isEqualTo(
|
||||||
|
AbstractAnnotationMetadataTests.TypeTests.class.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getEnclosingClassNameWhenHasNoEnclosingClassReturnsNull() {
|
||||||
|
assertThat(get(AbstractAnnotationMetadataTests.class).getEnclosingClassName()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getSuperClassNameWhenHasSuperClassReturnsName() {
|
||||||
|
assertThat(get(TestSubclass.class).getSuperClassName()).isEqualTo(TestClass.class.getName());
|
||||||
|
assertThat(get(TestClass.class).getSuperClassName()).isEqualTo(Object.class.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getSuperClassNameWhenHasNoSuperClassReturnsNull() {
|
||||||
|
assertThat(get(Object.class).getSuperClassName()).isNull();
|
||||||
|
assertThat(get(TestInterface.class).getSuperClassName()).isIn(null, "java.lang.Object");
|
||||||
|
assertThat(get(TestSubInterface.class).getSuperClassName()).isIn(null, "java.lang.Object");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getInterfaceNamesWhenHasInterfacesReturnsNames() {
|
||||||
|
assertThat(get(TestSubclass.class).getInterfaceNames()).containsExactly(TestInterface.class.getName());
|
||||||
|
assertThat(get(TestSubInterface.class).getInterfaceNames()).containsExactly(TestInterface.class.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getInterfaceNamesWhenHasNoInterfacesReturnsEmptyArray() {
|
||||||
|
assertThat(get(TestClass.class).getInterfaceNames()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getMemberClassNamesWhenHasMemberClassesReturnsNames() {
|
||||||
|
assertThat(get(TestMemberClass.class).getMemberClassNames()).containsExactlyInAnyOrder(
|
||||||
|
TestMemberClass.TestMemberClassInnerClass.class.getName(), TestMemberClass.TestMemberClassInnerInterface.class.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getMemberClassNamesWhenHasNoMemberClassesReturnsEmptyArray() {
|
||||||
|
assertThat(get(TestClass.class).getMemberClassNames()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TestClass {
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface TestInterface {
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface TestSubInterface extends TestInterface {
|
||||||
|
}
|
||||||
|
|
||||||
|
public @interface TestAnnotation {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class TestFinalClass {
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestNonStaticInnerClass {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TestSubclass extends TestClass implements TestInterface {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TestMemberClass {
|
||||||
|
|
||||||
|
public static class TestMemberClassInnerClass {
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TestMemberClassInnerInterface {
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class WithAnnotatedMethod {
|
@Nested
|
||||||
|
class AnnotationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getAnnotationsReturnsDirectAnnotations() {
|
||||||
|
assertThat(get(WithDirectAnnotations.class).getAnnotations().stream())
|
||||||
|
.filteredOn(MergedAnnotation::isDirectlyPresent)
|
||||||
|
.extracting(a -> a.getType().getName())
|
||||||
|
.containsExactlyInAnyOrder(DirectAnnotation1.class.getName(), DirectAnnotation2.class.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isAnnotatedWhenMatchesDirectAnnotationReturnsTrue() {
|
||||||
|
assertThat(get(WithDirectAnnotations.class).isAnnotated(DirectAnnotation1.class.getName())).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isAnnotatedWhenMatchesMetaAnnotationReturnsTrue() {
|
||||||
|
assertThat(get(WithMetaAnnotations.class).isAnnotated(MetaAnnotation2.class.getName())).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isAnnotatedWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() {
|
||||||
|
assertThat(get(NoAnnotationClass.class).isAnnotated(DirectAnnotation1.class.getName())).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getAnnotationAttributesReturnsAttributes() {
|
||||||
|
assertThat(get(WithAnnotationAttributes.class).getAnnotationAttributes(AnnotationAttributes.class.getName()))
|
||||||
|
.containsOnly(entry("name", "test"), entry("size", 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getAllAnnotationAttributesReturnsAllAttributes() {
|
||||||
|
MultiValueMap<String, Object> attributes =
|
||||||
|
get(WithMetaAnnotationAttributes.class).getAllAnnotationAttributes(AnnotationAttributes.class.getName());
|
||||||
|
assertThat(attributes).containsOnlyKeys("name", "size");
|
||||||
|
assertThat(attributes.get("name")).containsExactlyInAnyOrder("m1", "m2");
|
||||||
|
assertThat(attributes.get("size")).containsExactlyInAnyOrder(1, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getComplexAttributeTypesReturnsAll() {
|
||||||
|
MultiValueMap<String, Object> attributes =
|
||||||
|
get(WithComplexAttributeTypes.class).getAllAnnotationAttributes(ComplexAttributes.class.getName());
|
||||||
|
assertThat(attributes).containsOnlyKeys("names", "count", "type", "subAnnotation");
|
||||||
|
assertThat(attributes.get("names")).hasSize(1);
|
||||||
|
assertThat(attributes.get("names").get(0)).isEqualTo(new String[]{"first", "second"});
|
||||||
|
assertThat(attributes.get("count")).containsExactlyInAnyOrder(TestEnum.ONE);
|
||||||
|
assertThat(attributes.get("type")).containsExactlyInAnyOrder(TestEnum.class);
|
||||||
|
assertThat(attributes.get("subAnnotation")).hasSize(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getComplexAttributeTypesReturnsAllWithKotlinMetadata() {
|
||||||
|
MultiValueMap<String, Object> attributes =
|
||||||
|
get(WithComplexAttributeTypes.class).getAllAnnotationAttributes(Metadata.class.getName());
|
||||||
|
assertThat(attributes).containsKeys("k", "mv");
|
||||||
|
int[] values = {42};
|
||||||
|
assertThat(attributes.get("mv")).hasSize(1);
|
||||||
|
assertThat(attributes.get("mv").get(0)).isEqualTo(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getRepeatableReturnsAttributes() {
|
||||||
|
MultiValueMap<String, Object> attributes =
|
||||||
|
get(WithRepeatableAnnotations.class).getAllAnnotationAttributes(RepeatableAnnotations.class.getName());
|
||||||
|
assertThat(attributes).containsKeys("value");
|
||||||
|
assertThat(attributes.get("value")).hasSize(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getAnnotationTypesReturnsDirectAnnotations() {
|
||||||
|
AnnotationMetadata metadata = get(WithDirectAnnotations.class);
|
||||||
|
assertThat(metadata.getAnnotationTypes()).containsExactlyInAnyOrder(
|
||||||
|
DirectAnnotation1.class.getName(), DirectAnnotation2.class.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getMetaAnnotationTypesReturnsMetaAnnotations() {
|
||||||
|
AnnotationMetadata metadata = get(WithMetaAnnotations.class);
|
||||||
|
assertThat(metadata.getMetaAnnotationTypes(MetaAnnotationRoot.class.getName()))
|
||||||
|
.containsExactlyInAnyOrder(MetaAnnotation1.class.getName(), MetaAnnotation2.class.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hasAnnotationWhenMatchesDirectAnnotationReturnsTrue() {
|
||||||
|
assertThat(get(WithDirectAnnotations.class).hasAnnotation(DirectAnnotation1.class.getName())).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hasAnnotationWhenMatchesMetaAnnotationReturnsFalse() {
|
||||||
|
assertThat(get(WithMetaAnnotations.class).hasAnnotation(MetaAnnotation1.class.getName())).isFalse();
|
||||||
|
assertThat(get(WithMetaAnnotations.class).hasAnnotation(MetaAnnotation2.class.getName())).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hasAnnotationWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() {
|
||||||
|
assertThat(get(NoAnnotationClass.class).hasAnnotation(DirectAnnotation1.class.getName())).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hasMetaAnnotationWhenMatchesDirectReturnsFalse() {
|
||||||
|
assertThat(get(WithDirectAnnotations.class).hasMetaAnnotation(DirectAnnotation1.class.getName())).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hasMetaAnnotationWhenMatchesMetaAnnotationReturnsTrue() {
|
||||||
|
assertThat(get(WithMetaAnnotations.class).hasMetaAnnotation(MetaAnnotation1.class.getName())).isTrue();
|
||||||
|
assertThat(get(WithMetaAnnotations.class).hasMetaAnnotation(MetaAnnotation2.class.getName())).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hasMetaAnnotationWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() {
|
||||||
|
assertThat(get(NoAnnotationClass.class).hasMetaAnnotation(MetaAnnotation1.class.getName())).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hasAnnotatedMethodsWhenMatchesDirectAnnotationReturnsTrue() {
|
||||||
|
assertThat(get(WithAnnotatedMethod.class).hasAnnotatedMethods(DirectAnnotation1.class.getName())).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hasAnnotatedMethodsWhenMatchesMetaAnnotationReturnsTrue() {
|
||||||
|
assertThat(get(WithMetaAnnotatedMethod.class).hasAnnotatedMethods(MetaAnnotation2.class.getName())).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hasAnnotatedMethodsWhenDoesNotMatchAnyAnnotationReturnsFalse() {
|
||||||
|
assertThat(get(WithAnnotatedMethod.class).hasAnnotatedMethods(MetaAnnotation2.class.getName())).isFalse();
|
||||||
|
assertThat(get(WithNonAnnotatedMethod.class).hasAnnotatedMethods(DirectAnnotation1.class.getName())).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getAnnotatedMethodsReturnsMatchingAnnotatedAndMetaAnnotatedMethods() {
|
||||||
|
assertThat(get(WithDirectAndMetaAnnotatedMethods.class).getAnnotatedMethods(MetaAnnotation2.class.getName()))
|
||||||
|
.extracting(MethodMetadata::getMethodName)
|
||||||
|
.containsExactlyInAnyOrder("direct", "meta");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class WithAnnotatedMethod {
|
||||||
|
|
||||||
|
@DirectAnnotation1
|
||||||
|
public void test() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class WithMetaAnnotatedMethod {
|
||||||
|
|
||||||
|
@MetaAnnotationRoot
|
||||||
|
public void test() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class WithNonAnnotatedMethod {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class WithDirectAndMetaAnnotatedMethods {
|
||||||
|
|
||||||
|
@MetaAnnotation2
|
||||||
|
public void direct() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@MetaAnnotationRoot
|
||||||
|
public void meta() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@AnnotationAttributes(name = "test", size = 1)
|
||||||
|
public static class WithAnnotationAttributes {
|
||||||
|
}
|
||||||
|
|
||||||
|
@MetaAnnotationAttributes1
|
||||||
|
@MetaAnnotationAttributes2
|
||||||
|
public static class WithMetaAnnotationAttributes {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@AnnotationAttributes(name = "m1", size = 1)
|
||||||
|
public @interface MetaAnnotationAttributes1 {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@AnnotationAttributes(name = "m2", size = 2)
|
||||||
|
public @interface MetaAnnotationAttributes2 {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface AnnotationAttributes {
|
||||||
|
|
||||||
|
String name();
|
||||||
|
|
||||||
|
int size();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@ComplexAttributes(names = {"first", "second"}, count = TestEnum.ONE,
|
||||||
|
type = TestEnum.class, subAnnotation = @SubAnnotation(name="spring"))
|
||||||
|
@Metadata(mv = {42})
|
||||||
|
public static class WithComplexAttributeTypes {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface ComplexAttributes {
|
||||||
|
|
||||||
|
String[] names();
|
||||||
|
|
||||||
|
TestEnum count();
|
||||||
|
|
||||||
|
Class<?> type();
|
||||||
|
|
||||||
|
SubAnnotation subAnnotation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public @interface SubAnnotation {
|
||||||
|
|
||||||
|
String name();
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum TestEnum {
|
||||||
|
ONE, TWO, THREE
|
||||||
|
}
|
||||||
|
|
||||||
|
@RepeatableAnnotation(name = "first")
|
||||||
|
@RepeatableAnnotation(name = "second")
|
||||||
|
public static class WithRepeatableAnnotations {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Repeatable(RepeatableAnnotations.class)
|
||||||
|
public @interface RepeatableAnnotation {
|
||||||
|
|
||||||
|
String name();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface RepeatableAnnotations {
|
||||||
|
|
||||||
|
RepeatableAnnotation[] value();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface DirectAnnotation1 {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface DirectAnnotation2 {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@MetaAnnotation1
|
||||||
|
public @interface MetaAnnotationRoot {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@MetaAnnotation2
|
||||||
|
public @interface MetaAnnotation1 {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface MetaAnnotation2 {
|
||||||
|
}
|
||||||
|
|
||||||
@DirectAnnotation1
|
@DirectAnnotation1
|
||||||
public void test() {
|
@DirectAnnotation2
|
||||||
}
|
public static class WithDirectAnnotations {
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class WithMetaAnnotatedMethod {
|
|
||||||
|
|
||||||
@MetaAnnotationRoot
|
|
||||||
public void test() {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class WithNonAnnotatedMethod {
|
|
||||||
|
|
||||||
public void test() {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class WithDirectAndMetaAnnotatedMethods {
|
|
||||||
|
|
||||||
@MetaAnnotation2
|
|
||||||
public void direct() {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@MetaAnnotationRoot
|
@MetaAnnotationRoot
|
||||||
public void meta() {
|
public static class WithMetaAnnotations {
|
||||||
|
}
|
||||||
|
|
||||||
|
static class NoAnnotationClass {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@AnnotationAttributes(name = "test", size = 1)
|
@Nested
|
||||||
public static class WithAnnotationAttributes {
|
class MethodTests {
|
||||||
}
|
|
||||||
|
|
||||||
@MetaAnnotationAttributes1
|
@Test
|
||||||
@MetaAnnotationAttributes2
|
void declaredMethodsToString() {
|
||||||
public static class WithMetaAnnotationAttributes {
|
List<String> methods = get(TestMethods.class).getDeclaredMethods().stream().map(Object::toString).toList();
|
||||||
}
|
List<String> expected = Arrays.stream(TestMethods.class.getDeclaredMethods()).map(Object::toString).toList();
|
||||||
|
assertThat(methods).containsExactlyInAnyOrderElementsOf(expected);
|
||||||
|
}
|
||||||
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
static class TestMethods {
|
||||||
@AnnotationAttributes(name = "m1", size = 1)
|
public String test1(String argument) {
|
||||||
public @interface MetaAnnotationAttributes1 {
|
return "test";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
public String test2(String argument) {
|
||||||
@AnnotationAttributes(name = "m2", size = 2)
|
return "test";
|
||||||
public @interface MetaAnnotationAttributes2 {
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
public String test3(String argument) {
|
||||||
public @interface AnnotationAttributes {
|
return "test";
|
||||||
|
}
|
||||||
String name();
|
}
|
||||||
|
|
||||||
int size();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2019 the original author or authors.
|
* Copyright 2002-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -21,7 +21,8 @@ import org.springframework.core.type.AnnotationMetadata;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link SimpleAnnotationMetadata} and
|
* Tests for {@link SimpleAnnotationMetadata} and
|
||||||
* {@link SimpleAnnotationMetadataReadingVisitor}.
|
* {@link SimpleAnnotationMetadataReadingVisitor} on Java < 24,
|
||||||
|
* and for the ClassFile API variant on Java >= 24.
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
*/
|
*/
|
||||||
|
@ -30,9 +31,8 @@ class SimpleAnnotationMetadataTests extends AbstractAnnotationMetadataTests {
|
||||||
@Override
|
@Override
|
||||||
protected AnnotationMetadata get(Class<?> source) {
|
protected AnnotationMetadata get(Class<?> source) {
|
||||||
try {
|
try {
|
||||||
return new SimpleMetadataReaderFactory(
|
return MetadataReaderFactory.create(source.getClassLoader())
|
||||||
source.getClassLoader()).getMetadataReader(
|
.getMetadataReader(source.getName()).getAnnotationMetadata();
|
||||||
source.getName()).getAnnotationMetadata();
|
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
throw new IllegalStateException(ex);
|
throw new IllegalStateException(ex);
|
||||||
|
|
Loading…
Reference in New Issue