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