Use canonical names in error messages in annotation processing

Prior to this commit, we invoked `Class.getName()` when building error
messages during annotation processing, resulting in exceptions like the
following which use binary names for nested types and arrays.

  Attribute 'chars' in annotation
  org.springframework.core.annotation.AnnotationUtilsTests$CharsContainer
  should be compatible with [C but a [I value was returned

This commit switches to canonical names in error messages in annotation
processing, resulting in improved such errors messages such as the
following.

  Attribute 'chars' in annotation
  org.springframework.core.annotation.AnnotationUtilsTests.CharsContainer
  should be compatible with char[] but a int[] value was returned

In addition, this commit introduces a new getCanonicalName(Class) method
in ClassUtils, which has effectively been extracted from the following
classes where this functionality was previously duplicated.

- AttributeMethods
- SynthesizedMergedAnnotationInvocationHandler
- TypeDescriptor
- DefaultRetryPolicy
- ReflectiveIndexAccessor

Closes gh-36607
This commit is contained in:
Sam Brannen
2026-04-06 17:32:05 +02:00
parent 31d9fe5f41
commit 6062363738
21 changed files with 143 additions and 113 deletions
@@ -20,6 +20,8 @@ import java.util.Objects;
import org.jspecify.annotations.Nullable;
import org.springframework.util.ClassUtils;
/**
* Reference to a Java method, identified by its owner class and the method name.
*
@@ -43,7 +45,7 @@ public final class MethodReference {
}
public static MethodReference of(Class<?> klass, String methodName) {
return new MethodReference(klass.getCanonicalName(), methodName);
return new MethodReference(ClassUtils.getCanonicalName(klass), methodName);
}
/**
@@ -25,6 +25,7 @@ import org.jspecify.annotations.Nullable;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.TypeReference;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Record of an invocation of a method relevant to {@link org.springframework.aot.hint.RuntimeHints}.
@@ -181,7 +182,7 @@ public final class RecordedInvocation {
else {
Class<?> instanceType = (getInstance() instanceof Class<?> clazz) ? clazz : getInstance().getClass();
return "<%s> invocation of <%s> on type <%s> with arguments %s".formatted(
getHintType().hintClassName(), getMethodReference(), instanceType.getCanonicalName(), getArguments());
getHintType().hintClassName(), getMethodReference(), ClassUtils.getCanonicalName(instanceType), getArguments());
}
}
@@ -24,6 +24,7 @@ import java.util.function.Predicate;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Abstract base class for {@link MergedAnnotation} implementations.
@@ -215,7 +216,7 @@ abstract class AbstractMergedAnnotation<A extends Annotation> implements MergedA
T value = getAttributeValue(attributeName, type);
if (value == null) {
throw new NoSuchElementException("No attribute named '" + attributeName +
"' present in merged annotation " + getType().getName());
"' present in merged annotation " + ClassUtils.getCanonicalName(getType()));
}
return value;
}
@@ -25,6 +25,7 @@ import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
@@ -126,7 +127,7 @@ public class AnnotationAttributes extends LinkedHashMap<String, @Nullable Object
AnnotationAttributes(Class<? extends Annotation> annotationType, boolean validated) {
Assert.notNull(annotationType, "'annotationType' must not be null");
this.annotationType = annotationType;
this.displayName = annotationType.getName();
this.displayName = ClassUtils.getCanonicalName(annotationType);
this.validated = validated;
}
@@ -32,6 +32,7 @@ import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.core.annotation.AnnotationTypeMapping.MirrorSets.MirrorSet;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -649,7 +650,7 @@ final class AnnotationTypeMapping {
throw new AnnotationConfigurationException(String.format(
"Different @AliasFor mirror values for annotation [%s]%s; attribute '%s' " +
"and its alias '%s' are declared with values of [%s] and [%s].",
getAnnotationType().getName(), on,
ClassUtils.getCanonicalName(getAnnotationType()), on,
attributes.get(result).getName(),
attribute.getName(),
ObjectUtils.nullSafeToString(lastValue),
@@ -28,6 +28,7 @@ import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.lang.Contract;
import org.springframework.util.ClassUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
/**
@@ -124,7 +125,8 @@ final class AnnotationTypeMappings {
AnnotationUtils.rethrowAnnotationConfigurationException(ex);
if (failureLogger.isEnabled()) {
failureLogger.log("Failed to introspect " + (meta ? "meta-annotation @" : "annotation @") +
annotationType.getName(), (source != null ? source.getAnnotationType() : null), ex);
ClassUtils.getCanonicalName(annotationType),
(source != null ? ClassUtils.getCanonicalName(source.getAnnotationType()) : null), ex);
}
}
}
@@ -26,6 +26,7 @@ import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ReflectionUtils;
@@ -143,8 +144,9 @@ final class AttributeMethods {
throw ex;
}
catch (Throwable ex) {
throw new IllegalStateException("Could not obtain annotation attribute value for " +
get(i).getName() + " declared on @" + getName(annotation.annotationType()), ex);
throw new IllegalStateException(
"Could not obtain annotation attribute value for " + get(i).getName() +
" declared on @" + ClassUtils.getCanonicalName(annotation.annotationType()), ex);
}
}
}
@@ -305,13 +307,8 @@ final class AttributeMethods {
if (attributeName == null) {
return "(none)";
}
String in = (annotationType != null ? " in annotation [" + annotationType.getName() + "]" : "");
String in = (annotationType != null ? " in annotation [" + ClassUtils.getCanonicalName(annotationType) + "]" : "");
return "attribute '" + attributeName + "'" + in;
}
private static String getName(Class<?> clazz) {
String canonicalName = clazz.getCanonicalName();
return (canonicalName != null ? canonicalName : clazz.getName());
}
}
@@ -26,6 +26,7 @@ import org.jspecify.annotations.Nullable;
import org.springframework.lang.Contract;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ObjectUtils;
@@ -312,7 +313,7 @@ public abstract class RepeatableContainers {
if (returnType.componentType() != repeatable) {
throw new AnnotationConfigurationException(
"Container type [%s] must declare a 'value' attribute for an array of type [%s]"
.formatted(container.getName(), repeatable.getName()));
.formatted(ClassUtils.getCanonicalName(container), ClassUtils.getCanonicalName(repeatable)));
}
}
catch (AnnotationConfigurationException ex) {
@@ -321,7 +322,7 @@ public abstract class RepeatableContainers {
catch (Throwable ex) {
throw new AnnotationConfigurationException(
"Invalid declaration of container type [%s] for repeatable annotation [%s]"
.formatted(container.getName(), repeatable.getName()), ex);
.formatted(ClassUtils.getCanonicalName(container), ClassUtils.getCanonicalName(repeatable)), ex);
}
this.repeatable = repeatable;
this.container = container;
@@ -331,7 +332,7 @@ public abstract class RepeatableContainers {
private Class<? extends Annotation> deduceContainer(Class<? extends Annotation> repeatable) {
Repeatable annotation = repeatable.getAnnotation(Repeatable.class);
Assert.notNull(annotation, () -> "Annotation type must be a repeatable annotation: " +
"failed to resolve container type for " + repeatable.getName());
"failed to resolve container type for " + ClassUtils.getCanonicalName(repeatable));
return annotation.value();
}
@@ -136,7 +136,7 @@ final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> i
private String annotationToString() {
String string = this.string;
if (string == null) {
StringBuilder builder = new StringBuilder("@").append(getName(this.type)).append('(');
StringBuilder builder = new StringBuilder("@").append(ClassUtils.getCanonicalName(this.type)).append('(');
if (this.attributes.size() == 1 && this.attributes.get(0).getName().equals(MergedAnnotation.VALUE)) {
// Don't prepend "value=" for an annotation that only declares a "value" attribute.
builder.append(toString(getAttributeValue(this.attributes.get(0))));
@@ -208,7 +208,7 @@ final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> i
return e.name();
}
if (type == Class.class) {
return getName((Class<?>) value) + ".class";
return ClassUtils.getCanonicalName((Class<?>) value) + ".class";
}
return String.valueOf(value);
}
@@ -218,7 +218,7 @@ final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> i
Class<?> type = ClassUtils.resolvePrimitiveIfNecessary(method.getReturnType());
return this.annotation.getValue(attributeName, type).orElseThrow(
() -> new NoSuchElementException("No value found for attribute named '" + attributeName +
"' in merged annotation " + getName(this.annotation.getType())));
"' in merged annotation " + ClassUtils.getCanonicalName(this.annotation.getType())));
});
// Clone non-empty arrays so that users cannot alter the contents of values in our cache.
@@ -272,9 +272,4 @@ final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> i
return (A) Proxy.newProxyInstance(classLoader, interfaces, handler);
}
private static String getName(Class<?> clazz) {
String canonicalName = clazz.getCanonicalName();
return (canonicalName != null ? canonicalName : clazz.getName());
}
}
@@ -490,7 +490,7 @@ final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnn
}
if (!type.isInstance(value)) {
throw new IllegalArgumentException("Unable to adapt value of type " +
value.getClass().getName() + " to " + type.getName());
ClassUtils.getCanonicalName(value.getClass()) + " to " + ClassUtils.getCanonicalName(type));
}
return (T) value;
}
@@ -525,8 +525,8 @@ final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnn
}
if (!attributeType.isInstance(value)) {
throw new IllegalStateException("Attribute '" + attribute.getName() +
"' in annotation " + getType().getName() + " should be compatible with " +
attributeType.getName() + " but a " + value.getClass().getName() +
"' in annotation " + ClassUtils.getCanonicalName(getType()) + " should be compatible with " +
ClassUtils.getCanonicalName(attributeType) + " but a " + ClassUtils.getCanonicalName(value.getClass()) +
" value was returned");
}
return value;
@@ -583,7 +583,7 @@ final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnn
int attributeIndex = (isFiltered(attributeName) ? -1 : this.mapping.getAttributes().indexOf(attributeName));
if (attributeIndex == -1 && required) {
throw new NoSuchElementException("No attribute named '" + attributeName +
"' present in merged annotation " + getType().getName());
"' present in merged annotation " + ClassUtils.getCanonicalName(getType()));
}
return attributeIndex;
}
@@ -660,9 +660,10 @@ final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnn
catch (Exception ex) {
AnnotationUtils.rethrowAnnotationConfigurationException(ex);
if (logger.isEnabled()) {
String type = mapping.getAnnotationType().getName();
String type = ClassUtils.getCanonicalName(mapping.getAnnotationType());
String item = (mapping.getDistance() == 0 ? "annotation " + type :
"meta-annotation " + type + " from " + mapping.getRoot().getAnnotationType().getName());
"meta-annotation " + type + " from " +
ClassUtils.getCanonicalName(mapping.getRoot().getAnnotationType()));
logger.log("Failed to introspect " + item, source, ex);
}
return null;
@@ -541,7 +541,7 @@ public class TypeDescriptor implements Serializable {
public String toString() {
StringBuilder builder = new StringBuilder();
for (Annotation ann : getAnnotations()) {
builder.append('@').append(getName(ann.annotationType())).append(' ');
builder.append('@').append(ClassUtils.getCanonicalName(ann.annotationType())).append(' ');
}
builder.append(getResolvableType());
return builder.toString();
@@ -726,11 +726,6 @@ public class TypeDescriptor implements Serializable {
return new TypeDescriptor(property).nested(nestingLevel);
}
private static String getName(Class<?> clazz) {
String canonicalName = clazz.getCanonicalName();
return (canonicalName != null ? canonicalName : clazz.getName());
}
private interface AnnotatedElementSupplier extends Supplier<AnnotatedElementAdapter>, Serializable {
}
@@ -23,6 +23,7 @@ import java.util.function.Predicate;
import org.jspecify.annotations.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.ExceptionTypeFilter;
import org.springframework.util.backoff.BackOff;
@@ -96,8 +97,7 @@ class DefaultRetryPolicy implements RetryPolicy {
private static String names(Set<Class<? extends Throwable>> types) {
StringJoiner result = new StringJoiner(", ", "[", "]");
for (Class<? extends Throwable> type : types) {
String name = type.getCanonicalName();
result.add(name != null? name : type.getName());
result.add(ClassUtils.getCanonicalName(type));
}
return result.toString();
}
@@ -24,6 +24,8 @@ import java.util.StringJoiner;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.util.ClassUtils;
/**
* {@link ValueStyler} that converts objects to String form &mdash; generally for
* debugging purposes &mdash; using simple styling conventions that mimic the
@@ -45,7 +47,7 @@ public class SimpleValueStyler extends DefaultValueStyler {
/**
* Default {@link Class} styling function: {@link Class#getCanonicalName()}.
*/
public static final Function<Class<?>, String> DEFAULT_CLASS_STYLER = Class::getCanonicalName;
public static final Function<Class<?>, String> DEFAULT_CLASS_STYLER = ClassUtils::getCanonicalName;
/**
* Default {@link Method} styling function: converts the supplied {@link Method}
@@ -1125,6 +1125,20 @@ public abstract class ClassUtils {
return (lastDotIndex != -1 ? fqClassName.substring(0, lastDotIndex) : "");
}
/**
* Return the {@linkplain Class#getCanonicalName() canonical name} of the given
* class, or the {@linkplain Class#getName() binary name} of the class if the
* canonical name does not exist.
* @param clazz the class
* @return the canonical name of the class, or the binary name as a fallback
* @since 7.1
*/
public static String getCanonicalName(Class<?> clazz) {
Assert.notNull(clazz, "Class must not be null");
String canonicalName = clazz.getCanonicalName();
return (canonicalName != null ? canonicalName : clazz.getName());
}
/**
* Return the qualified name of the given class: usually simply
* the class name, but component type class name + "[]" for arrays.
@@ -102,7 +102,7 @@ class AnnotationTypeMappingsTests {
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForWithBothValueAndAttribute.class))
.withMessage("In @AliasFor declared on attribute 'test' in annotation [%s], attribute 'attribute' " +
"and its alias 'value' are present with values of 'foo' and 'bar', but only one is permitted.",
AliasForWithBothValueAndAttribute.class.getName());
AliasForWithBothValueAndAttribute.class.getCanonicalName());
}
@Test
@@ -111,7 +111,7 @@ class AnnotationTypeMappingsTests {
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForToSelfNonExistingAttribute.class))
.withMessage("@AliasFor declaration on attribute 'test' in annotation [%s] " +
"declares an alias for 'missing' which is not present.",
AliasForToSelfNonExistingAttribute.class.getName());
AliasForToSelfNonExistingAttribute.class.getCanonicalName());
}
@Test
@@ -119,8 +119,8 @@ class AnnotationTypeMappingsTests {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForToOtherNonExistingAttribute.class))
.withMessage("Attribute 'test' in annotation [%s] is declared as an @AliasFor nonexistent " +
"attribute 'missing' in annotation [%s].", AliasForToOtherNonExistingAttribute.class.getName(),
AliasForToOtherNonExistingAttributeTarget.class.getName());
"attribute 'missing' in annotation [%s].", AliasForToOtherNonExistingAttribute.class.getCanonicalName(),
AliasForToOtherNonExistingAttributeTarget.class.getCanonicalName());
}
@Test
@@ -129,7 +129,7 @@ class AnnotationTypeMappingsTests {
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForToSelf.class))
.withMessage("@AliasFor declaration on attribute 'test' in annotation [%s] points to itself. " +
"Specify 'annotation' to point to a same-named attribute on a meta-annotation.",
AliasForToSelf.class.getName());
AliasForToSelf.class.getCanonicalName());
}
@Test
@@ -147,13 +147,13 @@ class AnnotationTypeMappingsTests {
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForWithIncompatibleReturnTypes.class))
.withMessage("Misconfigured aliases: attribute 'test' in annotation [%s] and attribute 'test' " +
"in annotation [%s] must declare the same return type.",
AliasForWithIncompatibleReturnTypes.class.getName(),
AliasForWithIncompatibleReturnTypesTarget.class.getName());
AliasForWithIncompatibleReturnTypes.class.getCanonicalName(),
AliasForWithIncompatibleReturnTypesTarget.class.getCanonicalName());
}
@Test
void forAnnotationTypeWhenAliasForToSelfAnnotatedToOtherAttribute() {
String annotationType = AliasForToSelfAnnotatedToOtherAttribute.class.getName();
String annotationType = AliasForToSelfAnnotatedToOtherAttribute.class.getCanonicalName();
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForToSelfAnnotatedToOtherAttribute.class))
.withMessage("Attribute 'b' in annotation [%1$s] must be declared as an @AliasFor attribute 'a' in " +
@@ -167,8 +167,8 @@ class AnnotationTypeMappingsTests {
}
private void assertMixedImplicitAndExplicitAliases(Class<? extends Annotation> annotationType, String overriddenAttribute) {
String annotationName = annotationType.getName();
String metaAnnotationName = AliasPair.class.getName();
String annotationName = annotationType.getCanonicalName();
String metaAnnotationName = AliasPair.class.getCanonicalName();
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(annotationType))
.withMessage("Attribute 'b' in annotation [" + annotationName +
@@ -180,8 +180,8 @@ class AnnotationTypeMappingsTests {
void forAnnotationTypeWhenAliasForNonMetaAnnotated() {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForNonMetaAnnotated.class))
.withMessage("@AliasFor declaration on attribute 'test' in annotation [" + AliasForNonMetaAnnotated.class.getName() +
"] declares an alias for attribute 'test' in annotation [" + AliasForNonMetaAnnotatedTarget.class.getName() +
.withMessage("@AliasFor declaration on attribute 'test' in annotation [" + AliasForNonMetaAnnotated.class.getCanonicalName() +
"] declares an alias for attribute 'test' in annotation [" + AliasForNonMetaAnnotatedTarget.class.getCanonicalName() +
"] which is not meta-present.");
}
@@ -189,8 +189,8 @@ class AnnotationTypeMappingsTests {
void forAnnotationTypeWhenAliasForSelfWithDifferentDefaults() {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForSelfWithDifferentDefaults.class))
.withMessage("Misconfigured aliases: attribute 'a' in annotation [" + AliasForSelfWithDifferentDefaults.class.getName() +
"] and attribute 'b' in annotation [" + AliasForSelfWithDifferentDefaults.class.getName() +
.withMessage("Misconfigured aliases: attribute 'a' in annotation [" + AliasForSelfWithDifferentDefaults.class.getCanonicalName() +
"] and attribute 'b' in annotation [" + AliasForSelfWithDifferentDefaults.class.getCanonicalName() +
"] must declare the same default value.");
}
@@ -198,8 +198,8 @@ class AnnotationTypeMappingsTests {
void forAnnotationTypeWhenAliasForSelfWithMissingDefault() {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForSelfWithMissingDefault.class))
.withMessage("Misconfigured aliases: attribute 'a' in annotation [" + AliasForSelfWithMissingDefault.class.getName() +
"] and attribute 'b' in annotation [" + AliasForSelfWithMissingDefault.class.getName() +
.withMessage("Misconfigured aliases: attribute 'a' in annotation [" + AliasForSelfWithMissingDefault.class.getCanonicalName() +
"] and attribute 'b' in annotation [" + AliasForSelfWithMissingDefault.class.getCanonicalName() +
"] must declare default values.");
}
@@ -207,8 +207,8 @@ class AnnotationTypeMappingsTests {
void forAnnotationTypeWhenAliasWithExplicitMirrorAndDifferentDefaults() {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasWithExplicitMirrorAndDifferentDefaults.class))
.withMessage("Misconfigured aliases: attribute 'a' in annotation [" + AliasWithExplicitMirrorAndDifferentDefaults.class.getName() +
"] and attribute 'c' in annotation [" + AliasWithExplicitMirrorAndDifferentDefaults.class.getName() +
.withMessage("Misconfigured aliases: attribute 'a' in annotation [" + AliasWithExplicitMirrorAndDifferentDefaults.class.getCanonicalName() +
"] and attribute 'c' in annotation [" + AliasWithExplicitMirrorAndDifferentDefaults.class.getCanonicalName() +
"] must declare the same default value.");
}
@@ -352,8 +352,8 @@ class AnnotationTypeMappingsTests {
AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(AliasPair.class).get(0);
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> resolveMirrorSets(mapping, WithDifferentValueAliasPair.class, AliasPair.class))
.withMessage("Different @AliasFor mirror values for annotation [" + AliasPair.class.getName() + "] declared on " +
WithDifferentValueAliasPair.class.getName() +
.withMessage("Different @AliasFor mirror values for annotation [" + AliasPair.class.getCanonicalName() +
"] declared on " + WithDifferentValueAliasPair.class.getName() +
"; attribute 'a' and its alias 'b' are declared with values of [test1] and [test2].");
}
@@ -718,11 +718,11 @@ class AnnotationUtilsTests {
ImplicitAliasesWithMissingDefaultValuesContextConfig config = clazz.getAnnotation(annotationType);
assertThat(config).isNotNull();
assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() ->
synthesizeAnnotation(config, clazz))
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> synthesizeAnnotation(config, clazz))
.withMessageStartingWith("Misconfigured aliases:")
.withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]")
.withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]")
.withMessageContaining("attribute 'location1' in annotation [%s]", annotationType.getCanonicalName())
.withMessageContaining("attribute 'location2' in annotation [%s]", annotationType.getCanonicalName())
.withMessageContaining("default values");
}
@@ -733,11 +733,11 @@ class AnnotationUtilsTests {
ImplicitAliasesWithDifferentDefaultValuesContextConfig.class;
ImplicitAliasesWithDifferentDefaultValuesContextConfig config = clazz.getAnnotation(annotationType);
assertThat(config).isNotNull();
assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() ->
synthesizeAnnotation(config, clazz))
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> synthesizeAnnotation(config, clazz))
.withMessageStartingWith("Misconfigured aliases:")
.withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]")
.withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]")
.withMessageContaining("attribute 'location1' in annotation [%s]", annotationType.getCanonicalName())
.withMessageContaining("attribute 'location2' in annotation [%s]", annotationType.getCanonicalName())
.withMessageContaining("same default value");
}
@@ -919,8 +919,18 @@ class AnnotationUtilsTests {
Map<String, Object> map = Collections.singletonMap(VALUE, 42L);
assertThatIllegalStateException().isThrownBy(() ->
synthesizeAnnotation(map, Component.class, null).value())
.withMessageContaining("Attribute 'value' in annotation org.springframework.core.testfixture.stereotype.Component " +
"should be compatible with java.lang.String but a java.lang.Long value was returned");
.withMessageContaining("Attribute 'value' in annotation %s should be compatible with " +
"java.lang.String but a java.lang.Long value was returned",
Component.class.getCanonicalName());
}
@Test
void synthesizeAnnotationFromMapWithAttributeOfIncorrectArrayType() {
Map<String, Object> map = Collections.singletonMap(VALUE, new int[] {42});
assertThatIllegalStateException().isThrownBy(() ->
synthesizeAnnotation(map, CharsContainer.class, null).chars())
.withMessageContaining("Attribute 'chars' in annotation %s should be compatible with " +
"char[] but a int[] value was returned", CharsContainer.class.getCanonicalName());
}
@Test
@@ -183,32 +183,32 @@ class ComposedRepeatableAnnotationsTests {
assertThatIllegalArgumentException().isThrownBy(throwingCallable)
.withMessageStartingWith("Annotation type must be a repeatable annotation")
.withMessageContaining("failed to resolve container type for")
.withMessageContaining(NonRepeatable.class.getName());
.withMessageContaining(NonRepeatable.class.getCanonicalName());
}
private void expectContainerMissingValueAttribute(ThrowingCallable throwingCallable) {
assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(throwingCallable)
.withMessageStartingWith("Invalid declaration of container type")
.withMessageContaining(ContainerMissingValueAttribute.class.getName())
.withMessageContaining(ContainerMissingValueAttribute.class.getCanonicalName())
.withMessageContaining("for repeatable annotation")
.withMessageContaining(InvalidRepeatable.class.getName())
.withMessageContaining(InvalidRepeatable.class.getCanonicalName())
.withCauseExactlyInstanceOf(NoSuchMethodException.class);
}
private void expectContainerWithNonArrayValueAttribute(ThrowingCallable throwingCallable) {
assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(throwingCallable)
.withMessageStartingWith("Container type")
.withMessageContaining(ContainerWithNonArrayValueAttribute.class.getName())
.withMessageContaining(ContainerWithNonArrayValueAttribute.class.getCanonicalName())
.withMessageContaining("must declare a 'value' attribute for an array of type")
.withMessageContaining(InvalidRepeatable.class.getName());
.withMessageContaining(InvalidRepeatable.class.getCanonicalName());
}
private void expectContainerWithArrayValueAttributeButWrongComponentType(ThrowingCallable throwingCallable) {
assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(throwingCallable)
.withMessageStartingWith("Container type")
.withMessageContaining(ContainerWithArrayValueAttributeButWrongComponentType.class.getName())
.withMessageContaining(ContainerWithArrayValueAttributeButWrongComponentType.class.getCanonicalName())
.withMessageContaining("must declare a 'value' attribute for an array of type")
.withMessageContaining(InvalidRepeatable.class.getName());
.withMessageContaining(InvalidRepeatable.class.getCanonicalName());
}
private void assertGetRepeatableAnnotations(AnnotatedElement element) {
@@ -282,16 +282,16 @@ class MergedAnnotationsRepeatableAnnotationTests {
private void nonRepeatableRequirements(Exception ex) {
assertThat(ex)
.hasMessageStartingWith("Annotation type must be a repeatable annotation")
.hasMessageContaining("failed to resolve container type for", NonRepeatable.class.getName());
.hasMessageContaining("failed to resolve container type for", NonRepeatable.class.getCanonicalName());
}
private void missingValueAttributeRequirements(Exception ex) {
assertThat(ex)
.hasMessageStartingWith("Invalid declaration of container type")
.hasMessageContaining(
ContainerMissingValueAttribute.class.getName(),
ContainerMissingValueAttribute.class.getCanonicalName(),
"for repeatable annotation",
InvalidRepeatable.class.getName())
InvalidRepeatable.class.getCanonicalName())
.hasCauseInstanceOf(NoSuchMethodException.class);
}
@@ -299,18 +299,18 @@ class MergedAnnotationsRepeatableAnnotationTests {
assertThat(ex)
.hasMessageStartingWith("Container type")
.hasMessageContaining(
ContainerWithNonArrayValueAttribute.class.getName(),
ContainerWithNonArrayValueAttribute.class.getCanonicalName(),
"must declare a 'value' attribute for an array of type",
InvalidRepeatable.class.getName());
InvalidRepeatable.class.getCanonicalName());
}
private void wrongComponentTypeRequirements(Exception ex) {
assertThat(ex)
.hasMessageStartingWith("Container type")
.hasMessageContaining(
ContainerWithArrayValueAttributeButWrongComponentType.class.getName(),
ContainerWithArrayValueAttributeButWrongComponentType.class.getCanonicalName(),
"must declare a 'value' attribute for an array of type",
InvalidRepeatable.class.getName());
InvalidRepeatable.class.getCanonicalName());
}
private static ThrowableTypeAssert<AnnotationConfigurationException> assertThatAnnotationConfigurationException() {
@@ -1605,7 +1605,7 @@ class MergedAnnotationsTests {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> MergedAnnotation.from(annotation))
.withMessageStartingWith("@AliasFor declaration on attribute 'foo' in annotation")
.withMessageContaining(AliasForWithMissingAttributeDeclaration.class.getName())
.withMessageContaining(AliasForWithMissingAttributeDeclaration.class.getCanonicalName())
.withMessageContaining("points to itself");
}
@@ -1618,7 +1618,7 @@ class MergedAnnotationsTests {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> MergedAnnotation.from(annotation))
.withMessageStartingWith("In @AliasFor declared on attribute 'foo' in annotation")
.withMessageContaining(AliasForWithDuplicateAttributeDeclaration.class.getName())
.withMessageContaining(AliasForWithDuplicateAttributeDeclaration.class.getCanonicalName())
.withMessageContaining("attribute 'attribute' and its alias 'value' are present with values of 'baz' and 'bar'");
}
@@ -1630,7 +1630,7 @@ class MergedAnnotationsTests {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> MergedAnnotation.from(annotation))
.withMessageStartingWith("@AliasFor declaration on attribute 'foo' in annotation")
.withMessageContaining(AliasForNonexistentAttribute.class.getName())
.withMessageContaining(AliasForNonexistentAttribute.class.getCanonicalName())
.withMessageContaining("declares an alias for 'bar' which is not present");
}
@@ -1643,7 +1643,7 @@ class MergedAnnotationsTests {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> MergedAnnotation.from(annotation))
.withMessage("@AliasFor declaration on attribute 'bar' in annotation [" +
AliasForWithMirroredAliasForWrongAttribute.class.getName() +
AliasForWithMirroredAliasForWrongAttribute.class.getCanonicalName() +
"] declares an alias for 'quux' which is not present.");
}
@@ -1655,7 +1655,7 @@ class MergedAnnotationsTests {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> MergedAnnotation.from(annotation))
.withMessageStartingWith("Misconfigured aliases")
.withMessageContaining(AliasForAttributeOfDifferentType.class.getName())
.withMessageContaining(AliasForAttributeOfDifferentType.class.getCanonicalName())
.withMessageContaining("attribute 'foo'")
.withMessageContaining("attribute 'bar'")
.withMessageContaining("same return type");
@@ -1669,7 +1669,7 @@ class MergedAnnotationsTests {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> MergedAnnotation.from(annotation))
.withMessageStartingWith("Misconfigured aliases")
.withMessageContaining(AliasForWithMissingDefaultValues.class.getName())
.withMessageContaining(AliasForWithMissingDefaultValues.class.getCanonicalName())
.withMessageContaining("attribute 'foo' in annotation")
.withMessageContaining("attribute 'bar' in annotation")
.withMessageContaining("default values");
@@ -1684,7 +1684,7 @@ class MergedAnnotationsTests {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> MergedAnnotation.from(annotation))
.withMessageStartingWith("Misconfigured aliases")
.withMessageContaining(AliasForAttributeWithDifferentDefaultValue.class.getName())
.withMessageContaining(AliasForAttributeWithDifferentDefaultValue.class.getCanonicalName())
.withMessageContaining("attribute 'foo' in annotation")
.withMessageContaining("attribute 'bar' in annotation")
.withMessageContaining("same default value");
@@ -1699,9 +1699,9 @@ class MergedAnnotationsTests {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> MergedAnnotation.from(annotation))
.withMessageStartingWith("@AliasFor declaration on attribute 'xmlConfigFile' in annotation")
.withMessageContaining(AliasedComposedTestConfigurationNotMetaPresent.class.getName())
.withMessageContaining(AliasedComposedTestConfigurationNotMetaPresent.class.getCanonicalName())
.withMessageContaining("declares an alias for attribute 'location' in annotation")
.withMessageContaining(TestConfiguration.class.getName())
.withMessageContaining(TestConfiguration.class.getCanonicalName())
.withMessageContaining("not meta-present");
}
@@ -1792,8 +1792,8 @@ class MergedAnnotationsTests {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> MergedAnnotation.from(clazz, config))
.withMessageStartingWith("Misconfigured aliases:")
.withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]")
.withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]")
.withMessageContaining("attribute 'location1' in annotation [%s]", annotationType.getCanonicalName())
.withMessageContaining("attribute 'location2' in annotation [%s]", annotationType.getCanonicalName())
.withMessageContaining("default values");
}
@@ -1807,8 +1807,8 @@ class MergedAnnotationsTests {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> MergedAnnotation.from(clazz, config))
.withMessageStartingWith("Misconfigured aliases:")
.withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]")
.withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]")
.withMessageContaining("attribute 'location1' in annotation [%s]", annotationType.getCanonicalName())
.withMessageContaining("attribute 'location2' in annotation [%s]", annotationType.getCanonicalName())
.withMessageContaining("same default value");
}
@@ -1978,9 +1978,20 @@ class MergedAnnotationsTests {
MergedAnnotation<Component> annotation = MergedAnnotation.of(Component.class, map);
assertThatIllegalStateException()
.isThrownBy(() -> annotation.synthesize().value())
.withMessage("Attribute 'value' in annotation " +
"org.springframework.core.testfixture.stereotype.Component should be " +
"compatible with java.lang.String but a java.lang.Long value was returned");
.withMessage("Attribute 'value' in annotation %s should be compatible with " +
"java.lang.String but a java.lang.Long value was returned",
Component.class.getCanonicalName());
}
@Test
void synthesizeFromMapWithAttributeOfIncorrectArrayType() {
Map<String, Object> map = Collections.singletonMap("value", new int[] {42});
MergedAnnotation<TestPropertySource> annotation = MergedAnnotation.of(TestPropertySource.class, map);
assertThatIllegalStateException()
.isThrownBy(() -> annotation.synthesize().value())
.withMessage("Attribute 'value' in annotation %s should be compatible with " +
"java.lang.String[] but a int[] value was returned",
TestPropertySource.class.getCanonicalName());
}
@Test
@@ -120,7 +120,7 @@ class RepeatableContainersTests {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> RepeatableContainers.explicitRepeatable(ExplicitRepeatable.class, InvalidNoValue.class))
.withMessageContaining("Invalid declaration of container type [%s] for repeatable annotation [%s]",
InvalidNoValue.class.getName(), ExplicitRepeatable.class.getName());
InvalidNoValue.class.getCanonicalName(), ExplicitRepeatable.class.getCanonicalName());
}
@Test
@@ -128,7 +128,7 @@ class RepeatableContainersTests {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> RepeatableContainers.explicitRepeatable(ExplicitRepeatable.class, InvalidNotArray.class))
.withMessage("Container type [%s] must declare a 'value' attribute for an array of type [%s]",
InvalidNotArray.class.getName(), ExplicitRepeatable.class.getName());
InvalidNotArray.class.getCanonicalName(), ExplicitRepeatable.class.getCanonicalName());
}
@Test
@@ -136,7 +136,7 @@ class RepeatableContainersTests {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> RepeatableContainers.explicitRepeatable(ExplicitRepeatable.class, InvalidWrongArrayType.class))
.withMessage("Container type [%s] must declare a 'value' attribute for an array of type [%s]",
InvalidWrongArrayType.class.getName(), ExplicitRepeatable.class.getName());
InvalidWrongArrayType.class.getCanonicalName(), ExplicitRepeatable.class.getCanonicalName());
}
@Test
@@ -151,7 +151,7 @@ class RepeatableContainersTests {
assertThatIllegalArgumentException()
.isThrownBy(() -> RepeatableContainers.explicitRepeatable(ExplicitRepeatable.class, null))
.withMessage("Annotation type must be a repeatable annotation: failed to resolve container type for %s",
ExplicitRepeatable.class.getName());
ExplicitRepeatable.class.getCanonicalName());
}
}
@@ -156,7 +156,8 @@ public class ReflectiveIndexAccessor implements CompilableIndexAccessor {
}
catch (Exception ex) {
throw new IllegalArgumentException("Failed to find public read-method '%s(%s)' in class '%s'."
.formatted(readMethodName, getName(indexType), getName(targetType)));
.formatted(readMethodName, ClassUtils.getCanonicalName(indexType),
ClassUtils.getCanonicalName(targetType)));
}
this.readMethodToInvoke = ClassUtils.getPubliclyAccessibleMethodIfPossible(this.readMethod, targetType);
@@ -170,8 +171,9 @@ public class ReflectiveIndexAccessor implements CompilableIndexAccessor {
}
catch (Exception ex) {
throw new IllegalArgumentException("Failed to find public write-method '%s(%s, %s)' in class '%s'."
.formatted(writeMethodName, getName(indexType), getName(indexedValueType),
getName(targetType)));
.formatted(writeMethodName, ClassUtils.getCanonicalName(indexType),
ClassUtils.getCanonicalName(indexedValueType),
ClassUtils.getCanonicalName(targetType)));
}
this.writeMethodToInvoke = ClassUtils.getPubliclyAccessibleMethodIfPossible(writeMethod, targetType);
ReflectionUtils.makeAccessible(this.writeMethodToInvoke);
@@ -271,10 +273,4 @@ public class ReflectiveIndexAccessor implements CompilableIndexAccessor {
mv.visitMethodInsn(opcode, classDesc, methodName, methodDescr, isInterface);
}
private static String getName(Class<?> clazz) {
String canonicalName = clazz.getCanonicalName();
return (canonicalName != null ? canonicalName : clazz.getName());
}
}