Align with JDK by throwing TypeNotPresentException in MergedAnnotations

Prior to this commit, we used ClassUtils.resolveClassName() in
TypeMappedAnnotation.adapt(...) which throws an IllegalStateException
or IllegalArgumentException if a type referenced by an annotation
attribute cannot be loaded. However, if such an error occurs while
using the JDK's reflection APIs, a TypeNotPresentException is thrown
instead.

In order to align with the standard behavior of the JDK, this commit
modifies TypeMappedAnnotation.adapt(...) to use ClassUtils.forName()
and throw a TypeNotPresentException in such scenarios.

This commit also makes similar changes in
MergedAnnotationReadingVisitor and ClassFileAnnotationDelegate.

Closes gh-36593
This commit is contained in:
Sam Brannen
2026-04-03 16:16:43 +02:00
parent 822001c6a4
commit f993e9710d
6 changed files with 59 additions and 26 deletions
@@ -450,7 +450,12 @@ final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnn
value = clazz.getName();
}
else if (value instanceof String str && type == Class.class) {
value = ClassUtils.resolveClassName(str, getClassLoader());
try {
value = ClassUtils.forName(str, getClassLoader());
}
catch (ClassNotFoundException | LinkageError ex) {
throw new TypeNotPresentException(str, ex);
}
}
else if (value instanceof Class<?>[] classes && type == String[].class) {
String[] names = new String[classes.length];
@@ -461,8 +466,14 @@ final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnn
}
else if (value instanceof String[] names && type == Class[].class) {
Class<?>[] classes = new Class<?>[names.length];
ClassLoader classLoader = getClassLoader();
for (int i = 0; i < names.length; i++) {
classes[i] = ClassUtils.resolveClassName(names[i], getClassLoader());
try {
classes[i] = ClassUtils.forName(names[i], classLoader);
}
catch (ClassNotFoundException | LinkageError ex) {
throw new TypeNotPresentException(names[i], ex);
}
}
value = classes;
}
@@ -101,7 +101,13 @@ class MergedAnnotationReadingVisitor<A extends Annotation> extends AnnotationVis
@SuppressWarnings("unchecked")
public <E extends Enum<E>> void visitEnum(String descriptor, String value, Consumer<E> consumer) {
String className = Type.getType(descriptor).getClassName();
Class<E> type = (Class<E>) ClassUtils.resolveClassName(className, this.classLoader);
Class<E> type = null;
try {
type = (Class<E>) ClassUtils.forName(className, this.classLoader);
}
catch (ClassNotFoundException | LinkageError ex) {
throw new TypeNotPresentException(className, ex);
}
consumer.accept(Enum.valueOf(type, value));
}
@@ -113,7 +119,13 @@ class MergedAnnotationReadingVisitor<A extends Annotation> extends AnnotationVis
if (AnnotationFilter.PLAIN.matches(className)) {
return null;
}
Class<T> type = (Class<T>) ClassUtils.resolveClassName(className, this.classLoader);
Class<T> type = null;
try {
type = (Class<T>) ClassUtils.forName(className, this.classLoader);
}
catch (ClassNotFoundException | LinkageError ex) {
throw new TypeNotPresentException(className, ex);
}
return new MergedAnnotationReadingVisitor<>(this.classLoader, this.source, type, consumer);
}
@@ -139,7 +139,12 @@ abstract class ClassFileAnnotationDelegate {
private static Class<?> loadEnumClass(AnnotationValue.OfEnum enumValue, @Nullable ClassLoader classLoader) {
String className = ClassFileAnnotationMetadata.resolveTypeName(enumValue.classSymbol());
return ClassUtils.resolveClassName(className, classLoader);
try {
return ClassUtils.forName(className, classLoader);
}
catch (ClassNotFoundException | LinkageError ex) {
throw new TypeNotPresentException(className, ex);
}
}
private static Class<?> resolveArrayElementType(List<AnnotationValue> values, @Nullable ClassLoader classLoader) {
@@ -31,6 +31,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.core.annotation.MergedAnnotation.Adapt;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.InstanceOfAssertFactories.throwable;
@@ -143,16 +144,16 @@ class TypeMappedAnnotationTests {
Map.of(attributeName, "com.example.DoesNotExist"));
// 0) Sanity check MergedAnnotation.getClass() behavior.
assertThatIllegalArgumentException()
assertThatExceptionOfType(TypeNotPresentException.class)
.isThrownBy(() -> mergedAnnotation.getClass(attributeName))
.withMessage("Could not find class [com.example.DoesNotExist]")
.withCauseInstanceOf(ClassNotFoundException.class);
.withMessageContaining("com.example.DoesNotExist")
.withCauseExactlyInstanceOf(ClassNotFoundException.class);
var map = mergedAnnotation.asMap(Adapt.values(false, true));
// 1) Attribute should be present, even though its value is an exception.
assertThat(map).containsKey(attributeName);
assertThat(map.get(attributeName)).asInstanceOf(throwable(IllegalArgumentException.class))
assertThat(map.get(attributeName)).asInstanceOf(throwable(TypeNotPresentException.class))
.hasMessageContaining("com.example.DoesNotExist");
}
@@ -163,16 +164,16 @@ class TypeMappedAnnotationTests {
Map.of(attributeName, new String[] { "com.example.DoesNotExist" }));
// 0) Sanity check MergedAnnotation.getClassArray() behavior.
assertThatIllegalArgumentException()
assertThatExceptionOfType(TypeNotPresentException.class)
.isThrownBy(() -> mergedAnnotation.getClassArray(attributeName))
.withMessage("Could not find class [com.example.DoesNotExist]")
.withMessageContaining("com.example.DoesNotExist")
.withCauseExactlyInstanceOf(ClassNotFoundException.class);
var map = mergedAnnotation.asMap(Adapt.values(false, true));
// 1) Attribute should be present, even though its value is an exception.
assertThat(map).containsKey(attributeName);
assertThat(map.get(attributeName)).asInstanceOf(throwable(IllegalArgumentException.class))
assertThat(map.get(attributeName)).asInstanceOf(throwable(TypeNotPresentException.class))
.hasMessageContaining("com.example.DoesNotExist");
}
}
@@ -188,16 +189,16 @@ class TypeMappedAnnotationTests {
Map.of(attributeName, "com.example.DoesNotExist"));
// 0) Sanity check MergedAnnotation.getClass() behavior.
assertThatIllegalArgumentException()
assertThatExceptionOfType(TypeNotPresentException.class)
.isThrownBy(() -> mergedAnnotation.getClass(attributeName))
.withMessage("Could not find class [com.example.DoesNotExist]")
.withCauseInstanceOf(ClassNotFoundException.class);
.withMessageContaining("com.example.DoesNotExist")
.withCauseExactlyInstanceOf(ClassNotFoundException.class);
var attributes = mergedAnnotation.asAnnotationAttributes(Adapt.values(false, true));
// 1) Attribute should be present, even though its value is an exception.
assertThat(attributes).containsKey(attributeName);
assertThat(attributes.get(attributeName)).asInstanceOf(throwable(IllegalArgumentException.class))
assertThat(attributes.get(attributeName)).asInstanceOf(throwable(TypeNotPresentException.class))
.hasMessageContaining("com.example.DoesNotExist");
// 2) Accessing the attribute via AnnotationAttributes.getClassArray() should throw an
@@ -213,16 +214,16 @@ class TypeMappedAnnotationTests {
Map.of(attributeName, new String[] { "com.example.DoesNotExist" }));
// 0) Sanity check MergedAnnotation.getClassArray() behavior.
assertThatIllegalArgumentException()
assertThatExceptionOfType(TypeNotPresentException.class)
.isThrownBy(() -> mergedAnnotation.getClassArray(attributeName))
.withMessage("Could not find class [com.example.DoesNotExist]")
.withCauseInstanceOf(ClassNotFoundException.class);
.withMessageContaining("com.example.DoesNotExist")
.withCauseExactlyInstanceOf(ClassNotFoundException.class);
var attributes = mergedAnnotation.asAnnotationAttributes(Adapt.values(false, true));
// 1) Attribute should be present, even though its value is an exception.
assertThat(attributes).containsKey(attributeName);
assertThat(attributes.get(attributeName)).asInstanceOf(throwable(IllegalArgumentException.class))
assertThat(attributes.get(attributeName)).asInstanceOf(throwable(TypeNotPresentException.class))
.hasMessageContaining("com.example.DoesNotExist");
// 2) Accessing the attribute via AnnotationAttributes.getClassArray() should throw an
@@ -236,9 +237,9 @@ class TypeMappedAnnotationTests {
.isThrownBy(throwingCallable)
.withMessageMatching("""
Attribute '%s' for annotation \\[.+?\\] was not resolvable \
due to exception \\[.+?IllegalArgumentException.+?\\]""".formatted(attributeName))
due to exception \\[.+?TypeNotPresentException.+?\\]""".formatted(attributeName))
.havingCause()
.isExactlyInstanceOf(IllegalArgumentException.class)
.isExactlyInstanceOf(TypeNotPresentException.class)
.withMessageContaining("com.example.DoesNotExist")
.havingCause()
.isExactlyInstanceOf(ClassNotFoundException.class)
@@ -27,7 +27,7 @@ import org.springframework.core.type.AbstractAnnotationMetadataTests;
import org.springframework.core.type.AnnotationMetadata;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link SimpleAnnotationMetadata} and
@@ -59,7 +59,9 @@ class DefaultAnnotationMetadataTests extends AbstractAnnotationMetadataTests {
.getAnnotations()
.get(ClassAttributes.class);
assertThat(mergedAnnotation.getStringArray("types")).contains("javax.annotation.meta.When");
assertThatIllegalArgumentException().isThrownBy(() -> mergedAnnotation.getClassArray("types"));
assertThatExceptionOfType(TypeNotPresentException.class)
.isThrownBy(() -> mergedAnnotation.getClassArray("types"))
.withMessageContaining("javax.annotation.meta.When");
}
@@ -27,7 +27,7 @@ import org.springframework.core.type.AbstractAnnotationMetadataTests;
import org.springframework.core.type.AnnotationMetadata;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link SimpleAnnotationMetadata} and
@@ -58,7 +58,9 @@ class SimpleAnnotationMetadataTests extends AbstractAnnotationMetadataTests {
.getAnnotations()
.get(ClassAttributes.class);
assertThat(mergedAnnotation.getStringArray("types")).contains("javax.annotation.meta.When");
assertThatIllegalArgumentException().isThrownBy(() -> mergedAnnotation.getClassArray("types"));
assertThatExceptionOfType(TypeNotPresentException.class)
.isThrownBy(() -> mergedAnnotation.getClassArray("types"))
.withMessageContaining("javax.annotation.meta.When");
}